我的新书《Android App开发入门与实战》已于2020年8月由人民邮电出版社出版,欢迎购买。点击进入详情
概述
android自定义View是一种很常见的使用方式。系统自带的widget往往不能满足我们的需求,这个时候就需要我们自己动手实现View控件了。
自定义View有很多种实现方式:
- 继承系统View控件,比如TextView等;
- 多个控件组合成一个新的控件,比如继承LineaLayout;
- 继承View控件,根据View的绘制流程重新进行实现;
另外,我们实现自定义View的好处有哪些呢?
自定义View实际上也就是一个功能类,除了绘制UI以外,还可以封装一定的业务逻辑,这样我们只需要对外提供一些接口,调用方就像使用其它类一样调用即可。
举个例子,我们常见的adapter的item布局中:
左边的图片和右边的文字可以做成一个新的View控件,这样如果调用的地方多了,可以节省不少的代码量,因为实现的逻辑都在新的控件中处理了。
这个也就是我们平时对UI进行重构所用到的方法之一了。很多项目接手过来时,里面的UI代码可能都是从一个地方实现后,拷贝到另一个地方使用。没有进行View控件封装的后果就是代码冗余,阅读困难,而且View的功能如果要变动的话,所有调用的地方都得变动。严重违背代码设计的开闭原则。
View绘制流程
- measure()
作用:测量View的宽高
相关函数:measure(),setMeasuredDimension(),onMeasure() - layout()
作用:计算当前View和子View的位置
相关函数:layout(),onLayout(),setFrame() - draw()
作用:绘制View图形
相关函数:draw(),onDraw()
用一种很形象的说法来说明View的绘制:
meaure决定用多大的盘子装菜,layout决定怎么摆盘好看,而draw就是服务员,把上面的东西给客人摆好。
坐标系
借用网络图片说明这个问题,对我们绘制UI理解有帮助:
- Android坐标系:
- View坐标系:
1# 继承系统View控件
我们用实例说话,继承TextView控件:
public class MyTextView extends TextView {
private Paint mPaint;
private String mText;
private int mTextColor;
private int mTextSize;
public MyTextView(Context context) {
this(context, null);
}
public MyTextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyView, 0, 0);
mText = a.getString(R.styleable.MyView_iText);
mTextColor = a.getColor(R.styleable.MyView_iTextColor, Color.BLUE);
mTextSize = (int) a.getDimension(R.styleable.MyView_iTextSize, 16);
a.recycle(); //回收
mPaint = new Paint();
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mTextColor);
mPaint.setTextSize(mTextSize);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//1. 获取 自定义 View 的宽度,高度 的模式
int heigthMode = MeasureSpec.getMode(heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
if (MeasureSpec.AT_MOST == heigthMode) {
Rect bounds = new Rect();
mPaint.getTextBounds(mText, 0, mText.length(), bounds);
height = bounds.height() + getPaddingBottom() + getPaddingTop();
}
if (MeasureSpec.AT_MOST == widthMode) {
Rect bounds = new Rect();
mPaint.getTextBounds(mText, 0, mText.length(), bounds);
width = bounds.width() + getPaddingLeft() + getPaddingRight();
}
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//计算基线
Paint.FontMetricsInt fontMetricsInt = mPaint.getFontMetricsInt();
int dy = (fontMetricsInt.bottom - fontMetricsInt.top) / 2 - fontMetricsInt.bottom;
int baseLine = getHeight() / 2 + dy;
int x = getPaddingLeft();
// x: 开始的位置 y:基线
canvas.drawText(mText, x, baseLine, mPaint);
}
}
我们重写了OnMeasure和OnDraw两个方法,前面也说到了,这两个方法是View绘制过程中用到的,可以按需重写,关于OnMeasure和OnLayout具体的内容这里就暂不详述了。
说到自定义View我们不得不说View的属性,上面代码里面TypedArray就是我们自己给View定义的属性,在attrs中设置:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyView">
<attr name="iText" format="string" />
<attr name="iTextBackgroundColor" format="color" />
<attr name="iTextColor" format="color" />
<attr name="iTextSize" format="dimension" />
</declare-styleable>
</resources>
2# 组合控件
public class MyLinearLayout extends LinearLayout implements View.OnClickListener {
private Button mBtnBack;
private ClickCallBack mClickCallBack;
public MyLinearLayout(Context context) {
this(context, null);
}
public MyLinearLayout(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public MyLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
View view = LayoutInflater.from(context).inflate(R.layout.layout_my, this);
mBtnBack = view.findViewById(R.id.btn_left);
mBtnBack.setOnClickListener(this);
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.btn_left) {
if (mClickCallBack != null) {
mClickCallBack.onBack();
}
}
}
public void setClickCallBack(ClickCallBack clickCallBack) {
mClickCallBack = clickCallBack;
}
public interface ClickCallBack {
void onBack();
}
}
我们可以在layout_my布局文件里面将我们的控件组合完成,然后inflate进来即可。
在MyLinearLayout里面,我们还可以跟普通类一样,定义一些外部调用的接口,比如这里的ClickCallBack。
3# 重写View
这里我们直接继承View,从最基层开始:
public class MyView extends View implements View.OnClickListener {
private Rect mBounds;
private Paint mPaint;
private String mText;
private int mTextBackgroundColor;
private int mTextColor;
private int mTextSize;
private int count;
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyView, 0, 0);
mText = a.getString(R.styleable.MyView_iText);
mTextBackgroundColor = a.getColor(R.styleable.MyView_iTextBackgroundColor, Color.BLACK);
mTextColor = a.getColor(R.styleable.MyView_iTextColor, Color.BLUE);
mTextSize = (int) a.getDimension(R.styleable.MyView_iTextSize, 16);
a.recycle(); //回收
mPaint = new Paint();
mPaint.setStyle(Paint.Style.FILL);
mBounds = new Rect();
setOnClickListener(this);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(mTextBackgroundColor);
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);
mPaint.setColor(mTextColor);
mPaint.setTextSize(mTextSize);
mPaint.getTextBounds(mText, 0, mText.length(), mBounds); //获取文字的宽和高
float textWidth = mBounds.width();
float textHeight = mBounds.height();
canvas.drawText(mText, getWidth() / 2 - textWidth / 2, getHeight() / 2 + textHeight / 2, mPaint);
}
@Override
public void onClick(View v) {
mText = String.format("点击了%d次", ++count);
invalidate();
}
}