开源 java android app 开发(十五)绘图定义控件--仪表盘
文章的目的为了记录使用java 进行android app 开发学习的经历。本职为嵌入式软件开发,公司安排开发app,临时学习,完成app的开发。开发流程和要点有些记忆模糊,赶紧记录,防止忘记。
相关链接:
开源 java android app 开发(一)开发环境的搭建-CSDN博客
开源 java android app 开发(二)工程文件结构-CSDN博客
开源 java android app 开发(三)GUI界面布局和常用组件-CSDN博客
开源 java android app 开发(四)GUI界面重要组件-CSDN博客
开源 java android app 开发(五)文件和数据库存储-CSDN博客
开源 java android app 开发(六)多媒体使用-CSDN博客
开源 java android app 开发(七)通讯之Tcp和Http-CSDN博客
开源 java android app 开发(八)通讯之Mqtt和Ble-CSDN博客
开源 java android app 开发(九)后台之线程和服务-CSDN博客
开源 java android app 开发(十)广播机制-CSDN博客
开源 java android app 开发(十一)调试、发布-CSDN博客
开源 java android app 开发(十二)封库.aar-CSDN博客
推荐链接:
开源C# .net mvc 开发(一)WEB搭建_c#部署web程序-CSDN博客
开源 C# .net mvc 开发(二)网站快速搭建_c#网站开发-CSDN博客
开源 C# .net mvc 开发(三)WEB内外网访问(VS发布、IIS配置网站、花生壳外网穿刺访问)_c# mvc 域名下不可訪問內網,內網下可以訪問域名-CSDN博客
开源 C# .net mvc 开发(四)工程结构、页面提交以及显示_c#工程结构-CSDN博客
开源 C# .net mvc 开发(五)常用代码快速开发_c# mvc开发-CSDN博客
本章节主要内容是:自定义的仪表盘控件的使用,随机指向速度。
1.代码分析
2.所有源码
3.效果图
一、代码分析
DashboardView 自定义仪表盘视图详细分析
1. 类定义和常量声明
public class DashboardView extends View {// 默认颜色值常量private static final int DEFAULT_DIAL_COLOR = Color.parseColor("#3F51B5");private static final int DEFAULT_NEEDLE_COLOR = Color.parseColor("#FF4081");private static final int DEFAULT_TEXT_COLOR = Color.parseColor("#212121");private static final int DEFAULT_BG_COLOR = Color.parseColor("#FFFFFF");
功能分析:
继承自View基类,实现自定义视图
定义4个默认颜色常量,使用Material Design配色方案
Color.parseColor()将十六进制颜色字符串转换为整型颜色值
2. 成员变量定义
2.1 绘制对象
private Paint dialPaint; // 表盘圆弧画笔
private Paint needlePaint; // 指针画笔
private Paint textPaint; // 文字画笔
private Paint bgPaint; // 背景画笔
2.2 自定义属性
private String title = "仪表盘"; // 标题文本
private int dialColor; // 表盘颜色
private int needleColor; // 指针颜色
private int textColor; // 文字颜色
private int backgroundColor; // 背景颜色
2.3 数据范围
private float currentValue = 0; // 当前显示值
private float minValue = 0; // 最小值
private float maxValue = 100; // 最大值
private float targetValue = 0; // 动画目标值
2.4 动画控制
private boolean isAnimating = false; // 动画状态标志
private static final long ANIMATION_DURATION = 500; // 动画时长500ms
private long animationStartTime = 0; // 动画开始时间戳
3. 构造函数
3.1 单参数构造函数
public DashboardView(Context context) {super(context);init(null); // 调用初始化方法,无属性集
}
功能: 用于代码动态创建视图时的构造函数
3.2 双参数构造函数
public DashboardView(Context context, @Nullable AttributeSet attrs) {super(context, attrs);init(attrs); // 传入XML属性集
}
功能: 用于XML布局文件中引用的标准构造函数
3.3 三参数构造函数
public DashboardView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init(attrs); // 包含默认样式属性
}
功能: 支持样式属性的构造函数
4. init() 初始化函数
private void init(AttributeSet attrs) {// 获取自定义属性if (attrs != null) {TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.DashboardView);title = a.getString(R.styleable.DashboardView_title);dialColor = a.getColor(R.styleable.DashboardView_dialColor, DEFAULT_DIAL_COLOR);needleColor = a.getColor(R.styleable.DashboardView_needleColor, DEFAULT_NEEDLE_COLOR);textColor = a.getColor(R.styleable.DashboardView_textColor, DEFAULT_TEXT_COLOR);backgroundColor = a.getColor(R.styleable.DashboardView_backgroundColor, DEFAULT_BG_COLOR);a.recycle(); // 必须回收TypedArray} else {// 使用默认值dialColor = DEFAULT_DIAL_COLOR;// ... 其他颜色初始化}// 初始化表盘画笔dialPaint = new Paint(Paint.ANTI_ALIAS_FLAG); // 启用抗锯齿dialPaint.setColor(dialColor);dialPaint.setStyle(Paint.Style.STROKE); // 描边模式dialPaint.setStrokeWidth(10f); // 线宽10像素// 初始化指针画笔needlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);needlePaint.setColor(needleColor);needlePaint.setStyle(Paint.Style.FILL_AND_STROKE); // 填充和描边needlePaint.setStrokeWidth(5f); // 线宽5像素// 初始化文字画笔textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);textPaint.setColor(textColor);textPaint.setTextSize(40); // 文字大小40pxtextPaint.setTextAlign(Paint.Align.CENTER); // 文字居中对齐// 初始化背景画笔bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);bgPaint.setColor(backgroundColor);bgPaint.setStyle(Paint.Style.FILL); // 填充模式
}
功能分析:
属性解析: 使用TypedArray读取XML中定义的属性值
资源回收: 必须调用recycle()释放资源
画笔配置: 为不同绘制元素创建专用的Paint对象
抗锯齿: 所有画笔都启用抗锯齿以获得平滑效果5. onDraw() 核心绘制函数
@Override
protected void onDraw(Canvas canvas) {super.onDraw(canvas); // 调用父类绘制// 计算基本几何参数int width = getWidth();int height = getHeight();int centerX = width / 2; // 中心点X坐标int centerY = height / 2; // 中心点Y坐标 int radius = Math.min(width, height) / 2 - 20; // 表盘半径(留出边距)// 1. 绘制背景圆形canvas.drawCircle(centerX, centerY, radius + 20, bgPaint);// 2. 绘制表盘圆弧RectF oval = new RectF(centerX - radius, centerY - radius, centerX + radius, centerY + radius);canvas.drawArc(oval, 150, 240, false, dialPaint); // 从150°开始绘制240°圆弧// 3. 绘制刻度线和刻度值drawScale(canvas, centerX, centerY, radius);// 4. 绘制指针drawNeedle(canvas, centerX, centerY, radius);// 5. 绘制标题文字canvas.drawText(title, centerX, centerY + radius + 60, textPaint);// 6. 绘制当前数值(加大字号)textPaint.setTextSize(50); // 临时调整字号canvas.drawText(String.format("%.1f", currentValue), centerX, centerY + 20, textPaint);textPaint.setTextSize(40); // 恢复原字号// 7. 动画处理逻辑if (isAnimating) {long currentTime = System.currentTimeMillis();float elapsed = currentTime - animationStartTime; // 已过去的时间if (elapsed < ANIMATION_DURATION) {float progress = elapsed / ANIMATION_DURATION; // 动画进度(0-1)// 线性插值计算当前值currentValue = currentValue + (targetValue - currentValue) * progress;invalidate(); // 请求重绘,实现动画帧} else {currentValue = targetValue; // 动画结束,直接跳到目标值isAnimating = false; // 停止动画}}
}
6. drawScale() 刻度绘制函数
private void drawScale(Canvas canvas, int centerX, int centerY, int radius) {Paint scalePaint = new Paint(Paint.ANTI_ALIAS_FLAG);scalePaint.setColor(textColor);scalePaint.setTextSize(30);scalePaint.setTextAlign(Paint.Align.CENTER);// 绘制11个刻度(0-10,包含两端)for (int i = 0; i <= 10; i++) {// 计算刻度角度:起始150° + 每刻度24°(总240°范围)float angle = 150 + (i * 24);// 计算对应数值:最小值 + 等分比例float value = minValue + (maxValue - minValue) * i / 10;// 将角度转换为弧度double startAngleRad = Math.toRadians(angle);// 计算刻度线起点(向内偏移20px)int startX = centerX + (int) ((radius - 20) * Math.cos(startAngleRad));int startY = centerY + (int) ((radius - 20) * Math.sin(startAngleRad));// 计算刻度线终点(表盘边缘)int endX = centerX + (int) (radius * Math.cos(startAngleRad));int endY = centerY + (int) (radius * Math.sin(startAngleRad));// 绘制刻度线canvas.drawLine(startX, startY, endX, endY, scalePaint);// 计算刻度值文字位置(向内偏移50px)int textX = centerX + (int) ((radius - 50) * Math.cos(startAngleRad));int textY = centerY + (int) ((radius - 50) * Math.sin(startAngleRad)) + 10;// 绘制刻度数值(取整显示)canvas.drawText(String.valueOf((int)value), textX, textY, scalePaint);}
}
7. drawNeedle() 指针绘制函数
private void drawNeedle(Canvas canvas, int centerX, int centerY, int radius) {// 计算指针角度:150°对应minValue,390°对应maxValue(实际是150°+240°)float angle = 150 + (currentValue - minValue) / (maxValue - minValue) * 240;double angleRad = Math.toRadians(angle); // 转换为弧度// 计算指针终点坐标(向内偏移30px)int endX = centerX + (int) ((radius - 30) * Math.cos(angleRad));int endY = centerY + (int) ((radius - 30) * Math.sin(angleRad));// 绘制指针线段(从中心到终点)canvas.drawLine(centerX, centerY, endX, endY, needlePaint);// 绘制中心圆点(半径10px)canvas.drawCircle(centerX, centerY, 10, needlePaint);
}
8. setValue() 数值设置函数(带动画)
public void setValue(float value) {// 边界检查if (value < minValue) value = minValue;if (value > maxValue) value = maxValue;// 设置动画参数this.targetValue = value; // 目标值this.isAnimating = true; // 启动动画标志this.animationStartTime = System.currentTimeMillis(); // 记录开始时间invalidate(); // 触发重绘,启动动画
}
9. 其他设置函数
// 设置数值范围
public void setRange(float min, float max) {this.minValue = min;this.maxValue = max;invalidate(); // 重绘视图
}// 设置标题
public void setTitle(String title) {this.title = title;invalidate();
}// 颜色设置函数(都会更新对应画笔颜色并重绘)
public void setDialColor(int color) {this.dialColor = color;dialPaint.setColor(color);invalidate();
}// 获取当前值
public float getCurrentValue() {return currentValue;
}
10. 关键设计特点
10.1 动画实现机制
线性插值: 使用currentValue + (targetValue - currentValue) * progress计算过渡值
时间控制: 基于系统时间戳计算动画进度
连续重绘: 通过invalidate()在动画期间持续刷新视图
10.2 几何计算
角度映射: 将数值线性映射到150°-390°的角度范围
坐标转换: 使用三角函数计算圆周上的点坐标
自适应尺寸: 基于视图实际尺寸计算绘制参数
10.3 绘制层次
背景圆形
表盘圆弧
刻度线和数值
指针和中心点
标题和当前值文字
这个自定义视图实现了完整的仪表盘功能,具有良好的可定制性和平滑的动画效果
二、所有源码
文件结构,
DashboardView.java
MainActivity.java
activity_main.xml
attrs.xml
colors.xml
1.DashboardView.java源码
package com.example.dashboard;import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;public class DashboardView extends View {// 默认颜色值private static final int DEFAULT_DIAL_COLOR = Color.parseColor("#3F51B5");private static final int DEFAULT_NEEDLE_COLOR = Color.parseColor("#FF4081");private static final int DEFAULT_TEXT_COLOR = Color.parseColor("#212121");private static final int DEFAULT_BG_COLOR = Color.parseColor("#FFFFFF");// 绘制相关对象private Paint dialPaint;private Paint needlePaint;private Paint textPaint;private Paint bgPaint;// 自定义属性private String title = "仪表盘";private int dialColor;private int needleColor;private int textColor;private int backgroundColor;// 数据相关private float currentValue = 0;private float minValue = 0;private float maxValue = 100;private float targetValue = 0;// 动画相关private boolean isAnimating = false;private static final long ANIMATION_DURATION = 500; // 动画持续时间(ms)private long animationStartTime = 0;public DashboardView(Context context) {super(context);init(null);}public DashboardView(Context context, @Nullable AttributeSet attrs) {super(context, attrs);init(attrs);}public DashboardView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init(attrs);}private void init(AttributeSet attrs) {// 获取自定义属性if (attrs != null) {TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.DashboardView);title = a.getString(R.styleable.DashboardView_title);dialColor = a.getColor(R.styleable.DashboardView_dialColor, DEFAULT_DIAL_COLOR);needleColor = a.getColor(R.styleable.DashboardView_needleColor, DEFAULT_NEEDLE_COLOR);textColor = a.getColor(R.styleable.DashboardView_textColor, DEFAULT_TEXT_COLOR);backgroundColor = a.getColor(R.styleable.DashboardView_backgroundColor, DEFAULT_BG_COLOR);a.recycle();} else {dialColor = DEFAULT_DIAL_COLOR;needleColor = DEFAULT_NEEDLE_COLOR;textColor = DEFAULT_TEXT_COLOR;backgroundColor = DEFAULT_BG_COLOR;}// 初始化表盘画笔dialPaint = new Paint(Paint.ANTI_ALIAS_FLAG);dialPaint.setColor(dialColor);dialPaint.setStyle(Paint.Style.STROKE);dialPaint.setStrokeWidth(10f);// 初始化指针画笔needlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);needlePaint.setColor(needleColor);needlePaint.setStyle(Paint.Style.FILL_AND_STROKE);needlePaint.setStrokeWidth(5f);// 初始化文字画笔textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);textPaint.setColor(textColor);textPaint.setTextSize(40);textPaint.setTextAlign(Paint.Align.CENTER);// 初始化背景画笔bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);bgPaint.setColor(backgroundColor);bgPaint.setStyle(Paint.Style.FILL);}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);int width = getWidth();int height = getHeight();int centerX = width / 2;int centerY = height / 2;int radius = Math.min(width, height) / 2 - 20;// 绘制背景canvas.drawCircle(centerX, centerY, radius + 20, bgPaint);// 绘制表盘RectF oval = new RectF(centerX - radius, centerY - radius, centerX + radius, centerY + radius);canvas.drawArc(oval, 150, 240, false, dialPaint);// 绘制刻度drawScale(canvas, centerX, centerY, radius);// 绘制指针drawNeedle(canvas, centerX, centerY, radius);// 绘制标题canvas.drawText(title, centerX, centerY + radius + 60, textPaint);// 绘制当前值textPaint.setTextSize(50);canvas.drawText(String.format("%.1f", currentValue), centerX, centerY + 20, textPaint);textPaint.setTextSize(40);// 如果需要动画,继续重绘if (isAnimating) {long currentTime = System.currentTimeMillis();float elapsed = currentTime - animationStartTime;if (elapsed < ANIMATION_DURATION) {float progress = elapsed / ANIMATION_DURATION;currentValue = currentValue + (targetValue - currentValue) * progress;invalidate();} else {currentValue = targetValue;isAnimating = false;}}}private void drawScale(Canvas canvas, int centerX, int centerY, int radius) {Paint scalePaint = new Paint(Paint.ANTI_ALIAS_FLAG);scalePaint.setColor(textColor);scalePaint.setTextSize(30);scalePaint.setTextAlign(Paint.Align.CENTER);for (int i = 0; i <= 10; i++) {float angle = 150 + (i * 24);float value = minValue + (maxValue - minValue) * i / 10;// 计算刻度起点和终点double startAngleRad = Math.toRadians(angle);int startX = centerX + (int) ((radius - 20) * Math.cos(startAngleRad));int startY = centerY + (int) ((radius - 20) * Math.sin(startAngleRad));int endX = centerX + (int) (radius * Math.cos(startAngleRad));int endY = centerY + (int) (radius * Math.sin(startAngleRad));// 绘制刻度线canvas.drawLine(startX, startY, endX, endY, scalePaint);// 绘制刻度值int textX = centerX + (int) ((radius - 50) * Math.cos(startAngleRad));int textY = centerY + (int) ((radius - 50) * Math.sin(startAngleRad)) + 10;canvas.drawText(String.valueOf((int)value), textX, textY, scalePaint);}}private void drawNeedle(Canvas canvas, int centerX, int centerY, int radius) {// 计算指针角度 (150°到390°对应minValue到maxValue)float angle = 150 + (currentValue - minValue) / (maxValue - minValue) * 240;double angleRad = Math.toRadians(angle);// 计算指针终点int endX = centerX + (int) ((radius - 30) * Math.cos(angleRad));int endY = centerY + (int) ((radius - 30) * Math.sin(angleRad));// 绘制指针canvas.drawLine(centerX, centerY, endX, endY, needlePaint);// 绘制中心圆点canvas.drawCircle(centerX, centerY, 10, needlePaint);}// 设置当前值(带动画效果)public void setValue(float value) {if (value < minValue) value = minValue;if (value > maxValue) value = maxValue;this.targetValue = value;this.isAnimating = true;this.animationStartTime = System.currentTimeMillis();invalidate();}// 设置数值范围public void setRange(float min, float max) {this.minValue = min;this.maxValue = max;invalidate();}// 设置标题public void setTitle(String title) {this.title = title;invalidate();}// 设置表盘颜色public void setDialColor(int color) {this.dialColor = color;dialPaint.setColor(color);invalidate();}// 设置指针颜色public void setNeedleColor(int color) {this.needleColor = color;needlePaint.setColor(color);invalidate();}// 设置文字颜色public void setTextColor(int color) {this.textColor = color;textPaint.setColor(color);invalidate();}// 设置背景颜色public void setBackgroundColor(int color) {this.backgroundColor = color;bgPaint.setColor(color);invalidate();}public float getCurrentValue() {return currentValue;}
}
2.MainActivity.java源码
package com.example.dashboard;import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.os.Handler;
import android.widget.TextView;
import java.util.Random;public class MainActivity extends AppCompatActivity {private DashboardView dashboard;private TextView valueText;private Handler handler = new Handler();private Random random = new Random();private float minValue = 0;private float maxValue = 100;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);dashboard = findViewById(R.id.dashboard);valueText = findViewById(R.id.valueText);// 设置仪表盘属性dashboard.setTitle("速度仪表盘");dashboard.setDialColor(getResources().getColor(R.color.dialColor));dashboard.setNeedleColor(getResources().getColor(R.color.needleColor));dashboard.setTextColor(getResources().getColor(R.color.textColor));dashboard.setBackgroundColor(getResources().getColor(R.color.backgroundColor));dashboard.setRange(minValue, maxValue);// 开始定时更新数据startDataUpdates();}private void startDataUpdates() {handler.postDelayed(new Runnable() {@Overridepublic void run() {// 生成随机值float value = minValue + random.nextFloat() * (maxValue - minValue);// 更新仪表盘dashboard.setValue(value);// 更新文本显示valueText.setText(String.format("当前值: %.1f", value));// 1秒后再次更新handler.postDelayed(this, 1000);}}, 1000);}@Overrideprotected void onDestroy() {super.onDestroy();// 移除所有回调,防止内存泄漏handler.removeCallbacksAndMessages(null);}
}
3.activity_main.xml源码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"android:gravity="center"android:padding="16dp"tools:context=".MainActivity"><com.example.dashboard.DashboardViewandroid:id="@+id/dashboard"android:layout_width="300dp"android:layout_height="300dp"app:title="速度仪表盘"app:dialColor="#3F51B5"app:needleColor="#FF4081"app:textColor="#212121"app:backgroundColor="#FFFFFF" /><TextViewandroid:id="@+id/valueText"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="20dp"android:text="当前值: 0.0"android:textSize="18sp" /></LinearLayout>
4.attrs.xml源码用于主活动设置控件的属性
<?xml version="1.0" encoding="utf-8"?>
<resources><declare-styleable name="DashboardView"><attr name="title" format="string" /><attr name="dialColor" format="color" /><attr name="needleColor" format="color" /><attr name="textColor" format="color" /><attr name="backgroundColor" format="color" /></declare-styleable>
</resources>
5.colors.xml源码用途添加的各种颜色
<?xml version="1.0" encoding="utf-8"?>
<resources><color name="colorPrimary">#008577</color><color name="colorPrimaryDark">#00574B</color><color name="colorAccent">#D81B60</color><color name="purple_200">#FFBB86FC</color><color name="purple_500">#FF6200EE</color><color name="purple_700">#FF3700B3</color><color name="teal_200">#FF03DAC5</color><color name="teal_700">#FF018786</color><color name="black">#FF000000</color><color name="white">#FFFFFFFF</color><!-- 仪表盘自定义颜色 --><color name="dialColor">#3F51B5</color><color name="needleColor">#FF4081</color><color name="textColor">#212121</color><color name="backgroundColor">#FFFFFF</color></resources>
三、效果演示
主活动随机写入数据,指针指向仪表盘的相应位置。