1. 简述
建议:熟悉View的绘制、Android事件传递、手势处理再常用自绘式。
方式:直接继承View ,重写绘制流程三步骤
measure() 测量 layout() 布局计算摆放坐标 draw() 绘制
2. 前提基础
2.1了解View关联Activity时的生命周期
可参考 : Android自定义View(1):基础
2.2 了解自定义View的自定义属性,以及获取、使用
可参考 : Android自定义View(1):基础
2.3 重要的生命周期函数
- 1.onAttachedToWindow()
表示自身View 被父View通过addView添加到窗口中。 在这里,可以通过id获取同一布局文件的其他兄弟View。
- 2.onMeasure()
作用:测量View的宽高。 Android中,View使用MeasureSpec存放尺寸数据:
MeasureSpec = mode (前2位值)+ size (后30位值)
MeasureSpec的值保存在一个int值当中。一个int值有32位,前两位表示模式mode后30位表示大小size
| mode | 比较 | 设置值 |
|---|---|---|
| EXACTLY | 精准模式,View需要一个精确值 赋予 MeasureSpec的Size | 50dp 、 match_parent |
| AT_MOST | 最大模式,View的尺寸有一个最大值,View不可以超过MeasureSpec当中的Size值 | wrap_content |
| UNSPECIFIED | 无限制,原生View自用,一般不用。 | - |
正确设置自定义View尺寸
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//==============================================
//========= 设置自定义View的默认大小 ==============
//==============================================
//取出值
int width = getCalSize(100, widthMeasureSpec);
int height = getCalSize(100, heightMeasureSpec);
//处理值
//设置
setMeasuredDimension(width, height);
}
private int getCalSize(int defaultSize, int measureSpec) {
int calSize = defaultSize;
int mode = MeasureSpec.getMode(measureSpec);//获取模式
int size = MeasureSpec.getSize(measureSpec);//获取大小
switch (mode) {
case MeasureSpec.UNSPECIFIED: //未指定,就设置为默认大小
calSize = defaultSize;
break;
case MeasureSpec.AT_MOST: //最大取值,将大小取最大值,也可设置自定义View的mMaxHeight / mMaxWidth
calSize = size;
break;
case MeasureSpec.EXACTLY: //指定值
calSize = size;
break;
default:
break;
}
return calSize;
}
- 3.onLayout()
此方法用于分配子Viewde 布局位置、大小,【组合式】自定义View按需重写,在【自绘式】自定义View中不需要重写。
- 4.onDraw()
使用Canvas【画布】和Paint【画笔】绘制所需View样式UI
注意 onDraw()是耗时方法,Google官方不建议在该方法内声明新对象,否则会产生作用重复的对象,拉低运行效率。本质就是开辟过多【用完不要】的内存空间,最后得GC回收,影响绘制、运行。
正确用法
//1.声明重复使用的画笔
private Paint mPaint;
//2.初始化
public AlwaysMarqueeTextView(Context context,.......) {
super(context, attrs);
mPaint = new Paint();
}
//3.多次使用
@Override
protected void onDraw(Canvas canvas) {
//添加下划线
super.onDraw(canvas);
mPaint.setColor(Color.GREEN);
canvas.drawLine( 0 ,getHeight()/2 ,getWidth() ,getHeight()/2 , mPaint);
}
2.4 View的自刷新
-
1.invalidate() 与 requestLayout区别 | 重绘 | 比较 | | ---- | --- | | invalidate() | 轻量刷新,更新文本、颜色、状态等,只调用 三大周期方法之一 onDraw() | | requestLayout | 重量、约等于重新绘制 重新调用onMeasure() onLayout() onDraw() 绘制 |
-
2.View自刷新动画
int mViewRadius = 0;
int mViewRadiusReal = 50;
private void anim(){
ValueAnimator anim = ValueAnimator.ofInt(0, 100);
anim.setDuration(200);
anim.setInterpolator(new DecelerateInterpolator());
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
int newRadius = (int) animation.getAnimatedValue();
mViewRadius = mViewRadiusReal * newRadius /100;
invalidate();
}
});
}
3. 实践
该例未完善,仅供学习,如需使用,需自行扩展
3.1声明自定义属性
/res/values/attrs.xml文件新增
<resources>
<!--xml快捷属性-->
<declare-styleable name="DiyView">
<attr name="chart_x_color" format="color" />
<attr name="chart_line_color" format="color" />
<attr name="chart_line_width" format="dimension"/>
<attr name="chart_line_space" format="dimension"/>
</declare-styleable>
</resources>
3.2 自定义View类java文件
package com.cupster.base_super_resource;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import java.util.Map;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
public class DiyView extends View {
public DiyView(Context context) {
super(context);
}
public DiyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initAttrs(context, attrs);
mPaint = new Paint();
mPath = new Path();
}
public DiyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initAttrs(context, attrs);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public DiyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initAttrs(context, attrs);
}
private Paint mPaint;
private Path mPath;
private int chart_x_color = Color.parseColor("#808080");
private int chart_line_color = Color.parseColor("#6ce4d8");
private float chart_line_width = 10.0f;
private float space = 10.0f;
private float zeroX = 120f;
private float zeroY = 120f;
private float axisY = 500f;
private float axisX = 900f;
private float scalX = 1.0f;//TODO 扩展:数据缩放呈现
private float scalY = 1.0f;//TODO 扩展:数据缩放呈现
private float[] lines = new float[]{0,0,0,0,0,0};
private String[] xLabs = new String[]{"1月","2月","3月","4月","5月","6月"};
private void initAttrs(Context context, AttributeSet attrs) {
//1.获取属性数组
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.DiyView);
//2.获取属性值
chart_x_color = typedArray.getColor(R.styleable.DiyView_chart_x_color, Color.parseColor("#404040"));
chart_line_color = typedArray.getColor(R.styleable.DiyView_chart_line_color, Color.parseColor("#0000ee"));
chart_line_width = typedArray.getDimension(R.styleable.DiyView_chart_line_width, 10.0f);
space = typedArray.getDimension(R.styleable.DiyView_chart_line_space, 10.0f);
typedArray.recycle();
//初始化假数据
// lines = new float[]{
// //view的左上角为坐标系的(0,0)
// //line1: x1:【原点x轴坐标值zeroX + 间距space +柱形宽的一半值chart_line_width/2】,y1:【原点y轴坐标值=y轴高度值axisY+y轴顶点在View坐标系的Y轴值zeroY】
// // x2:【原点x轴坐标值zeroX + 间距space +柱形宽的一半值chart_line_width/2】,y2:【原点y轴坐标值 - 要绘制的条形图高度-此处为假数据】
// zeroX + space + chart_line_width / 2, axisY + zeroY, zeroX + space + chart_line_width / 2, axisY + zeroY - 20 * 1,
// //line2: x1:【原点x轴坐标值zeroX + 间距space +柱形宽的一半值chart_line_width/2 +[第一个条形图的宽chart_line_width+第二个间距space] 】,y1:【原点y轴坐标值=y轴高度值axisY+y轴顶点在View坐标系的Y轴值zeroY】
// // x2 = x1 ,y2 = axisY+zeroY - 要绘制的高度值
// zeroX + space + chart_line_width + space + chart_line_width / 2, axisY + zeroY, zeroX + space + chart_line_width + space + chart_line_width / 2, axisY + zeroY - 20 * 2,
// zeroX + space + chart_line_width + space + chart_line_width + space + chart_line_width / 2, axisY + zeroY, zeroX + space + chart_line_width + space + chart_line_width + space + chart_line_width / 2, axisY + zeroY - 20 * 3,
// zeroX + space + chart_line_width * 3 + space * 3 + chart_line_width / 2, axisY + zeroY, zeroX + space + chart_line_width * 3 + space * 3 + chart_line_width / 2, axisY + zeroY - 20 * 4,
// zeroX + space + chart_line_width * 4 + space * 4 + chart_line_width / 2, axisY + zeroY, zeroX + space + chart_line_width * 4 + space * 4 + chart_line_width / 2, axisY + zeroY - 20 * 5,
// zeroX + space + chart_line_width * 5 + space * 5 + chart_line_width / 2, axisY + zeroY, zeroX + space + chart_line_width * 5 + space * 5 + chart_line_width / 2, axisY + zeroY - 20 * 6,
// zeroX + space + chart_line_width * 6 + space * 6 + chart_line_width / 2, axisY + zeroY, zeroX + space + chart_line_width * 6 + space * 6 + chart_line_width / 2, axisY + zeroY - 20 * 7,
// zeroX + space * (1 + 7) + chart_line_width * 7 + chart_line_width / 2, axisY + zeroY, zeroX + space * (1 + 7) + chart_line_width * 7 + chart_line_width / 2, axisY + zeroY - 20 * 8,
// //line9: x1:【原点x轴坐标值zeroX + 间距space * n +柱形宽的一半值chart_line_width *(n-1)+柱形宽的一半值chart_line_width/2 】,y1:【原点y轴坐标值=y轴高度值axisY+y轴顶点在View坐标系的Y轴值zeroY】
// // x2 = x1 ,y2 = axisY+zeroY - 要绘制的高度值
// zeroX + space * (9) + chart_line_width * (9 - 1) + chart_line_width / 2, axisY + zeroY, zeroX + space * (9) + chart_line_width * (9 - 1) + chart_line_width / 2, axisY + zeroY - 20 * 9,
// zeroX + space * 10 + chart_line_width * (10 - 1) + chart_line_width / 2, axisY + zeroY, zeroX + space * (10) + chart_line_width * (10 - 1) + chart_line_width / 2, axisY + zeroY - 20 * 10,
// zeroX + space * 11 + chart_line_width * (11 - 1) + chart_line_width / 2, axisY + zeroY, zeroX + space * (11) + chart_line_width * (11 - 1) + chart_line_width / 2, axisY + zeroY - 20 * 11,
// zeroX + space * 12 + chart_line_width * (12 - 1) + chart_line_width / 2, axisY + zeroY, zeroX + space * (12) + chart_line_width * (12 - 1) + chart_line_width / 2, axisY + zeroY - 20 * 12
// //lineX:最终固定公式为
// // //line9: x1:【zeroX + space * n +chart_line_width*(n-0.5f) 】,y1:【axisY+zeroY】
// // x2 = x1 ,y2 = axisY+zeroY - 要绘制的高度值
// // x1 y1 x2 y2
// , zeroX + space * 13 + chart_line_width * 12.5f, axisY + zeroY, zeroX + space * 13 + chart_line_width * 12.5f, axisY + zeroY - 111
// };
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
Log.d("DiyView", "onAttachedToWindow");
}
// @Override
// protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// Log.d("DiyView","onMeasure");
// //==============================================
// //========= 设置自定义View的默认大小 ==============
// //==============================================
// //取出值
// int width = getCalSize(600, widthMeasureSpec);
// int height = getCalSize(300, heightMeasureSpec);
//
// //TODO 处理值
//
// //设置
// setMeasuredDimension(width, height);
// }
//
// private int getCalSize(int defaultSize, int measureSpec) {
// int calSize = defaultSize;
//
// int mode = MeasureSpec.getMode(measureSpec);//获取模式
// int size = MeasureSpec.getSize(measureSpec);//获取大小
//
// switch (mode) {
// case MeasureSpec.UNSPECIFIED: //未指定,就设置为默认大小
// calSize = defaultSize;
// break;
//
// case MeasureSpec.AT_MOST: //最大取值,将大小取最大值,也可设置自定义View的mMaxHeight / mMaxWidth
// calSize = size;
// break;
//
// case MeasureSpec.EXACTLY: //指定值
// calSize = size;
// break;
// default:
// break;
// }
// return calSize;
// }
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
Log.d("DiyView", "onLayout");
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.d("DiyView", "onDraw");
mPaint.reset();
mPath.reset();
//1.画坐标轴
mPaint.setColor(Color.parseColor("#404040"));
mPaint.setStyle(Paint.Style.STROKE);
mPath.moveTo(zeroX, zeroY);//y轴顶点(120,120)
mPath.rLineTo(0, axisY);//y轴方向
mPath.rLineTo(axisX, 0);//x轴方向
canvas.drawPath(mPath, mPaint);//绘制
//2.画x轴label
mPaint.reset();
mPaint.setColor(chart_x_color);
mPaint.setTextSize(18);//px
mPaint.setStyle(Paint.Style.FILL);
//假数据
// canvas.drawText("1月", zeroX + space, zeroY + axisY + 28, mPaint);
// canvas.drawText("2月", zeroX + space * 2 + chart_line_width * (2 - 1), zeroY + axisY + 28, mPaint);
// canvas.drawText("3月", zeroX + space * 3 + chart_line_width * (3 - 1), zeroY + axisY + 28, mPaint);
// canvas.drawText("4月", zeroX + space * 4 + chart_line_width * (4 - 1), zeroY + axisY + 28, mPaint);
// canvas.drawText("5月", zeroX + space * 5 + chart_line_width * (5 - 1), zeroY + axisY + 28, mPaint);
// canvas.drawText("6月", zeroX + space * 6 + chart_line_width * (6 - 1), zeroY + axisY + 28, mPaint);
// canvas.drawText("7月", zeroX + space * 7 + chart_line_width * (7 - 1), zeroY + axisY + 28, mPaint);
// canvas.drawText("8月", zeroX + space * 8 + chart_line_width * (8 - 1), zeroY + axisY + 28, mPaint);
// canvas.drawText("9月", zeroX + space * 9 + chart_line_width * (9 - 1), zeroY + axisY + 28, mPaint);
// canvas.drawText("10月", zeroX + space * 10 + chart_line_width * (10 - 1), zeroY + axisY + 28, mPaint);
// canvas.drawText("11月", zeroX + space * 11 + chart_line_width * (11 - 1), zeroY + axisY + 28, mPaint);
// canvas.drawText("12月", zeroX + space * 12 + chart_line_width * (12 - 1), zeroY + axisY + 28, mPaint);
for (int i = 0 ; i < xLabs.length ; i++){
canvas.drawText(xLabs[i], zeroX + space*(i+1) + i*chart_line_width, zeroY + axisY + 28, mPaint);
}
//3.画条形图
if (lines==null || lines.length<1){
return;
}
mPaint.reset();
mPaint.setColor(chart_line_color);
mPaint.setStrokeWidth(chart_line_width);
canvas.drawLines(lines, mPaint);
}
public void setHeightArr(Map<String ,Float> datas) {
//0.判空
if (datas == null || datas.size() < 1) {
return;
}
//1.取数据
float[] arr = new float[datas.size()];
xLabs = new String[datas.size()];
int i = 0;
for (Map.Entry entry : datas.entrySet()){
xLabs[i] = entry.getKey()==null ? "":(String) entry.getKey();
arr[i] = entry.getValue() ==null? 0.0f :(float) entry.getValue();
i++;
}
lines = new float[arr.length * 4];
//2.检查最大值,变更Y轴上限
for (int n = 0; n < arr.length; n++) {
if (arr[n] > axisY) {
axisY = arr[n] +20;
}
}
//3.装载条形图数据
for (int n = 0; n < arr.length; n++) {
lines[4*n] = zeroX + space * (n+1) + chart_line_width * ((n+1) - 1 + 0.5f);
lines[1 + 4*n] = axisY + zeroY;
lines[2 + 4*n] = zeroX + space * (n+1) + chart_line_width * ((n+1) - 1 + 0.5f);
lines[3 + 4*n] = axisY + zeroY - arr[n];
}
//通知更新View
invalidate();
}
}
3.3 布局文件使用
<com.cupster.base_super_resource.DiyView
android:id="@+id/diy_chart"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:chart_line_color="#3ca1f9"
app:chart_line_width="12dp"
app:chart_line_space="5dp"
app:chart_x_color="#ff8696"
/>
3.4 Activity中使用
package com.cupster.base_super_resource;
import android.os.Bundle;
import android.util.ArrayMap;
import java.util.Map;
import androidx.appcompat.app.AppCompatActivity;
public class BaseSuperResourceActivity extends AppCompatActivity {
DiyView diyChart;
Map<String ,Float> datas = new ArrayMap<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_base_super_resource);
diyChart = findViewById(R.id.diy_chart);
datas.put("1月",100f);
datas.put("2月",200f);
datas.put("3月",300f);
datas.put("4月",500f);
datas.put("5月",50f);
datas.put("6月",400f);
datas.put("7月",200f);
datas.put("8月",30f);
datas.put("9月",180f);
datas.put("10月",330f);
datas.put("11月",20f);
datas.put("12月",400f);
diyChart.postDelayed(new Runnable() {
@Override
public void run() {
diyChart.setHeightArr(datas);
}
},5000);
}
}