android 自定义View 继承View
上篇关于自定义View的介绍
接下来将会针对自定义View三种情况一一实现。
源码地址
最后实现效果如下图:
继承View
创建一个class MyView 继承View
目标是写一个折线图
现在res/values下面新建一个attrs.xml文件,来申明我们需要的属性。
1 2 3 4 5 6 7 8 9 10 11
| <?xml version="1.0" encoding="utf-8"?> <resources>
<declare-styleable name="LineView"> <attr name="axieColor" format="color"/> <attr name="pointRadius" format="dimension" />
</declare-styleable>
</resources>
|
先新建一个clas 继承View, 并初始化几个构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public class MyView extends View { // 代码生成时,才会调用该构造函数 public MyView(Context context) { super(context); this.context = context; }
public MyView(Context context, AttributeSet attrs) { this(context, attrs, 0); } // xml配置时,会调用这个生命周期 public MyView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.context = context; initData(attrs); }
// 做初始化配置 获取各种颜色、尺寸,还有自定义的一些属性。 private void initData(AttributeSet attrs) { Log.d(TAG, "initData: "); // 获取xml中配置的数据 TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MyView); paintColor = array.getColor(R.styleable.MyView_axieColor, context.getResources().getColor(R.color.black));
// 画笔初始化 paint = new Paint(); paint.setColor(context.getResources().getColor(R.color.black)); paint.setTextSize(40); paint.setStrokeWidth(10); // 线条粗细 } }
|
在onMeasure函数中对尺寸做约束
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
|
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); Log.d(TAG, "onMeasure: "); int height = measuretDimension(defaultHeight, heightMeasureSpec); int width = measuretDimension(0, widthMeasureSpec); top = 0; left = 0; bottom = top + height; right = left + width; setMeasuredDimension(width, height);
}
/** * 测量实际尺寸 * @param defaultSize: 默认尺寸 * @param measureSpec: 测量规格 * @return */ public int measureDimension(int defaultSize, int measureSpec) { int resultSize = defaultSize; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { // 没有做限制,取默认值 case MeasureSpec.UNSPECIFIED: resultSize = defaultSize; break;
// WRAP case MeasureSpec.AT_MOST: // 要取默认值和测量值中较小值 // 当默认值为0时,取最大值, 即宽充满屏幕 resultSize = defaultSize == 0 ?specSize : Math.min(defaultSize, specSize); break;
// 具体值 或 MATCH case MeasureSpec.EXACTLY: resultSize = specSize; break;
default: break; } return resultSize; }
|
先绘制两个轴线
注意canvas.draw..()方法,中的位置参数都是相对定位尺寸,都是相对于该视图左上角的坐标定位,而onLayout中的四个位置参数都是相对于屏幕左上角。
这两个里的坐标不要弄混。
1 2 3 4 5 6 7 8
| /** 绘制两条轴线 */ private void drawXY(Canvas canvas) { Log.d(TAG, "drawXY: "); // 绘制x轴 canvas.drawLine(left + 20, bottom, right, bottom, paint); // 绘制y轴 canvas.drawLine(left + 20, top, left + 20, bottom, paint); }
|
在MainActivity中配置该视图
1 2 3 4 5 6 7 8 9
| <com.justin.customview.MyView android:layout_width="match_parent" android:layout_height="100dp" app:layout_constraintTop_toBottomOf="@+id/text_hello" android:layout_marginTop="20dp" android:padding="10dp" app:axieColor="@color/black" android:id="@+id/myView" />
|
我们直接运行,效果如下:
x,y轴就画好了。但很明显我们设置的padding没起作用,因为padding是需要我们自己处理的。
新增一个方法初始化这些尺寸数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| /** * 尺寸数据初始化 */ private void initSize () { // 获取padding尺寸 paddingLeft = getPaddingLeft(); paddingTop = getPaddingTop(); paddingRight = getPaddingRight(); paddingBottom = getPaddingBottom(); StringBuilder sb = new StringBuilder(); sb.append("paddingLeft =").append(paddingLeft) .append("paddingTop =").append(paddingTop) .append("paddingRight =").append(paddingRight) .append("paddingBottom =").append(paddingBottom); Log.d(TAG, "initSize: ".concat(sb.toString())); top = paddingTop; left = paddingLeft; bottom = height - top - paddingBottom; right = width - left; setMeasuredDimension(width, height); }
|
这样我们就对padding做了处理,接下来接着绘制我们需要的图形。
我们之前绘制了两个轴,xy,但我们平常绘制的轴都会有箭头,我们再在x轴右侧、y轴上侧绘制两个箭头
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| private void drawArrow(Canvas canvas) { Path path = new Path(); // 先绘制x轴三角 //先移动到三角形一个点 path.moveTo(right-20, bottom + 20); path.lineTo(right-20, bottom - 20); // 画线 path.lineTo(right, bottom); // 画线 path.close(); // 图形闭合 canvas.drawPath(path, paint);
// 绘制y轴三角 path.moveTo(left - 20, top + 20); path.lineTo(left + 20, top + 20); path.lineTo(left, top); path.close(); canvas.drawPath(path, paint); }
|
这里主要用到了drawPath函数。其实canvas对象还有很多其他的绘制图形的方法。
设置数据并绘制点
我们已经完成了绘制两条轴线,现在要开始绘制数据了。
首先我们要确认标准线,x轴的标准线肯定就是xValue的值,
但y轴的标准线是不定的,我们要先找出最大值,确定几条标准线,确定每条标准线的值。
我们先假设我们的值在0-100以内,取5条标准线,每条间距20.
先设置两个数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| // 数据 private float[] yValue; private String[] xValue; private int lineNum = 5;
/** 设置数据并刷新 */ public void setData(float[]yValue, String[]xValue) { this.yValue = yValue; this.xValue = xValue; postInvalidate(); }
/** 设置标准线数目 */ public void setData(int lineNum) { this.lineNum = lineNum; postInvalidate(); }
|
然后我们开始绘制标准线、各个点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| /** 绘制各个点 */ private void drawPoint(Canvas canvas) { if(xValue == null || yValue == null) return; // 先绘制5条y轴标准线位置 取高度的90%作为图线的最高。 float maxHeight = (float)((bottom - top) * 0.9); float itemHeight = maxHeight / 5; for(int i = 1; i <=5; i ++) { canvas.drawLine(left, bottom - itemHeight * i, left + 15, bottom - itemHeight * i, paint); } // 再绘制x轴的数据, x轴线的标准值就是x轴的值,数目也是xValue的值 float maxWidth = (float)((right - left) * 0.9); float itemWidth = ((float) (maxWidth * 1.0)) / xValue.length; for (int i = 1; i <= xValue.length; i ++) { float x = left + itemWidth * i; // 绘制轴线 canvas.drawLine(x, bottom, x, bottom - 15, paint); // 绘制点 float y = bottom - maxHeight * yValue[i-1] / yMax; canvas.drawCircle(x, y, 5, paint); } }
|
而在MainActivity.kt中,我们可以这样使用
1 2 3 4 5 6 7 8 9 10 11
| // kotlin语法 class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) var xValue = arrayOf("一", "二", "san") var yValue = floatArrayOf(70f, 80f, 90f); myView.setData(yValue, xValue); } }
|
此时的实现效果:
现在我们完成了图形的大致绘制,但在x、y轴却没有一些文字说明,接下来我们就要加上这些
绘制x、y轴标准线值,将各个点连接起来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| /** 绘制各个点 */ private void drawPoint(Canvas canvas) { if(xValue == null || yValue == null) return; // 先绘制5条y轴标准线位置 取高度的90%作为图线的最高。 float maxHeight = (float)((bottom - top) * 0.9); float itemHeight = maxHeight / 5; int itemValue = yMax / 5; for(int i = 1; i <=5; i ++) { canvas.drawLine(left, bottom - itemHeight * i, left + 15, bottom - itemHeight * i, paint); // 绘制y轴标准值 canvas.drawText(itemValue * i + "", left - 90, bottom - itemHeight * i, paint); }
// 再绘制x轴的数据, x轴线的标准值就是x轴的值,数目也是xValue的值 float maxWidth = (float)((right - left) * 0.9); float itemWidth = ((float) (maxWidth * 1.0)) / xValue.length; for (int i = 1; i <= xValue.length; i ++) { float x = left + itemWidth * i; // 绘制轴线 canvas.drawLine(x, bottom, x, bottom - 15, paint); // 绘制点 float y = bottom - maxHeight * yValue[i-1] / yMax; canvas.drawCircle(x, y, 10, paint);
// 绘制点与点之间的连线 if(lastX > 0f) { canvas.drawLine(lastX, lastY, x, y, paint); }
// 绘制x轴标准值 canvas.drawText(xValue[i-1], x, bottom + 50, paint);
lastX = x; lastY = y; } }
|
最后实现效果如下图: