四、自定义View--难点解析

作者: kim_liu | 来源:发表于2018-10-25 18:01 被阅读180次

    自定义View是一个Android工程师的必备技能,最近我打算写几个自定义View,却发现好多基础知识都忘得差不多了,所以,我打算再系统的学习一下,学东西有一个好老师非常重要,我看了很多博客,书,包括Hencoder,都觉得讲的不是很系统,直到看到了这位大神的博客,http://blog.csdn.net/xmxkf/article/details/51454685,从头开始,再学习一次。

    文章目录

    一、初识View

    View这个类代表用户界面组件的基本构建块。View在屏幕上占据一个矩形区域,并负责绘制和事件处理。
    View是用于创建交互式用户界面组件(按钮、文本等)的基础类。它的子类ViewGroup是所有布局的父类,
    它是一个可以包含其他view或者viewGroup并定义它们的布局属性的看不见的容器。
    实现一个自定义View,你通常会覆盖一些framework层在所有view上调用的标准方法。
    你不需要重写所有这些方法。事实上,你可以只是重写onDraw(android.graphics.Canvas)。
    ------官方文档对View的定义

    View中的方法: view中的方法

    上表已经把自定义View的各个方法调用时机写的很清楚了,构造方法好像写的不是那么清楚,那么我们来详细解释一下。

    自定义View的构造方法(constructors)

    因为View类不具有无参的构造方法,因此自定义View需要重写其构造方法, View的构造方法有4个,第4个方法是在API21之后出现的,如下所示的4种构造方法。

    //1.一个参数的构造方法
     public RangeBarView(Context context) {
            super(context);
        }
    
    //2.两个参数的构造方法
        public RangeBarView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
        }
    
    //3.三个参数的构造方法
        public RangeBarView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
    //4. 四个参数的构造方法
        public RangeBarView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
            super(context, attrs, defStyleAttr, defStyleRes);
        }
    

    如上所示:
    1.带有一个参数的构造方法,使用代码直接创建控件时调用该方法,源码中这样解释:

    Simple constructor to use when creating a view from code.

    2.带有两个参数的构造方法。使用XML创建控件时,调用此构造方法,第二个参数attrs,是在XML文件中指定的该控件的属性。源码中这样解释:

    Constructor that is called when inflating a view from XML. This is called
    when a view is being constructed from an XML file, supplying attributes that were specified in the XML file. This version uses a default style of 0, so the only attribute values applied are those in the Context's Theme and the given AttributeSet.

    我们可以在XML文件中创建一个控件,在该构造方法中打印出attrs的值,如下所示:Xml文件中只指定了该控件的宽高。

         <com.kimliu.customview.view.RangeBarView
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    

    我们在Java代码中获取attrs的值。

    public RangeBarView(Context context, @Nullable AttributeSet attrs) {
            super(context,null);
            int attributeCount = attrs.getAttributeCount();
            for(int i = 0; i < attributeCount; i++){
                Log.i("带有两个参数的构造方法", "RangeBarView: --"+attrs.getAttributeName(i) +"----"+ attrs.getAttributeValue(i));
            }
        }
    

    打印结果:

    打印结果 可以看到,在XML文件中指定的属性值全部打印了出来。
    系统只会调用前两个构造方法,后两个构造方法是需要我们自己调用的,一般我们使用自定义属性的话,会在第三个构造方法中使用TypedArray获取用户输入的自定义属性的值。

    自定义属性

    假如我们自定义一个MyTextView继承自View,如果我们要改变MyTextView的背景颜色为红色,是否需要重新再写一个红色背景的MyTextView呢,当然是不需要的,我们只需要给MyTextView添加一个自定义属性,用户在这个自定义属性里添加颜色,MyTextView根据用户添加的颜色来设置背景颜色。

    要使用自定义属性,我们首先要创建自定义属性,怎样创建自定义属性呢,我们可以去学习系统的做法,当我们使用系统的控件时,比如TextView,它的那些属性是如何获取到的呢?我们打开一下源码看一下coogle的工程师是怎么做的,首先打开的是attrs.xml文件,这个文件在\sdk\platforms\android-xx\data\res\values文件夹下。截取TextView相关的。

    <declare-styleable name="View"> 
         <attr name="id" format="reference" />
         <attr name="background" format="reference|color" />
         <attr name="padding" format="dimension" /> 
         ... 
         <attr name="focusable" format="boolean" />
         ... 
    </declare-styleable>
    
    <declare-styleable name="TextView"> 
          <attr name="text" format="string" localization="suggested" /> 
          <attr name="hint" format="string" /> 
          <attr name="textColor" /> 
          <attr name="textColorHighlight" /> 
          <attr name="textColorHint" />
     ... 
    </declare-styleable>
    

    因为所有控件都是View的子类,所以为View定义的属性所有的控件都能使用,但TextView是View的子类,子类有自己的特性,得单独为它扩展一些属性,而单独扩展的这些属性只能它自己使用,比如text等。

    系统中还有一种属性的定义方法,如上所示的textColor属性,没有format,这种属性是系统已经定义过的,直接拿过来用的属性,textColor在Theme中被定义过。拿过来声明一下,就可以给TextView使用了。

    format

    format支持的类型一共有11种
    1) reference 参考某一资源id

    属性定义:
    <declare-styleable name = "名称">
         <attr name = "background" format = "reference" />
    </declare-styleable>
    
    属性使用:
    <ImageView android:background = "@drawable/图片ID"/>
    

    2) color 颜色值 使用时填写类似于 #00FF00等
    3) boolean 布尔值 使用时填写true/false
    4) dimension 尺寸值 使用时填写 42dip等
    5)float 浮点值 使用时填写 1.0等
    6)integer 整型值
    7)string 字符串
    8)fraction 百分数
    9)enum 枚举值

    属性定义:
    <declare-styleable name="名称"> 
    <attr name="orientation">
       <enum name="horizontal" value="0" /> 
       <enum name="vertical" value="1" /> 
    </attr> 
    </declare-styleable>
    
    属性使用:
    <LinearLayout  
        android:orientation = "vertical">
    </LinearLayout>
    

    10) flag:位或运算

    属性定义:
    <declare-styleable name="名称">
     <attr name="gravity"> 
          <flag name="top" value="0x30" />
          <flag name="bottom" value="0x50" /> 
          <flag name="left" value="0x03" /> 
          <flag name="right" value="0x05" /> 
          <flag name="center_vertical" value="0x10" /> 
          ... 
    </attr> 
    </declare-styleable>
    
    属性使用:
    <TextView android:gravity="bottom|left"/>
    
    

    11) 混合类型:属性定义时可以指定多种类型值

    属性定义:
    <declare-styleable name = "名称">
         <attr name = "background" format = "reference|color" />
    </declare-styleable>
    
    属性使用:
    <ImageView  android:background = "@drawable/图片ID" />
    或者:
    <ImageView  android:background = "#00FF00" />
    

    命名空间

    在使用自定义属性的时候,我们需要在Xml文件的根节点上添加上这么一句话,xmlns:app="http://schemas.android.com/apk/res-auto" 这个叫做命名空间,只有加上了这么一句话,XML才知道下面使用的属性去哪里找。当我们使用系统定义的属性,比如android:id 时,会添加 xmlns:android="http://schemas.android.com/apk/res/android” 这表示到android系统中查找该属性的来源。
    我们在使用自定义属性时,会添加xmlns:app="http://schemas.android.com/apk/res-auto" 这样一句话,这句话表示自动查找,也可以写成xmlns:app="http://schemas.android.com/apk/com.kimliu.customview2" com.kimliu.customview2是应用程序的包名,通常我们使用第一种即可。

    写到这里,可以写一个demo了 ,定义一个MyTextView extends View。
    1.使用自定义属性前必须定义它,学着系统的做法,首先创建一个attrs.xml文件,在其中创建自定义属性,如下:

    <declare-styleable name="MyTextView">
            <attr name="android:text"/>
            <attr name="mTextColor"  format="color"/>
            <attr name="mTextSize" format="dimension"/>
        </declare-styleable>
    
    1. 在XML文件中使用,注意 要添加命名空间
    
    <android.support.constraint.ConstraintLayout 
        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"
        tools:context="com.kimliu.customview3.MainActivity">
    
        <com.kimliu.customview3.MyTextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:mTextSize="14sp"
            app:mTextColor="#000"
            android:text="哈哈"
            />
    
    </android.support.constraint.ConstraintLayout>
    

    3.在构造方法中获取自定义属性的值。

    public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
    
            TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyTextView);
            String string = ta.getString(R.styleable.MyTextView_android_text);
            int color = ta.getColor(R.styleable.MyTextView_mTextColor, Color.BLACK);
            float dimension = ta.getDimension(R.styleable.MyTextView_mTextSize, 100);
    
            Log.i("获取属性", "MyTextView: "+string +"----"+color+"------"+dimension);
    
        }
    
    log输出: 图片.png

    到这,自定义属性我们已经会做了。现在来了解一下AttributeSet和TypedArray。

    AttributeSet

    AttributeSet是一个属性集合,实际上,它内部就是一个XML解析器,帮我们将布局中所有的属性解析出来,并以key-value的键值对的形式维护起来, 我们可以通过下面的代码获取写在XML文件的属性值。

     public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            for(int i = 0; i< attrs.getAttributeCount();i++){
                Log.i("属性值"+i, "MyTextView: "
                        +attrs.getAttributeName(i)+"--->"+attrs.getAttributeValue(i));
            }
        }
    
    使用AttributeSet解析出来的属性数据: 图片.png

    可以看到,用这种方式解析出来的数据,是原封不动的解析出来,这样不能直接拿来使用,换成TypeArray试试:


    image.png

    可以看到,我们得到的color是十进制的颜色值,获得到了可以直接使用的值。比AttributeSet方便。

    declare-styleable

    我们都知道所有的资源文件在R中都会对应一个整型常亮,我们可以通过这个ID值找到资源文件。
      属性在R中对应的类是public static final class attr,如果我们写了declare-styleable,在R文件中就会生成styleable类,这个类其实就是将每个控件的属性分组,然后记录属性的索引值,而TypedArray正好需要通过此索引值获取属性。

    public static final class styleable {
    
             public static final int[] MyTextView =
                  { 0x0101014f, 0x7f010038, 0x7f010039 }; 
             public static final int MyTextView_android_text = 0; 
             public static final int MyTextView_mTextColor = 1; 
             public static final int MyTextView_mTextSize = 2; 
    
    }
    

    二、深入理解onMeasure()

    1.onMeasure()什么时候被调用?

    创建一个View的时候不需要测量控件的大小,当将这个View放入容器(父控件)中的时候才测量,这个测量方法是父控件调起的,当控件的父控件要放置该控件的时候,父控件会调用子控件的onMeasure方法询问子控件:“你有多大的尺寸,我要给你多大的地方才能容纳你?”,然后传入两个参数(widthMeasureSpec和heightMeasureSpec),这两个参数就是父控件告诉子控件可获得的空间以及关于这个空间的约束条件(好比我在思考需要多大的碗盛菜的时候我要看一下碗柜里最大的碗有多大,菜的分量不能超过这个容积,这就是碗对菜的约束),子控件拿着这些条件就能正确的测量自身的宽高了。

    在给子控件确定位置之前,必须要获取到子控件的大小(只有确定了子控件的大小才能正确的确定上下左右四个点的坐标),而ViewGroup并没有重写View的onMeasure方法,也就是说抽象类ViewGroup没有为子控件测量大小的能力,它只能测量自己的大小。但是既然ViewGroup是一个能容纳子控件的容器,系统当然也考虑到测量子控件的问题,所以ViewGroup提供了三个测量子控件相关的方法(measureChildren\measureChild\measureChildWithMargins),只是在ViewGroup中没有调用它们,所以它本身不具备为子控件测量大小的能力,但是他有这个潜力哦。

    为什么都有测量子控件的方法了而ViewGroup中不直接重写onMeasure方法,然后在onMeasure中调用呢?因为不同的容器摆放子控件的方式不同,比如RelativeLayout,LinearLayout这两个ViewGroup的子类,它们摆放子控件的方式不同,有的是线性摆放,而有的是叠加摆放,这就导致测量子控件的方式会有所差别,所以ViewGroup就干脆不直接测量子控件,他的子类要测量子控件就根据自己的布局特性重写onMeasure方法去测量。这么看来ViewGroup提供的三个测量子控件的方法岂不是没有作用?答案是NO,既然提供了就肯定有作用,这三个方法只是按照一种通用的方式去测量子控件,很多ViewGruop的子类测量子控件的时候就使用了ViewGroup的measureChildxxx系列方法;还有一个作用就是为我们自定义ViewGroup提供方便咯。

    测量过程

    测量的时候父控件的onMeasure方法会遍历他所有的子控件,挨个调用子控件的measure方法,measure方法会调用onMeasure,然后会调用setMeasureDimension方法保存测量的大小,一次遍历下来,第一个子控件以及这个子控件中的所有子控件都会完成测量工作;然后开始测量第二个子控件…;最后父控件所有的子控件都完成测量以后会调用setMeasureDimension方法保存自己的测量大小。值得注意的是,这个过程不只执行一次,也就是说有可能重复执行,因为有的时候,一轮测量下来,父控件发现某一个子控件的尺寸不符合要求,就会重新测量一遍。 测量过程
    测量时序图

    MeasureSpec类

    上面说到MeasureSpec约束是由父控件传递给子控件的,这个类里面到底封装了什么东西?我们看一看源码:

    public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK = 0x3 << MODE_SHIFT;
         /**
         * 父控件不强加任何约束给子控件,它可以是它想要任何大小
         */ 
         public static final int UNSPECIFIED = 0 << MODE_SHIFT; 
         /**
         * 父控件已为子控件确定了一个确切的大小,孩子将被给予这些界限,不管子控件自己希望的是多大
         */ 
          public static final int EXACTLY = 1 << MODE_SHIFT; 
         /**
         * 父控件会给子控件尽可能大的尺寸
         */ 
           public static final int AT_MOST = 2 << MODE_SHIFT; 
          /**
         * 根据所提供的大小和模式创建一个测量规范
         */ 
         public static int makeMeasureSpec(int size, int mode) {
               if (sUseBrokenMakeMeasureSpec) {
                        return size + mode; 
                   } else {
                        return (size & ~MODE_MASK) | (mode & MODE_MASK); 
                  } 
             }
           /**
         * 从所提供的测量规范中提取模式
         */ 
          public static int getMode(int measureSpec) { 
                     return (measureSpec & MODE_MASK);
                  }
          /**
         * 从所提供的测量规范中提取尺寸
         */ 
           public static int getSize(int measureSpec) {
                 return (measureSpec & ~MODE_MASK);
              } 
            ... 
      }
    

    从源码中我们知道,MeasureSpec其实就是尺寸和模式通过各种位运算计算出的一个整型值,它提供了三种模式,还有三个方法(合成约束、分离模式、分离尺寸)。

    约束 布局参数 说明
    UNSPECIFIED 0 父控件没有对子控件施加任何约束,子控件可以得到任意想要的大小(使用较少)
    EXACTLY(完全) match_parent/具体宽高值 1073741824 父控件给子控件决定了确切大小,子控件将被限定在给定的边界里而忽略它本身大小。特别说明如果是填充父窗体,说明父控件已经明确知道子控件想要多大的尺寸了(就是剩余空间都要了)。
    AT_MOST(至多) wrap_content -2147483648 子控件至多达到指定大小的值,包裹内容就是父窗体并不知道子控件到底需要多大尺寸(具体值),需要子控件自己测量之后再让父控件给他一个尽可能大的尺寸以便让内容全部显示但不能超过包裹内容的大小。

    上面说了,在ViewGroup中没有重写View的onMeasure(),但它其中有三个测量子控件大小的方法,这三个方法的源码如下:

    /**    
          遍历ViewGroup中所有的子控件,调用measuireChild测量宽高
         * Ask all of the children of this view to measure themselves, taking into
         * account both the MeasureSpec requirements for this view and its padding.
         * We skip children that are in the GONE state The heavy lifting is done in
         * getChildMeasureSpec.
         *  
         * @param widthMeasureSpec The width requirements for this view
         * @param heightMeasureSpec The height requirements for this view
         */
        protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
            final int size = mChildrenCount;
            final View[] children = mChildren;
            for (int i = 0; i < size; ++i) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                     //测量某一个子控件宽高
                    measureChild(child, widthMeasureSpec, heightMeasureSpec);
                }
            }
        }
    
     /**
           测量某一个child的宽高
         * Ask one of the children of this view to measure itself, taking into
         * account both the MeasureSpec requirements for this view and its padding.
         * The heavy lifting is done in getChildMeasureSpec.
         *
         * @param child The child to measure
         * @param parentWidthMeasureSpec The width requirements for this view
         * @param parentHeightMeasureSpec The height requirements for this view
         */
        protected void measureChild(View child, int parentWidthMeasureSpec,
                int parentHeightMeasureSpec) {
            final LayoutParams lp = child.getLayoutParams();
           //获取子控件的宽高约束规则
            final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                    mPaddingLeft + mPaddingRight, lp.width);
            final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                    mPaddingTop + mPaddingBottom, lp.height);
    
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    
    
     /**
         测量某一个child的宽高,考虑margin值
         * Ask one of the children of this view to measure itself, taking into
         * account both the MeasureSpec requirements for this view and its padding
         * and margins. The child must have MarginLayoutParams The heavy lifting is
         * done in getChildMeasureSpec.
         *
         * @param child The child to measure
         * @param parentWidthMeasureSpec The width requirements for this view
         * @param widthUsed Extra space that has been used up by the parent
         *        horizontally (possibly by other children of the parent)
         * @param parentHeightMeasureSpec The height requirements for this view
         * @param heightUsed Extra space that has been used up by the parent
         *        vertically (possibly by other children of the parent)
         */
        protected void measureChildWithMargins(View child,
                int parentWidthMeasureSpec, int widthUsed,
                int parentHeightMeasureSpec, int heightUsed) {
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
           //获取子控件的宽高约束规则
            final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                    mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                            + widthUsed, lp.width);
            final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                    mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                            + heightUsed, lp.height);
            //测量子控件
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    
    

    这三个方法分别做了那些工作大家应该比较清楚了吧?measureChildren 就是遍历所有子控件挨个测量,最终测量子控件的方法就是measureChild 和measureChildWithMargins 了,我们先了解几个知识点:

    measureChildWithMargins跟measureChild的区别就是父控件支不支持margin属性

    支不支持margin属性对子控件的测量是有影响的,比如我们的屏幕是1080x1920的,子控件的宽度为填充父窗体,如果使用了marginLeft并设置值为100;
    在测量子控件的时候,如果用measureChild,计算的宽度是1080,而如果是使用measureChildWithMargins,计算的宽度是1080-100 = 980。

    那么怎样让ViewGroup支持Margin属性呢?

    ViewGroup中有两个内部类ViewGroup.LayoutParams和ViewGroup. MarginLayoutParams,MarginLayoutParams继承自LayoutParams ,这两个内部类就是ViewGroup的布局参数类,比如我们在LinearLayout等布局中使用的layout_width\layout_hight等以“layout_ ”开头的属性都是布局属性。在View中有一个mLayoutParams的变量用来保存这个View的所有布局属性。
    LayoutParams 中定义了两个属性:

    <declare-styleable name= "ViewGroup_Layout">
     <attr name ="layout_width" format="dimension">
             <enum name ="fill_parent" value="-1" /> 
             <enum name ="match_parent" value="-1" /> 
             <enum name ="wrap_content" value="-2" />
     </attr >
     <attr name ="layout_height" format="dimension"> 
             <enum name ="fill_parent" value="-1" /> 
            <enum name ="match_parent" value="-1" /> 
            <enum name ="wrap_content" value="-2" />
     </attr > 
    </declare-styleable >
    

    MarginLayoutParams 是LayoutParams的子类,它当然也延续了layout_width\layout_hight 属性,但是它扩充了其他属性:

    < declare-styleable name ="ViewGroup_MarginLayout">
     <attr name ="layout_width" /> <!--使用已经定义过的属性-->
     <attr name ="layout_height" /> 
     <attr name ="layout_margin" format="dimension"  />
     <attr name ="layout_marginLeft" format= "dimension"  />
     <attr name ="layout_marginTop" format= "dimension" />
     <attr name ="layout_marginRight" format= "dimension"  />
     <attr name ="layout_marginBottom" format= "dimension"  />
     <attr name ="layout_marginStart" format= "dimension"  />
     <attr name ="layout_marginEnd" format= "dimension"  />
    </declare-styleable >
    
    LayoutParams的更多知识,会在ViewGroup知识点中详细说明。我们看到measureChild()和measureChildWithMargins() 中都调用了getChildMeasureSpec(),那么这个getChildMeasureSpec()是做什么用的呢?它的作用就是:子控件的宽高约束规则是父控件调用getChildMeasureSpec方法生成。 即通过父控件的宽高约束规则和父控件加在子控件上的宽高布局参数生成一个子控件的约束。我们知道View的onMeasure方法需要两个参数(父控件对View的宽高约束),这个宽高约束就是通过这个方法生成的。有人会问为什么不直接拿着子控件的宽高参数去测量子控件呢?打个比方,父控件的宽高约束为wrap_content,而子控件为match_perent,是不是很有意思,父控件说我的宽高就是包裹我的子控件,我的子控件多大我就多大,而子控件说我的宽高填充父窗体,父控件多大我就多大。最后该怎么确定大小呢?所以我们需要为子控件重新生成一个新的约束规则。 图片.png
    getChildMeasure方法代码不多,也比较简单,就是几个switch将各种情况考虑后生成一个子控件的新的宽高约束,这个方法的结果能够用一个表来概括: 图片.png
    进行了上面的步骤,接下来就是在measureChildWithMarginsh或者measureChild中 调用子控件的measure方法测量子控件的尺寸了。

    View中的onMeasure()

    View中onMeasure方法已经默认为我们的控件测量了宽高,我们看看它做了什么工作:

    protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec) { 
        setMeasuredDimension( getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); 
    } 
    
    /**
     * 为宽度获取一个建议最小值
     */ 
    protected int getSuggestedMinimumWidth () { 
       return (mBackground == null) ? mMinWidth : max(mMinWidth , mBackground.getMinimumWidth()); 
    } /**
     * 获取默认的宽高值
     */ 
    public static int getDefaultSize (int size, int measureSpec) {
            int result = size; 
            int specMode = MeasureSpec. getMode(measureSpec);
            int specSize = MeasureSpec. getSize(measureSpec);
            switch (specMode) { 
               case MeasureSpec. UNSPECIFIED: 
                    result = size;
                       break;
               case MeasureSpec. AT_MOST: 
               case MeasureSpec. EXACTLY: 
                    result = specSize; 
                       break;
               }
               return result;
    }
    

    从源码我们了解到:

    如果View的宽高模式为未指定,他的宽高将设置为android:minWidth/Height =”“值与背景宽高值中较大的一个;
    如果View的宽高 模式为 EXACTLY (具体的size ),最终宽高就是这个size值;
    如果View的宽高模式为EXACTLY (填充父控件 ),最终宽高将为填充父控件;
    如果View的宽高模式为AT_MOST (包裹内容),最终宽高也是填充父控件。

    也就是说如果我们的自定义控件在布局文件中,只需要设置指定的具体宽高,或者MATCH_PARENT 的情况,我们可以不用重写onMeasure方法。

    但如果自定义控件需要设置包裹内容WRAP_CONTENT ,我们需要重写onMeasure方法,为控件设置需要的尺寸;默认情况下WRAP_CONTENT 的处理也将填充整个父控件。

    setMeasuredDimension()

    onMeasure方法最后需要调用setMeasuredDimension方法来保存测量的宽高值,如果不调用这个方法,可能会产生不可预测的问题。

    关于onMeasure():
    1.测量控件大小是父控件发起的
    2.父控件要测量子控件大小,需要重写onMeasure方法,然后调用measureChildren或者measureChildWithMargin方法
    3.onMeasure方法的参数是通过getChildMeasureSpec生成的
    4.如果我们自定义控件需要使用wrap_content,我们需要重写onMeasure方法
    5.测量控件的步骤:
    父控件onMeasure->measureChildren/measureChildWithMargin->getChildMeasureSpec->
    子控件的measure->onMeasure->setMeasureDimension->
    父控件onMeasure结束调用setMeasureDimension保存自己的大小

    关于自定义View,到这里就差不多告一段落了,后续还会有自定义ViewGroup的内容。
    如果对我的内容感兴趣,可以关注我的公众号:平头哥写代码。内容不多,但都是精选。


    平头哥写代码

    相关文章

      网友评论

        本文标题:四、自定义View--难点解析

        本文链接:https://www.haomeiwen.com/subject/mrqszftx.html