天天看点

走向自定义View的深坑——自定义属性

为了让我们的自定义看起来和官方的差不多,正经一点,对没错是正经一点。我们的自定义控件要做的全面一点。

BB两句

  • 为什么要自定义控件?
    • 为了装逼
    • 为了满足脑洞
    • 为了世界的发展

自定义属性,单独自定义属性没啥用,因为自定义属性是提供给自定义View使用的,所以我们要先创建一个自定义View才能愉快的使用。

前排提示,文章略长,请耐心看完。

流程

  • 创建自定义View
  • 编写要用到的属性
  • 使用style给属性赋值
  • 代码中获取属性的值
  • 画出文字

创建自定义View

自定义View的第一步就是要把我们的类写成View,怎么写成View呢,只要我们继承View这个类就可以了,一般情况下我们都是继承View或者ViewGroup这两个类进行View扩展。为了方便通常也是直接集成相关方面的View进行修改,如这些TextView、EditText、LinearLayout等等。

首先创建一个类,继承View,此时应该是这样的

public class TextView extends View {
    }
           

啊嘞,这好像啥变化都没有啊-_-! 此时会报错,提示需要重写构造方法,一般情况下我们需要重写三个构造方法,以满足各个地方使用的需求,下面介绍使用场景

public TextView(Context context) {
        this(context,null);
    }

    public TextView(Context context, AttributeSet attrs) {
        this(context, attrs,);
    }

    public TextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
           
  • 第一个构造方法简单来说就是在代码中实例化的时候执行
  • 第二个构造方法是在XML布局文件中使用的时候执行,并且是没有使用Style来定义要使用的属性
  • 第三个构造方法是在XML布局文件中使用并且指定了style的时候执行。

注意看第一个方法和第二个方法内,我是用的this,就是调用当前类的第二个、第三个构造,依次类推,这是为了简化代码,直接在第三个方法中初始化一次就行了

好了,简单结构了解了,就开始走下一步。

编写要用到的属性

为什么要自定义属性?想想你使用的TextView

<TextView
        android:text="@string/app_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
           

这些都是属性,我们的自定义View想要有更好的体验,我们需要进行自定义一些属性方便在XML中直接配置。

自定义属性需要在res/value/attrs.xml中配置,没有的创建一个,使用declare-styleable标签进行定义,标签中的name属性写成自定义View的类名,下面看代码。

<declare-styleable name="TextView">
        <attr name="textSize" format="dimension"/>
        <attr name="textColor" format="color"/>
        <attr name="text" format="string"/>
        <attr name="gravity" format="enum">
            <enum name="center" value="0"/>
            <enum name="top" value="1"/>
            <enum name="bottom" value="2"/>
            <enum name="left" value="3"/>
            <enum name="right" value="4"/>
            <enum name="center_horizontal" value="5"/>
            <enum name="center_vertical" value="6"/>
        </attr>
    </declare-styleable>
           

好了,现在定义完了,可以在XML使用了,在使用前,需要先给自定义View设置一个命名控件,以便在代码中可以找到自定义的属性。

<!--命名控件的声明-->
    xmlns:app="http://schemas.android.com/apk/res-auto"
           
<com.github.odriver.viewdemo.view.TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:gravity="center"
        app:textSize="16sp"
        app:text="@string/app_name"
        app:textColor="@color/colorPrimary"
        />
           

好了,属性的使用就是这些了。

使用Style给属性赋值

有时候我们在使用控件的时候为了方便,为了懒,不想写重复代码。会使用style达到复用效果,这个时候,就是第三个构造方法执行,因为使用了style。

<com.github.odriver.viewdemo.view.TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        style="@style/textStyle"
        />
           

上面这段代码是使用style定义属性的值,下面我们来看看style的定义,style也是在declare-styleable定义的。

<style name="textStyle">
        <item name="textSize">sp</item>
        <item name="textColor">#FFFFFF</item>
        <item name="text">@string/app_name</item>
        <item name="gravity">bottom</item>
    </style>
           

在代码中获取属性的值

我们在代码中使用了属性,并且设置了属性的值。但是现在我们只是设置了,还没有进行处理,也就是相当于没个*用,所以我们还要继续,下面我们来获取属性的值,并且使用上,下面来看构造方法中的代码。

public TextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        // 使用context对象获得我们自定义的属性的值,attrs是我们xml中使用的属性集合,其中包括属性名和属性值等相关信息,后面是我们自定义的属性,也就是我们定义的declare-styleable。
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TextView);

        // 使用相应的方法获取各个类型属性的值,第二个参数是获取不到属性值默认的属性值。
        mTextColor = typedArray.getColor(R.styleable.TextView_textColor, Color.BLACK);
        System.out.println("mTextColor = " + mTextColor +"------defColor="+Color.BLACK);

        mTextSize = typedArray.getDimension(R.styleable.TextView_textSize, );
        System.out.println("mTextSize = " + mTextSize +"-----defDimension = " + );

        mGravity = typedArray.getInt(R.styleable.TextView_gravity, );
        System.out.println("mGravity = " + mGravity +"-----defAnInt = " + );

        mText = typedArray.getString(R.styleable.TextView_text);
        System.out.println("mText = " + mText);

        // 没啥,google推荐的,使用完及时释放
        typedArray.recycle();

        // 创建一个画笔,我们要在界面上画出相应的东西,都需要靠这支笔。
        mPaint = new Paint();
        // 设置抗锯齿
        mPaint.setAntiAlias(true);
    }
           

如果你设置给这些属性都赋值了,那么我们在获取值的时候是和默认值不一样的,可以打印出来看结果。

画出文字

自定义属性的工作基本上已经完成了,下面就是把自定义属性使用上了,我们现在View显示的还是空荡荡的一片,现在我们要让他显示出我们设定的文字,首先介绍几个方法,是我们自定义View是常用的方法。

- onDraw(Canvas canvas) 最重要的方法,用于将我们想要展示的东西绘制到屏幕上,不然看不到。。

- onMeasure(int widthMeasureSpec, int heightMeasureSpec) 测量方法有时候我们设置的是wrap_content或者match_parent又或者是具体值,我们需要在这个方法里进行判断,计算出我们自身能使用的大小。在自定义ViewGroup的时候该方法也是用来子View显示的大小和位置的。

- onLayout(boolean changed, int left, int top, int right, int bottom) 用于确定子View的位置,在自定义ViewGroup使用。

- onTouchEvent(MotionEvent event) 处理触摸事件

- on一大堆。。。

下面我们先让我们定义的文字显示在界面上,重写onDraw方法。

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 使用canvas类的drawText方法将我们的文字画到屏幕上。
        canvas.drawText(mText, , , mPaint);
    }
           

Canvas的draw方法,以后再讲。

现在来看屏幕,注意看白色部分那块黑色部分,没错,那就是我们画上去的文字。。。

走向自定义View的深坑——自定义属性

直到现在,我们还没有使用过任何自定义的属性,下面就使用上。

画出指定大小的文字

我们的文字太小了,我们把字体加大,在onDraw(Canvas canvas)方法中修改,还记的我们创建的那个画笔吗,全靠配置他来实现。

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 设置我们要显示的字体的大小
        mPaint.setTextSize(mTextSize);
        // 使用canvas类的drawText方法将我们的文字画到屏幕上。
        canvas.drawText(mText, , , mPaint);
    }
           

现在再看界面上

走向自定义View的深坑——自定义属性

主要就是靠这支画笔来控制。

画出指定颜色的文字

我们来给文字加上颜色,还是要通过这个画笔来实现。

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 设置我们要显示的字体的大小
        mPaint.setTextSize(mTextSize);
        // 设置文字的颜色
        mPaint.setColor(mTextColor);
        // 使用canvas类的drawText方法将我们的文字画到屏幕上。
        canvas.drawText(mText, , , mPaint);
    }
           

看,我们想要给文字加上颜色,只要用画笔的setColor就可以了

这个画笔就和我们现实中的画笔是一样的,我们的Canvas就相当于画布,现实中我们一般的绘画都是画布摆在哪里,不做任何动作,全靠画笔和人控制来进行绘画,我们需要画什么颜色,什么大小都是人和画笔的动作。而Canvas(画布)就跟他们说一句话:* 坐上来,自己动 *。画面有点污。。。

画出背景

看下面的代码我们是为了设置一个背景,而背景在我们自定义View中有些需求是局部背景而不是整个控件背景,所以我们要在画布上画出一个背景,原理就是画出一个矩形,使用颜色填充作为背景。

先看一个小例子来理解一下onDraw方法里面的绘制层级

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
// 第一层
        // 设置画笔颜色
        mPaint.setColor(Color.CYAN);
        // 画出背景,以上面设置的颜色填充
        canvas.drawRect(, , , , mPaint);

        // 设置我们要显示的字体的大小
        mPaint.setTextSize(mTextSize);
        // 设置文字的颜色
        mPaint.setColor(mTextColor);
        // 使用canvas类的drawText方法将我们的文字画到屏幕上。
        canvas.drawText("第一层", , , mPaint);

//-----------------------------------------------------------//
// 第二层
        // 设置画笔颜色
        mPaint.setColor(Color.BLUE);
        // 画出背景,以上面设置的颜色填充
        canvas.drawRect(, , , , mPaint);
        // 设置我们要显示的字体的大小
        mPaint.setTextSize(mTextSize);
        // 设置文字的颜色
        mPaint.setColor(Color.WHITE);
        // 使用canvas类的drawText方法将我们的文字画到屏幕上。
        canvas.drawText("第二层", , , mPaint);
    }
           
走向自定义View的深坑——自定义属性
从上面代码加图片中的样式我们不难看出,画出来的第二层把第一层给覆盖掉了,我们的代码也是由上至下执行的,最上面的代码画出来的东西会被我们在下面写的代码给压在下面。

注意:因为我们使用的画笔只有一个,在画出背景的时候我们给为了让背景显示特定的颜色填充整个背景,需要在画矩形的时候给画笔设置一个颜色,而这个画笔又只有一个并且现在已经设置上了一个颜色,如果下面再使用这个画笔在画布上画其他东西,使用的颜色是现在设置上的颜色,这样的话我们下面要画出的文字的颜色就和背景是一个颜色的了,为了给我们的文字设置文字指定的颜色我们需要在没有在画布上绘画的时候重新给画笔设置一个颜色。

onMeasure方法的使用

我们先把代码简化到只画文字,此时我们还有一个需求就是设置一个背景色,而设置背景颜色这个在我们继承的* View 这个类已经帮我们提供好了,那就是 android:background*这个属性,下面我们先把这个属性在布局文件中给我们自定义的View设置上,设置为#555555,也就是灰色,然后我们来看现在显示的效果。

走向自定义View的深坑——自定义属性

可以看到,上图显示出来的,我们整个屏幕都变成了灰色,为什么,我们设置的也是* wrap_content*为什么还是全屏呢。

因为我们的View所在的父控件并没有限制我们当前View可显示的大小,默认我们的View就充满整个屏幕了。这样我们想要设置一个具体的值也是没有用的,因为他现在是根据父View能给的大小来显示。

为了我们能够方便的管理这个View的大小,我们需要重写onMeasure方法在父View调用我们子View的onMeasure方法询问我们想要使用的大小的时候进行相应的计算,来合理地显示我们的View。

下面是我们在onMeasure中的逻辑处理。

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

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        // 指定了精确值,也就是设置了绝对大小,10dp这种
        if (widthMode == MeasureSpec.EXACTLY){
            mWidth = widthSize;
        }else{
            // 使用我们的画笔来测量文字的大小,然后加上左边padding和右边padding值,来算出我们想要的宽度
            widthSize= mBounds.width() + getPaddingLeft() + getPaddingRight();

            // 等于这个模式就相当wrap_content,我们要找最小值
            if (widthMode == MeasureSpec.AT_MOST){
                // 让默认的宽度和计算出的宽度做对比,哪个小使用哪个,就是尽可能的小
                widthSize = Math.min(mWidth, widthSize);
            }
        }

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        // 指定了精确值,也就是设置了绝对大小,10dp这种
        if (heightMode == MeasureSpec.EXACTLY){
            mHeight = heightSize;
        }else{
            // 使用我们的画笔来测量文字的大小,然后加上左边padding和右边padding值,来算出我们想要的宽度
            heightSize= mBounds.height() + getPaddingTop()+ getPaddingBottom();

            // 等于这个模式就相当wrap_content,我们要找最小值
            if (heightMode == MeasureSpec.AT_MOST){
                // 让默认的宽度和计算出的宽度做对比,哪个小使用哪个,就是尽可能的小
                heightSize = Math.min(mHeight, heightSize);
            }
        }
        setMeasuredDimension(widthSize,heightSize);
    }
           

其中mBounds实在构造方法中,初始化画笔的时候创建的,使用他主要是为了方便获取文字的宽高,下面贴出初始化代码。

// 创建一个画笔,我们要在界面上画出相应的东西,都需要靠这支笔。
        mPaint = new Paint();
        // 设置抗锯齿
        mPaint.setAntiAlias(true);
        mPaint.setTextSize(mTextSize);
        mBounds = new Rect();
        mPaint.getTextBounds(mText,,mText.length(), mBounds);
        mWidth = mBounds.width();
        mHeight = mBounds.height();
           

在初始化的时候注意,一定要在下面把默认的宽高也初始化,就是* mWidth = mBounds.width()和mHeight = mBounds.height(),从bound对象中拿到宽高,给我们默认的宽高赋值,否则在我们测量的方法中设置如果是 wrap_content*的时候,我们的view在界面上是看不到的,因为没有初始化宽高,默认是0,好了下面我们来看现在的效果图。

走向自定义View的深坑——自定义属性

可以看到,我们设置的背景成功的显示了,并且是最小范围的显示,下面我们把宽高都设置成* match_parent*

走向自定义View的深坑——自定义属性

可以看到,我们的文字跑到了最下面,这是为什么呢,下面我们观察onDraw方法里面的代码

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 使用canvas类的drawText方法将我们的文字画到屏幕上。
        canvas.drawText(mText, , getMeasuredHeight(), mPaint);
    }
           

注意看我们drawText的第三个参数,这个参数我们设置的是测量后的高度,参数本身是用来定义从什么高度的位置开始画的,我们定义的是测量后的高度,而且我们现在是* match_parent*充满屏幕的,所以现在就是从最底下开始画文字,而这个文字画的时候是文字的底部是0点,也就是我们现在文字的底部是在测量后的高度的最大值处,这时候我们想要让文字从顶部开始画,就用到我们刚才的Bound对象了,因为前面说了,我们的文字的底部是0点,那么我们文字顶部就是负的高度,也就是说我们从高度为0的这个位置开始画,我们的文字是在屏幕外面的。。。。看不见他,所以下面代码改成这个样子。

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 使用canvas类的drawText方法将我们的文字画到屏幕上。
        canvas.drawText(mText, , mBounds.height(), mPaint);
    }
           

来我们看看效果

走向自定义View的深坑——自定义属性

现在我们看到,文字刚好显示在屏幕的顶部。

Gravity属性的创造

看看我们开始的时候定义的declare-styleable,其中还有一个属性我们现在还没有适配,gravity,下面我们来适配它。

在前面我们的测量方法已经写好了,所以下面我们适配这个属性很轻松了,

- top 按照我们上面写好的代码,就相当于top,不多做解释(其实更相当于start,我想偷点懒。。。)

- bottom 还记得我们刚才那个文字跑到底下去了的那个吗,那个就相当于bottom。

- 等等。。。。下面我们看代码好了,有注释的。。。

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;

import com.github.odriver.viewdemo.R;

/**
 * Created by odriver on 16-8-25.
 */

class TextView extends View {

    private int mTextColor;
    private float mTextSize;
    private int mGravity;
    private String mText;
    private Paint mPaint;
    private int mWidth;
    private int mHeight;
    private Rect mBounds;

    public TextView(Context context) {
        this(context,null);
    }

    public TextView(Context context, AttributeSet attrs) {
        this(context, attrs,);
    }

    public TextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        // 使用context对象获得我们自定义的属性的值,attrs是我们xml中使用的属性集合,其中包括属性名和属性值等相关信息,后面是我们自定义的属性,也就是我们定义的declare-styleable。
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TextView);

        // 使用相应的方法获取各个类型属性的值,第二个参数是获取不到属性值默认的属性值。
        mTextColor = typedArray.getColor(R.styleable.TextView_textColor, Color.BLACK);
        mTextSize = typedArray.getDimension(R.styleable.TextView_textSize, );
        mGravity = typedArray.getInt(R.styleable.TextView_gravity, );
        mText = typedArray.getString(R.styleable.TextView_text);

        // 没啥,google推荐的,使用完及时释放
        typedArray.recycle();

        // 创建一个画笔,我们要在界面上画出相应的东西,都需要靠这支笔。
        mPaint = new Paint();
        // 设置抗锯齿
        mPaint.setAntiAlias(true);
        mPaint.setTextSize(mTextSize);
        mBounds = new Rect();
        mPaint.getTextBounds(mText,,mText.length(), mBounds);
        mWidth = mBounds.width();
        mHeight = mBounds.height();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // top 1 || left 3
        float startx = ;
        float starty = mBounds.height();

        // center
        if (mGravity == ){
            startx = getMeasuredWidth()/-mBounds.width()/;
            starty = getMeasuredHeight()/-mBounds.height()/;
        }else if (mGravity == ){ // bottom
            starty = getMeasuredHeight();
        }else if (mGravity == ){ // right
            startx = getMeasuredWidth()-mBounds.width();
        }else if (mGravity == ){ // center_horizontal
            startx = getMeasuredWidth()/-mBounds.width()/;
        }else if (mGravity == ){ // center_vertical
            starty = getMeasuredHeight()/-mBounds.height()/;
        }

        // 使用canvas类的drawText方法将我们的文字画到屏幕上。
        canvas.drawText(mText, startx, starty, mPaint);

    }

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

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        // 指定了精确值,也就是设置了绝对大小,10dp这种
        if (widthMode == MeasureSpec.EXACTLY){
            mWidth = widthSize;
        }else{
            // 使用我们的画笔来测量文字的大小,然后加上左边padding和右边padding值,来算出我们想要的宽度
            widthSize= mBounds.width() + getPaddingLeft() + getPaddingRight();

            // 等于这个模式就相当wrap_content,我们要找最小值
            if (widthMode == MeasureSpec.AT_MOST){
                // 让默认的宽度和计算出的宽度做对比,哪个小使用哪个,就是尽可能的小
                widthSize = Math.min(mWidth, widthSize);
            }
        }

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        // 指定了精确值,也就是设置了绝对大小,10dp这种
        if (heightMode == MeasureSpec.EXACTLY){
            mHeight = heightSize;
        }else{
            // 使用我们的画笔来测量文字的大小,然后加上左边padding和右边padding值,来算出我们想要的宽度
            heightSize= mBounds.height() + getPaddingTop()+ getPaddingBottom();

            // 等于这个模式就相当wrap_content,我们要找最小值
            if (heightMode == MeasureSpec.AT_MOST){
                // 让默认的宽度和计算出的宽度做对比,哪个小使用哪个,就是尽可能的小
                heightSize = Math.min(mHeight, heightSize);
            }
        }
        setMeasuredDimension(widthSize,heightSize);
    }
}
           

* 以上就是整个自定义View的相关代码,style里面配置的都在前面讲的有*,下面我们来欣赏一下各种摆放姿势。

走向自定义View的深坑——自定义属性
走向自定义View的深坑——自定义属性
走向自定义View的深坑——自定义属性
走向自定义View的深坑——自定义属性
走向自定义View的深坑——自定义属性
走向自定义View的深坑——自定义属性

* 好了简单的自定义控件结束了,如果你有想法,那么这么多已经可以简单自定义其他的控件了,比如进度条,只需要改改onDraw方法把文字的相关操作去掉就可以了*