自定义View是每个开发者绕不过去的一座山,高山仰止,不管看过多少的技术博客,都需要真正动手敲上一遍才能真正看到高处的风景,今天,趁着业务需求,就顺手来写一个基础入门的自定义View。初步完成效果如下:
这样的图形基本上在页面顶部会使用到,相对而言使用到的技术点较少,很适合用来做学习项目。现在,让我们一步一步来拆分者个View吧。
1.分析
这个View我们可以把它分为三层,第一层为一个纯色矩形,第二层为从左到右依次排列的多个小矩形,帝三层为裁切层,即上,左,右三条边为直线,下边为弧线的特殊图形。
这么一分析,我们发现,只需要在2个方法里进行操作就可以实现我们的所有操作:onMeasure(),onDraw().
onMeasure()用于测量View的宽度和高度,方便后续的绘制。
onDraw()是这个View的重中之重。
2.正式开始编写代码
我们先定义自定义View的class文件,继承View,重写相关的构造函数(重点是2个参数的构造函数,必须)
2.1 自定义VIew 的属性
经过第一步的分析,有几个属性是需要在布局里进行赋值的,这些属性我们都定义在attr.xml中:
2.2在zidingView中获取我们的自定义属性的相关值
现在让我们回到代码中,获取那些自定义的属性值:
在构造函数中有个AttributeSet对象,这里就包含了所有我们需要的东西。
以及在onMeasure获取到当前View宽高(相对而言很简单):
2.3开始绘制
准备工作已经做好,让我们来到onDraw()里进行绘制。
2.3.1 绘制背景矩形
/**
* 绘制一个背景 矩形
*/
private void drawMain() {
// 设置颜色
mPaint.setColor(mainColor);
// 设置填充样式 为填满
mPaint.setStyle(Paint.Style.FILL);
// 规定矩形相对于当前View 上,下,左,右的距离
RectF rect = new RectF();
rect.left = 0;
rect.top = 0;
rect.right = width;
rect.bottom = height;
if (radios == 0f) {
// 无圆角矩形
mCanvas.drawRect(rect, mPaint);
} else {
// 带圆角矩形
mCanvas.drawRoundRect(rect, radios, radios, mPaint);
}
}
2.3.2 绘制条纹(即多个小矩形):所用到的api跟上述一致,很好理解吧。
private void drwaLine() {
mPaint.setColor(lineColor);
mPaint.setStyle(Paint.Style.FILL);
boolean flag = true;
int i = 0;
while (flag) {
RectF rectF = new RectF();
rectF.left = lineWidth * (2 * i + 1);
rectF.right = lineWidth * (2 * i + 2);
rectF.top = 0;
rectF.bottom = height;
mCanvas.drawRect(rectF, mPaint);
if (lineWidth * (2 * i + 1) > width) {
break;
}
i++;
}
}
2.3.3 绘制不规则图形,并对原有的图形进行裁剪 。先看代码:
这里我们引入了一个新的对象Path,不规则图形的使用就是基于它来实现。基于Path我们可以实现很多奇妙的效果,在这里我们先练一下手。
然后我们在onDraw()里依次调用这几个方法:
现在,自定义View已经写好了,我们去布局里使用:
运行结果:(这里我们指定了圆角为20dp)
是不是很简单?
但是可能有人会说:啊,好麻烦,老子画个图还要考虑这么多属性,就不能直接用一个图形去裁剪另一个图形么?难道Android没有这样的api么?
不要着急啊,少年,继续向下看:canva.ClipXXX()系列方法就完全可以这样的效果:
/**
* 真裁剪
*/
private void drawArcRect() {
//从当前位置到目标点(x,y) 用直线连起 即左边直线
path.lineTo(0, height * multiple);
// 从当前位置到目标点(x2,y2)做一条贝塞尔曲线,控制点为(x1,y1) 即底部曲线
path.quadTo(width / 2, height, width, height * multiple);
// 绘制右边直线
path.lineTo(width, 0);
// 将图形封闭,只有当style为Fill时生效,等同于 path.lineTo(0,0);
path.close();
// 重要 裁剪
mCanvas.clipPath(path);
}
还没完呢,裁剪的操作比较奇妙,需要我们在裁剪之前进行视图的保存,在裁剪,绘制之后进行视图的恢复,因此,我们改变了onDraw()里图层的绘制顺序:
到了这一步,我们的View就算是大功告成了!
附完整的代码:
package com.zyp.kotlin.views;
import android.annotation.SuppressLint;
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.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
import com.zyp.kotlin.R;
/**
* 双色 条纹 view/可只使用单个颜色
* Created by zhangyanpeng on 2020/5/28
*/
public class AnnieTwoLineView extends View {
private Context context;
/**
* 控件宽高
*/
private int height, width;
/**
* 背景颜色
*/
@SuppressLint("ResourceAsColor")
private int mainColor;
/**
* 条纹颜色
*/
@SuppressLint("ResourceAsColor")
private int lineColor;
/**
* 条纹宽度 px
*/
private float lineWidth;
/**
* 圆角
*/
private float radios;
private Canvas mCanvas;
private Paint mPaint = new Paint();
private Path path = new Path();
public AnnieTwoLineView(Context context) {
this(context, null);
}
public AnnieTwoLineView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
this.context = context;
}
public AnnieTwoLineView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.AnnieTwoLineView);
lineWidth = typedArray.getDimension(R.styleable.AnnieTwoLineView_lineWidth, 0f);
mainColor = typedArray.getColor(R.styleable.AnnieTwoLineView_mainColor, getResources().getColor(R.color.colorWhite));
lineColor = typedArray.getColor(R.styleable.AnnieTwoLineView_lineColor, getResources().getColor(R.color.colorAccent));
radios = typedArray.getDimension(R.styleable.AnnieTwoLineView_rectRadios, 0);
typedArray.recycle();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 测量宽度和高度
width = MeasureSpec.getSize(widthMeasureSpec);
height = MeasureSpec.getSize(heightMeasureSpec);
// 方便生效
setMeasuredDimension(width, height);
}
/**
* 绘制一个背景 矩形
*/
private void drawMain() {
// 设置颜色
mPaint.setColor(mainColor);
// 设置填充样式 为填满
mPaint.setStyle(Paint.Style.FILL);
// 规定矩形相对于当前View 上,下,左,右的距离
RectF rect = new RectF();
rect.left = 0;
rect.top = 0;
rect.right = width;
rect.bottom = height;
if (radios == 0f) {
// 无圆角矩形
mCanvas.drawRect(rect, mPaint);
} else {
// 带圆角矩形
mCanvas.drawRoundRect(rect, radios, radios, mPaint);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
this.mCanvas = canvas;
// 图像保存
canvas.save();
drawArcRect();
drawMain();
if (lineWidth > 0) {
drwaLine();
}
// 图像恢复
canvas.restore();
}
private void drwaLine() {
mPaint.setColor(lineColor);
mPaint.setStyle(Paint.Style.FILL);
boolean flag = true;
int i = 0;
while (flag) {
RectF rectF = new RectF();
rectF.left = lineWidth * (2 * i + 1);
rectF.right = lineWidth * (2 * i + 2);
rectF.top = 0;
rectF.bottom = height;
mCanvas.drawRect(rectF, mPaint);
if (lineWidth * (2 * i + 1) > width) {
break;
}
i++;
}
}
/**
* 绘制下边界为弧线的矩形
*/
private float multiple = 0.7f;
/**
* 真裁剪
*/
private void drawArcRect() {
//从当前位置到目标点(x,y) 用直线连起 即左边直线
path.lineTo(0, height * multiple);
// 从当前位置到目标点(x2,y2)做一条贝塞尔曲线,控制点为(x1,y1) 即底部曲线
path.quadTo(width / 2, height, width, height * multiple);
// 绘制右边直线
path.lineTo(width, 0);
// 将图形封闭,只有当style为Fill时生效,等同于 path.lineTo(0,0);
path.close();
// 重要 裁剪
mCanvas.clipPath(path);
}
/**
* 第一种“裁剪方案”假裁剪
* @param dpValue
* @return
*/
// private void drawArcRect() {
//// 重要,此颜色可以作为自定义View的属性进行,方便与布局的背景协调
// mPaint.setColor(Color.WHITE);
// mPaint.setStyle(Paint.Style.FILL);
// //从当前位置到目标点(x,y) 用直线连起 即左边直线
// path.lineTo(0, height * multiple);
//// 从当前位置到目标点(x2,y2)做一条贝塞尔曲线,控制点为(x1,y1) 即底部曲线
// path.quadTo(width / 2, height, width, height * multiple);
//// 绘制右边直线
// path.lineTo(width, 0);
//// 将图形封闭,只有当style为Fill时生效,等同于 path.lineTo(0,0);
// path.close();
//// 重要,设置当前View与之前图形的交叠之后的显示方式
// path.setFillType(Path.FillType.INVERSE_WINDING);
//// 重要 绘制
// mCanvas.drawPath(path,mPaint);
// }
private float dp2px(float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (dpValue - 0.5f) * scale;
}
}