自定义View最详细的资料整理与总结

前言

本篇博客的目的是为android新手总结整合自定义View的资料。第一篇的大佬没有上传完整的项目,我整理了下并加上了些注释,如果想下载的话,戳链接。如果呢积分还是5分就别下载,等到变更为0分时再下载。
本篇博客主要参考了Android自定义view案例一气泡框,Android 深入理解Android中的自定义属性,深入理解Android View的构造函数,手把手教你写一个完整的自定义View。在实现了第一篇博客后,将二三四篇所讲博客结合到第一篇博客之中。

基础篇

如果想对自定义View,有个大概了解和速成,建议去看WangRain1大大的博客。博主对于自定义View,讲的比较浅显易懂,新手易于上手,不过一些具体内部的属性大家可能有疑问。同时博主有一个无伤大雅的小问题,就是没说要在R.dimen文件里定义pwidth和pheight;在这里可以不设置这个判断,因为博主在layout文件里的width和height属性不是wrap_content。

 if(widthMode==MeasureSpec.AT_MOST&&heightMode==MeasureSpec.AT_MOST){
               widthSize = R.dimen.pwidth;
               heightSize = R.dimen.pheight;
           } 

理解篇

如果想要对自定义View中各种参数及自定义属性深入理解,这里推荐鸿洋大神的Android 深入理解Android中的自定义属性,建议将之前WangRain1大大的博客看完后再去看理解篇,这样理解起来会比较容易,有豁然开朗的感觉。这篇文章主要包括以下三点。

  • attrs.xml里面的declare-styleable以及item,android会根据其在R.java中生成一些常量方便我们使用(aapt干的),本质上,我们可以不声明declare-styleable仅仅声明所需的属性即可。
  • AttributeSet包括我们在布局文件中定义的所有属性的key和value。我们在View的构造方法中,可以通过AttributeSet去获得自定义属性的值,但是比较麻烦,具体麻烦之处在于当布局文件为引用形式时,使用AttributeSet就需要先获取id,在解析id的资源才可以获取,而TypedArray可以很方便的便于我们去获取。
  • 我们在自定义View的时候,可以使用系统已经定义的属性。这时,在attrs.xml文件中就不用写format而是只需要使用已经定义好的属性(只需要写name就好)。在java中正常调用,而在布局文件中不用app:name,用android:name即可。

源码篇

如果想要理解onMeasure(),onDraw()等方法都是如何运行的,强烈推荐Carson_Ho大神的手把手教你写一个完整的自定义View,以及之前的自定义View基础 - 最易懂的自定义View原理系列(1),自定义View Measure过程 - 最易懂的自定义View原理系列(2),自定义View Layout过程 - 最易懂的自定义View原理系列(3),自定义View Draw过程- 最易懂的自定义View原理系列(4)。看完之后你会惊叹这位大哥怎么这么猛,当然内容很多需要静下心来慢慢看。

总结

根据基础篇与理解篇的博客,总结自定义View有以下几个步骤,并尝试实现WangRain1大大的博客。其完成的工作是自定义了一个View,并使用paint画笔绘制了圆角矩形与三角形。圆角矩形的圆角半径与画笔的颜色是通过读取布局中的两个自定义属性设置的。

  • 写一个MyView类,继承自View。
  • 在values文件夹下创建attrs.xml文件,声明自定义的属性。
  • 在layout布局中使用自定义属性,注意开头要使用app,而非android。
  • 在构造函数中使用TypedArray获取在布局中定义的属性,并使用。
  • 支持wrap_content属性,padding属性。

在第一步中需要注意的是,构造函数有三个,第一个是在代码中调用,第二个是在layout布局中调用。不过通过以下这种构造函数一个调用另一个就不会出现nullpointer的问题。使用TypedArray获取属性要在第三个构造函数中使用。

public MyView(Context context)
    {
        //在java中调用
        this(context,null);
    }
public MyView(Context context, @Nullable AttributeSet attrs)
    {
        //在layout中调用
        this(context,attrs,0);
    }
public MyView(Context context,@Nullable AttributeSet attrs,int defstyleAttr ){
            super(context, attrs,defstyleAttr);
            }

在第二步中需要注意的就是,declare-styleable 不是必要的,但是styleable可以帮我们节省开发工作量,帮我们生成数组的索引常量。同时,我们可以使用android中已经定义好的属性,例如android.text,在这里直接使用name即可,而自定义属性类似于声明,二者区别就在于是否声明其类型(format)。仿照第一个博客,我们定义了两个自定义属性,分别是ridus和popu_bg。
写到这里可能有人会问到底什么才是属性,同时这些属性是从哪里来的呢?深入理解Android View的构造函数这篇文章写的很好。实际上是通过declare-style把这些属性明确的声明为系统需要处理的东西。每个declare-styleable产生一个R.styleable.[name],外加每个属性的R.styleable.[name]_[attribute] 。
比如,上面的代码产生R.styleable.MyView和R.styleable.MyView_popu_bg和R.styleable.MyView_ridus,以及R.styleable.MyView_android.text。
这些资源是什么东西呢?R.styleable.[name]是所有属性资源的数组,系统使用它来查找属性值。每个R.styleable.[name]_[attribute]只不过是这个数组的索引罢了,所以你可以一次性取出所有属性,然后分别查询每个的值。
如果你把它想象成一个cursor,R.styleable.[name]就可以看成是一个待查找的column列表,而R.styleable.[name]_[attribute]就是一个column的索引。这也和上文为什么使用delcare-style相对应,使用的话会自动生成索引(int的值),否则就要自己写索引值。
关于declare-styleable的更多信息,这里是关于自定义declare-styleable的官方文档。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyView">
        <attr name="ridus" format="dimension">

        </attr>
        <attr name="popu_bg" format="color">
        </attr>
        <attr name="android.text">
        </attr>
    </declare-styleable>
</resources>

第三步,主要就是要注意在使用时,自定义属性要使用app前缀而非android。同时要在前面定义 :xmlns:app=“http://schemas.android.com/apk/res-auto”。

?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.aaa.viewtest.MainActivity">
    <com.example.aaa.viewtest.MyView
        android:layout_width="300dp"
        android:layout_height="200dp"
        android:layout_gravity="center"
        app:ridus="20dp"
        app:popu_bg="@color/colorAccent"
        />
</LinearLayout>

第四步就是我们的重中之重,获取布局中的属性以及使用布局中的属性。如何获取布局中的属性呢,注意在构造函数中有AttributeSet,AttributeSet以数组形式存储属性。AttributeSet可以获取布局文件中定义的所有属性的key和value,例如width,height,以及其他自定义的属性。但是当布局文件使用引用时,使用getAttributeName()和getAttributeValue()获取的是@+字符串,如果想用这种方法的话,就要首先获取id,再解析id。而TypedArray帮我们实现了这个功能。
那么我们是如何获取到的TypedArray呢,使用的是obtainStyledAttributes(
AttributeSet set, @StyleableRes int[] attrs, @AttrRes int defStyleAttr,
@StyleRes int defStyleRes) 方法,第一个和第二个参数很好理解,第一个参数就是我们的AttributeSet,我们要从其中获取layout布局中的所有属性。第二个参数是一个数组,也就是我们要获取的属性的数组。这里我们只需要获取ridus和popu_bg,所以就找包含他们两个的数组,R.styleable.Myview数组。最后两个值是两个资源引用,可以不去考虑。如果想深入了解,戳这里。
理解了TypedArray的作用以及获取,接下来就要使用array.getDimension(index,defValue)方法来获取属性的value,dimension其实就是dp,也就是我们之前在attrs.xml中定义的radius对应的format。注释是我源码中的注释,也就是硕,如果这个index没有被定义或者不是一个资源,那么我就把这个值赋值defValue,def其实就是define的缩写嘛。recycle()方法的作用就是关闭了这个TypedArray。

   TypedArray array=context.obtainStyledAttributes(attrs,R.styleable.MyView,0,0);
        //getDimension(index,defValue)
        //* @param index Index of attribute to retrieve.
       // * @param defValue Value to return if the attribute is not defined or
        //*                 not a resource.
        //getColor(index,defValue)同理。
        ridus=array.getDimension(R.styleable.MyView_ridus,5);
        popu_bg= array.getColor(R.styleable.MyView_popu_bg,0xFF4081);
        /*
        Recycles the TypedArray, to be re-used by a later caller. After calling
        this function you must not ever touch the typed array again
         */
        array.recycle();

那么我们还在构造函数中做了什么呢?我们还定义了画笔paint的一些基本属性。我们不介绍OnDraw()方法,因为在其中,设计的是paint绘制圆角矩形与三角形的知识。在onDraw()方法中,设置画笔paint的颜色为popu_bg,以及绘制圆角矩形语句中圆角半径为radius。这个可以读者自行编写,或者参照WangRain1大大的博客Android自定义view案例一气泡框 。
最后写一下onMeasure()方法,在我的理解,这个方法,是起到适配的作用,该方法通过MeasureSpec.getMode(int measureSpec)来获取模式,模式一共有三种,UNSPECIFIED, EXACTLY, AT_MOST。我们能看到,注释中写到UNSPECIFIED为父视图不约束子视图。EXACTLY为父视图为子视图确定一个大小,例如(match_parent,100dp)。AT_MOST为父视图为子视图设置一个最大大小,子视图必须确保其可适应在该大小内,自适应(wrap_content)。更详细的可以看源码分析。

 /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;

第五步参考自手把手教你写一个完整的自定义View。首先介绍下wrap_content与match_parent之间的区别,wrap_content:视图的宽和高被设定成刚好适应视图内容的最小尺寸。match_parent:视图的宽和高延伸至充满整个父布局。前文说到onMeasure(),如果不修改这个方法的话,两者效果会相同。这里我们采用参考处的代码来解决问题,具体原因可以参考上述博客。

  • 在onMeasure()中的getDefaultSize()的默认实现中,当View的测量模式是AT_MOST或EXACTLY时,View的大小都会被设置成子View MeasureSpec的specSize。
  • 在计算子View MeasureSpec的getChildMeasureSpec()中,子View MeasureSpec在属性被设置为wrap_content或match_parent情况下,子View MeasureSpec的specSize被设置成parenSize = 父容器当前剩余空间大小。

解决方法,简单来说呢,就是要自己设定一个大小

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

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);


        // 获取宽-测量规则的模式和大小
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        // 获取高-测量规则的模式和大小
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        // 设置wrap_content的默认宽 / 高值
        // 默认宽/高的设定并无固定依据,根据需要灵活设置
        // 类似TextView,ImageView等针对wrap_content均在onMeasure()对设置默认宽 / 高值有特殊处理,具体读者可以自行查看
        int mWidth = 400;
        int mHeight = 400;

        // 当布局参数设置为wrap_content时,设置默认值
        if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT && getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
            setMeasuredDimension(mWidth, mHeight);
            // 宽 / 高任意一个布局参数为= wrap_content时,都设置默认值
        } else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
            setMeasuredDimension(mWidth, heightSize);
        } else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
            setMeasuredDimension(widthSize, mHeight);
        }
    }

通过下图可以看出,wrap_content与match_parent之间的区别。
自定义View最详细的资料整理与总结_第1张图片 自定义View最详细的资料整理与总结_第2张图片
接下来介绍如何实现padding属性。这里补充一下padding和margin的区别,由下图可以看出padding是子控件中的内容,与子控件之间的间距,而margin是子控件与父控件之间的间距。
自定义View最详细的资料整理与总结_第3张图片

 @Override
    protected void  onDraw(Canvas canvas){
        super.onDraw(canvas);
        paint.setColor(popu_bg);
        //画矩形
        int width=getWidth();
        int height=getHeight();
        int paddingleft=getPaddingLeft();
        int paddingright=getPaddingRight();
        int paddingtop=getPaddingTop();
        int paddingbottom=getPaddingBottom();
        RectF rectF=new RectF(paddingleft,paddingtop,width-paddingright,height-20-paddingbottom);
        canvas.drawRoundRect(rectF,ridus,ridus,paint);
        //画三角形
        Path path=new Path();
        //这是矩形的长度
        width=width-paddingleft-paddingright;
        height=height-paddingbottom;
        //实际的中心需要加上paddingleft
        path.moveTo((width/2)+paddingleft-50, height-20);
        path.lineTo(width / 2+paddingleft, height);
        path.lineTo((width/2)+paddingleft+50, height-20);
        path.close();
        canvas.drawPath(path, paint);

    }
		android:paddingLeft="20dp"
        android:paddingRight="60dp"
        android:paddingBottom="20dp"
        android:paddingTop="30dp"

其实实现很简单,就是把上下左右空出来就可以了。我们为了效果明显,将布局的属性设置的有区别。只不过究竟哪里才是中间的位置需要大家注意一下。

自定义View最详细的资料整理与总结_第4张图片

你可能感兴趣的:(android)