Android实现自定义View的方法

我的新书《Android App开发入门与实战》已于2020年8月由人民邮电出版社出版,欢迎购买。点击进入详情

概述

android自定义View是一种很常见的使用方式。系统自带的widget往往不能满足我们的需求,这个时候就需要我们自己动手实现View控件了。
自定义View有很多种实现方式:

  1. 继承系统View控件,比如TextView等;
  2. 多个控件组合成一个新的控件,比如继承LineaLayout;
  3. 继承View控件,根据View的绘制流程重新进行实现;

另外,我们实现自定义View的好处有哪些呢?
自定义View实际上也就是一个功能类,除了绘制UI以外,还可以封装一定的业务逻辑,这样我们只需要对外提供一些接口,调用方就像使用其它类一样调用即可。

举个例子,我们常见的adapter的item布局中:
在这里插入图片描述
左边的图片和右边的文字可以做成一个新的View控件,这样如果调用的地方多了,可以节省不少的代码量,因为实现的逻辑都在新的控件中处理了。

这个也就是我们平时对UI进行重构所用到的方法之一了。很多项目接手过来时,里面的UI代码可能都是从一个地方实现后,拷贝到另一个地方使用。没有进行View控件封装的后果就是代码冗余,阅读困难,而且View的功能如果要变动的话,所有调用的地方都得变动。严重违背代码设计的开闭原则。

View绘制流程

  1. measure()
    作用:测量View的宽高
    相关函数:measure(),setMeasuredDimension(),onMeasure()
  2. layout()
    作用:计算当前View和子View的位置
    相关函数:layout(),onLayout(),setFrame()
  3. draw()
    作用:绘制View图形
    相关函数:draw(),onDraw()

用一种很形象的说法来说明View的绘制:
meaure决定用多大的盘子装菜,layout决定怎么摆盘好看,而draw就是服务员,把上面的东西给客人摆好。

坐标系

借用网络图片说明这个问题,对我们绘制UI理解有帮助:

  1. Android坐标系:
    在这里插入图片描述
  2. 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();
    }
}

git地址

https://github.com/ddnosh/android-demo-customized-view

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值