Android中如何从零开始自定义一个View总结

1. view的基本知识

1.1 什么是View

View是Android中所有控件的基类,无论是我们常用的TextView、Button,或者包含子控件的LinearLayout、RelativeLayout甚至是第三方引入的控件,它们的共同父类都是View。

ViewGroup是控件组,它的内部包含了许多个控件,也就是拥有一组view。在Android中ViewGroup也同样继承View,也就是说包含着一组控件的控件组也是控件的一种。
在这里插入图片描述

1.2 View的位置参数

View的位置根据四个属性决定:top,left,right,bottom。top对应View左上角的纵坐标,left对应横坐标,右下角同理。需要注意的是,View的坐标是相对于父容器而言的,是一种相对坐标。
在这里插入图片描述
由此可以得到View的宽度width = right - left,高度height = bottom - top。(注意,坐标系往上往左是负数,往下往右才是正数,所以width和height都是正数)

1.3 View的绘制流程

对于单个View而言,View的绘制流程主要是走onMeasure() -> onLayout() -> onDraw()三个方法。

  • onMeasure():测量视图及其内容以确定测量的宽度和测量高度。我们在自定义View的时候,一定要重写一遍这个方法,根据具体情况决定View的宽高。
  • onLayout():这是布局机制的第二阶段,为视图及其所有子视图指定大小和位置,当然这个位置是相对于父容器而言,是一种相对位置。我们只有在自定义ViewGroup时,会需要重写这个方法,自定义View不需要。
  • onDraw():进行具体的绘图流程,像是TextView的写Text,Button的画出按钮,都是在这个方法进行。

onMeasure()的方法在onLayout()的第一行,所以是先测量宽高,再决定具体的大小和位置

2. 自定义view的流程

下面会通过设计一个简单的TextView来简单描述一下自定义View的流程

2.1 AttributeSet和自定义属性

Android中,我们在XML文件中定义的属性,如宽高,Margin,Padding等。都会在View的构造方法中传递给attributeSet的对象,再通过TypedArray进行读取使用其中的变量

我们自定义的属性也会传给AttributeSet,首先在res/values文件夹下,新建一个attrs.xml文件夹,添加相关的元素,这里添加文本和字体大小。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyView">
        <attr name="myText" format="string"/>
        <attr name="myTextSize" format="integer"/>
    </declare-styleable>
</resources>

然后在View的构造方法中,通过TypedArray读取。

public class MyView extends View {
	private String text; // 文本
    private int textSize;
    
 	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) {
        super(context, attrs, defStyleAttr);

        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyView); // 获取attribute的属性
        Log.i(TAG, "MyView: " + typedArray);
        text = typedArray.getString(R.styleable.MyView_myText);
        textSize = typedArray.getInt(R.styleable.MyView_myTextSize, 100);
        typedArray.recycle(); // TypedArray使用完后需要释放内存
        init();
}

2.2 进行view具体的内容布置,onDraw()

Draw过程非常简单,就是将我们的view绘制到屏幕上,绘制内容需要笔类Paint和画布类Canvas,其中Canvas是onDraw()方法自带的参数,Paint则需要我们初始化。

public class MyView extends View {
	
	private Rect mTextBounds; 
    private String text; // 文本内容
    private int textSize; // 字体大小
    private Paint paint; // 画笔
	
	......省略无关代码......

	private void init() {
	    paint = new Paint();
	    paint.setTextSize(textSize); // 设置字体大小
	    
	    mTextBounds = new Rect(); 
	    paint.getTextBounds(text, 0, text.length(), mTextBounds); 
	    // getTextBounds 是将TextView 的文本放入一个矩形中, 测量TextView的高度和宽度,等下Measure时会用到
	}
}

初始化后直接画上去就可以了,调用Canvas.drawText方法搞定。

public class MyView extends View {
	
    private String text; // 文本内容
    private Paint paint; // 画笔
	
	......省略无关代码......

	 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawText(text, 0, mTextBounds.height(), paint); // 把字用笔写在画布上
    }
	
}

2.3 决定view的宽高,onMeasure()

onMeasure中的参数widthMeasureSpec和 heightMeasureSpec两个参数是父控件传递来的,实际上是将两个数specMode和specSize封装到一个int值里面,要用MeasureSpec类进行拆解,才能得到原来的数据。

widthMeasureSpec和heightMeasureSpec由于是int类型,所以是32位,其中高2位是specMode,低30位是specSize。

其中specMode这个值一共有三个模式
UNSPECIFIED:不对View大小做限制,如:ListView,ScrollView
EXACTLY:确切的大小,如:100dp或者march_parent
AT_MOST:大小不可超过某数值,如:wrap_content

一般来说,当且仅当xml文件中设置为wrap_content时,这个值才为AT_MOST,其他情况下都是EXACTLY,UNSPECIFIED不太常用。

对于我们的TextView而言,如果xml中设定了具体的宽高,那就直接采用即可;但是如果是wrap_content,那我们就需要根据文本的实际长度来决定TextView的宽高,这种时候刚刚获取的Rect变量就有用了。

具体如下:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    int specMode = MeasureSpec.getMode(widthMeasureSpec);
    int specWidth = MeasureSpec.getSize(widthMeasureSpec);
    if (specMode == MeasureSpec.AT_MOST) { // 当且仅当view的宽高设置为wrap_content时,对应的specMode才是AT_MOST,其余情况下是EXACTLY
        width  = mTextBounds.width(); // AT_MOST时,需要根据实际情况,自行决定控件的大小,这里就根据矩形的长宽决定
    }else if(specMode == MeasureSpec.EXACTLY){
        width = specWidth;
    }

    specMode = MeasureSpec.getMode(heightMeasureSpec);
    int specHeight = MeasureSpec.getSize(heightMeasureSpec);
    if (specMode == MeasureSpec.AT_MOST) {
        height  = mTextBounds.height();
    }else if(specMode == MeasureSpec.EXACTLY){
        height = specHeight;
    }

    setMeasuredDimension(width, height); // 决定自定义控件的大小
}

2.4 运行用例

运行之前放一下我们xml文件中的设置

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <com.wodongx123.customviewdemo.MyView
        android:layout_width="wrap_content"
        android:layout_height="100dp"
        app:myText="测试测试测试"
        app:myTextSize="50"
        android:background="@color/colorAccent"/>

</LinearLayout>

在这里插入图片描述
可以看到,我们的宽度设置成wrap_content,所以和文本宽度一致,高度设置成100dp,所以高出文本的内容很多。

3. 补充内容

3.1 计算宽高和view位置时应带上padding

由于我们是自定义View,如果想要让设置的Padding生效的话,需要我们手动在代码中重新计算宽高和文本的显示位置。
这样一来,onMeasure()方法和onDraw()方法要改为这样。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int specMode = MeasureSpec.getMode(widthMeasureSpec);
        int specSize = MeasureSpec.getSize(widthMeasureSpec);
        // 当且仅当view的宽高设置为wrap_content时,对应的specMode才是AT_MOST,其余情况下是EXACTLY
        if (specMode == MeasureSpec.AT_MOST) { 
            // AT_MOST时,需要根据实际情况,自行决定控件的大小,这里就根据矩形的长宽决定
            width  = mTextBounds.width() + getPaddingTop() + getPaddingBottom(); 
        }else if(specMode == MeasureSpec.EXACTLY){
            width = specSize + getPaddingLeft() + getPaddingRight();
        }

        specMode = MeasureSpec.getMode(heightMeasureSpec);
        specSize = MeasureSpec.getSize(heightMeasureSpec);
        if (specMode == MeasureSpec.AT_MOST) {
            height  = mTextBounds.height() + getPaddingTop() + getPaddingBottom();
        }else if(specMode == MeasureSpec.EXACTLY){
            height = specSize + getPaddingTop() + getPaddingBottom();

        }

        setMeasuredDimension(width, height); // 决定自定义控件的大小
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawText(text, getPaddingTop(), mTextBounds.height() + getPaddingLeft(), paint); // 把字用笔写在画布上,算上padding
    }

3.2 TypedArray使用后需要recycle

每次使用完typedArray后要调用recycle释放内存
recycle方法会将内部的一些对象指向为null
由于我们的Android界面有无数个View,所以会多次调用typedArray。
typedArray会被反复的创建和销毁,对于处理器的消耗比较大
为了防止这个问题,typedArray是使用了类似线程池一样的原理
在app生成的时候,就会创建很多typedArray放到堆区中,我们通过obtain方法来获取而不是通过new方法来创建
在用完之后,调用recycle,将typedArray释放回去,以便于另一个对象重用

同理的还有Handler中的Message(两者都是用obtain方法而不是new来创建实例)

3.3 让参数可以同时在java文件和xml文件中配置

我们平时在使用TextView的时候,不仅仅是会在xml文件中设置text,有时候也会在Java代码中调用setText这个方法。

我们的自定义View也需要能够支持同时在java文件和xml中配置的能力,根据实际的情况需求添加不同的方法,这里添加一个setText方法举例

public void setText(CharSequence c){
    text = (String) c;
    mTextBounds = new Rect();
    paint.getTextBounds(text, 0, text.length(), mTextBounds); // 获取文本
    requestLayout(); //重新调用onMeasure()和onLayout()
    invalidate(); // 重新调用onDraw()
}

参考材料

Android开发艺术探索
3.1.1,3.1.2(P122-123),4.1 - 4.4(P14-217)
码牛学院 android移动互联网高级开发 课程 2020.10.15 录播
加QQ:1979846055可以获取

Android自定义控件并且使其可以在xml中自定义属性_XiaoM的专栏-CSDN博客
https://blog.csdn.net/mthhk008/article/details/30070119
MeasureSpec详解 - 简书
https://www.jianshu.com/p/cecd0de7ec27
getTextBounds 方法作用_liweicai137的博客-CSDN博客
https://blog.csdn.net/liweicai137/article/details/51327096
自定义View的重新绘制和更新_qq_39700454的博客-CSDN博客
https://blog.csdn.net/qq_39700454/article/details/79272136

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值