Android自定义View详解

栏目: IOS · Android · 发布时间: 7年前

内容简介:Android自定义View详解

自定义 View 对于一个 Android 开发者来说是必须掌握的知识点,也是 Android 开发进阶的必经之路。

为什么要自定义 View ?主要是 Android 系统内置的 View 无法实现我们的需求,我们需要针对我们的业务需求定制我们想要的 View

自定义 View 的最基本的三个方法分别是: onMeasure()onLayout()onDraw() ;

View 在Activity中显示出来,要经历测量、布局和绘制三个步骤,分别对应三个动作:measure、layout和draw。

测量: onMeasure() 决定View的大小;

布局: onLayout() 决定View在ViewGroup中的位置;

绘制: onDraw() 决定绘制这个View。

自定义控件又分为 自定义View自定义ViewGroup ,自定义 View 只需要重写 onMeasure()onDraw() 即可,而自定义 ViewGroup 则只需要重写 onMeasure()onLayout()

自定义View的基础

自定义View原理是Android开发者必须了解的基础,基础掌握了我们才能进行下一步的学习

基础的学习思路:

Android自定义View详解

1. View的分类

视图View主要分为两类:

类别 解释 特点
单一视图 即一个View,如TextView 不包含子View
视图组 即多个View组成的ViewGroup,如LinearLayout 包含子View

2. View类简介

  • View类是Android中各种组件的基类,如View是ViewGroup基类
  • View表现为显示在屏幕上的各种视图

    Android中的UI组件都由View、ViewGroup组成。
  • View的构造函数:共有4个,具体如下:

    自定义View必须重写至少一个构造函数:
// 如果View是在 Java 代码里面new的,则调用第一个构造函数
 public CarsonView(Context context) {
        super(context);
    }

// 如果View是在.xml里声明的,则调用第二个构造函数
// 自定义属性是从AttributeSet参数传进来的
    public  CarsonView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

// 不会自动调用
// 一般是在第二个构造函数里主动调用
// 如View有style属性时
    public  CarsonView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    //API21之后才使用
    // 不会自动调用
    // 一般是在第二个构造函数里主动调用
    // 如View有style属性时
    public  CarsonView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

更加具体的使用请看: 深入理解View的构造函数理解View的构造函数

3. View视图结构

对于 多View 的视图,结构是 树形结构 :最顶层是ViewGroup,ViewGroup下可能有多个ViewGroup或View,如下图:

Android自定义View详解

一定要记住: 无论是measure过程、layout过程还是draw过程,永远都是从View树的根节点开始测量或计算(即从树的顶端开始),一层一层、一个分支一个分支地进行(即树形递归) ,最终计算整个View树中各个View,最终确定整个View树的相关属性。

4. Android坐标系

Android的坐标系定义为:

  • 屏幕的左上角为坐标原点

  • 向右为x轴增大方向

  • 向下为y轴增大方向

具体如下图:

Android自定义View详解

注:区别于一般的数学坐标系,如下图

Android自定义View详解

5. View位置(坐标)描述

View的位置由4个顶点决定的(如下A、B、C、D):

Android自定义View详解

4个顶点的位置描述分别由4个值决定:

(请记住:View的位置是相对于父控件而言的)

  • Top:子View上边界到父view上边界的距离

  • Left:子View左边界到父view左边界的距离

  • Bottom:子View下边距到父View上边界的距离

  • Right:子View右边界到父view左边界的距离

如下图:

Android自定义View详解

个人建议:按顶点位置来记忆:

  • Top:子View左上角距父View顶部的距离;

  • Left:子View左上角距父View左侧的距离;

  • Bottom:子View右下角距父View顶部的距离

  • Right:子View右下角距父View左侧的距离

6. 位置获取方式

  • View的位置是通过view.getxxx()函数进行获取:(以Top为例)
// 获取Top位置
public final int getTop() {  
    return mTop;  
}  

// 其余如下:
  getLeft();      //获取子View左上角距父View左侧的距离
  getBottom();    //获取子View右下角距父View顶部的距离
  getRight();     //获取子View右下角距父View左侧的距离
//get() :触摸点相对于其所在组件坐标系的坐标
 event.getX();       
 event.getY();

//getRaw() :触摸点相对于屏幕默认坐标系的坐标
 event.getRawX();    
 event.getRawY();

具体如下图:

Android自定义View详解

7. Android的角度(angle)与弧度(radian)

  • 自定义View实际上是将一些简单的形状通过计算,从而组合到一起形成的效果。

    这会涉及到画布的相关操作(旋转)、正余弦函数计算等,即会涉及到角度(angle)与弧度(radian)的相关知识。
  • 角度和弧度都是描述角的一种度量单位,区别如下图:

Android自定义View详解

在默认的屏幕坐标系中角度增大方向为顺时针。

Android自定义View详解

注:在常见的数学坐标系中角度增大方向为逆时针

8. Android中颜色相关内容

Android中的颜色相关内容包括颜色模式,创建颜色的方式,以及颜色的混合模式等。

Android支持的颜色模式:

Android自定义View详解

以ARGB8888为例介绍颜色定义:

Android自定义View详解

在java中定义颜色:

//java中使用Color类定义颜色
int color = Color.GRAY;     //灰色

 //Color类是使用ARGB值进行表示
 int color = Color.argb(127, 255, 0, 0);   //半透明红色
 int color = 0xaaff0000;                   //带有透明度的红色

在xml文件中定义颜色:

<?xml version="1.0" encoding="utf-8"?>
<resources>

    //定义了红色(没有alpha(透明)通道)
    <color name="red">#ff0000</color>
    //定义了蓝色(没有alpha(透明)通道)
    <color name="green">#00ff00</color>
</resources>

在xml文件中以”#“开头定义颜色,后面跟十六进制的值,有如下几种定义方式:

#f00            //低精度 - 不带透明通道红色
#af00           //低精度 - 带透明通道红色

#ff0000         //高精度 - 不带透明通道红色
#aaff0000       //高精度 - 带透明通道红色

在java文件中引用xml中定义的颜色:

//方法1
int color = getResources().getColor(R.color.mycolor);

//方法2(API 23及以上)
int color = getColor(R.color.myColor);

在xml文件(layout或style)中引用或者创建颜色:

<!--在style文件中引用-->
   <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
       <item name="colorPrimary">@color/red</item>
   </style>
	
<!--在layout文件中引用在/res/values/color.xml中定义的颜色-->
 android:background="@color/red"     
	
<!--在layout文件中创建并使用颜色-->
 android:background="#ff0000"

取色工具:

  • 颜色都是用RGB值定义的,而我们一般是无法直观的知道自己需要颜色的值,需要借用取色 工具 直接从图片或者其他地方获取颜色的RGB值。

  • 有时候一些简单的颜色选取就不用去麻烦UI了,开发者自己去选取效率更高

  • 这里,取色工具 Markman :一款设计师用于标注的工具,主要用于尺寸标注、字体大小标注、颜色标注,而且使用简单。

自定义View Measure过程

OnMeasure()方法是自定义控件中非常重要的一个方法,下面我们来系统的学习,由浅至深来Measure的过程

Android自定义View详解

1. Measure的作用

测量View的宽/高

1.在某些情况下,需要多次测量(measure)才能确定View最终的宽/高;
2.在这种情况下measure过程后得到的宽/高可能是不准确的;
3.建议在layout过程中onLayout()去获取最终的宽/高

2. 准备的基础

在了解measure 过程前,我们需要先了解measure过程中传递尺寸(宽 / 高测量值)的两个类:

ViewGroup.LayoutParams (View 自身的布局参数)
MeasureSpecs 类(父视图对子视图的测量要求)

2.1 ViewGroup.LayoutParams

  • 这个类我们很常见,用来指定视图的高度(height)和宽度(width)等布局参数。可通过以下参数进行指定:
参数 解释
fill_parent 即一个View,如TextView
match_parent 与fill_parent相同,用于Android 2.3及之后版本
wrap_content 自适应大小,强制性地使视图扩展以便显示其全部内容(含 padding )
android:layout_weight="wrap_content"   //自适应大小  
android:layout_weight="match_parent"   //与父视图等高  
android:layout_weight="fill_parent"    //与父视图等高  
android:layout_weight="100dip"         //精确设置高度值为 100dip
1.ViewGroup 的子类包括RelativeLayout、LinearLayout等;
2.如 RelativeLayout的 ViewGroup.LayoutParams 的子类是RelativeLayoutParams。
  • 构造函数

    构造函数是View的入口,可以用于初始化一些的内容,和获取自定义属性。

// View的构造函数有四种重载
    public DIY_View(Context context){
        super(context);
    }

    public DIY_View(Context context,AttributeSet attrs){
        super(context, attrs);
    }

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

// 第三个参数:默认Style
// 默认Style:指在当前Application或Activity所用的Theme中的默认Style
// 且只有在明确调用的时候才会生效,
    }

    public DIY_View(Context context,AttributeSet attrs,int defStyleAttr ,int defStyleRes){
        super(context, attrs,defStyleAttr,defStyleRes);
    }

// 最常用的是1和2
}

2.2 MeasureSpec

2.2.1 定义

测量规格

可以理解为:测量View的依据

2.2.2 类型

MeasureSpec的类型分为两种:

Android自定义View详解

即每个MeasureSpec代表了一组宽度和高度的测量规格

2.2.3 作用

决定了一个View的大小(宽/高)

即宽测量值(widthMeasureSpec)和高测量值(heightMeasureSpec)决定了View的大小

2.2.4 组成

如下图:

Android自定义View详解

Android自定义View详解

其中,Mode模式共分为三类:

  • UNSPECIFIED模式()

    unspecified(未指明的;未详细说明的)

  • EXACTLY模式

    exactly(恰好地;正是;精确地;正确地)

  • AT_MOST模式

具体说明如下图:

Android自定义View详解

2.2.5 MeasureSpec类的使用

  • MeasureSpec 、Mode 和Size都封装在View类中的一个内部类里 - MeasureSpec类。

  • MeasureSpec类通过使用二进制,将mode和size打包成一个int值来减少对象内存分配,用一个变量携带两个数据(size,mode),并提供了打包和解包的方法。具体源代码解析如下:

public class MeasureSpec {  
        //进位大小为2的30次方
        //int的大小为32位,所以进位30位就是要使用int的32和31位做标志位) 
        private static final int MODE_SHIFT = 30;  

        // 运算遮罩,0x3为16进制,10进制为3,二进制为11。3向左进位30,就是11 00000000000(11后跟30个0)  
        // 遮罩的作用是用1标注需要的值,0标注不要的值。因为1与任何数做与运算都得任何数,0与任何数做与运算都得0
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;  

        // 0向左进位30 = 00后跟30个0,即00 00000000000
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;  

        // 1向左进位30 = 01后跟30个0 ,即01 00000000000
        public static final int EXACTLY     = 1 << MODE_SHIFT;  

        // 2向左进位30 = 10后跟30个0,即10 00000000000
        public static final int AT_MOST     = 2 << MODE_SHIFT;  

        /* 根据提供的size和mode得到一个详细的测量结果 */  
        public static int makeMeasureSpec(int size, int mode) {  
        // measureSpec = size + mode
        //注:二进制的加法,不是十进制的加法!
            return size + mode;  
        //设计的目的就是使用一个32位的二进制数,32和31位代表了mode的值,后30位代表size的值  
        // 例如size=100(4),mode=AT_MOST,则measureSpec=100+10000...00=10000..00100  
        }  


        /* 通过详细测量结果获得mode */   
        public static int getMode(int measureSpec) {  
         // mode = measureSpec & MODE_MASK;  
        // MODE_MASK = 11 00000000000(11后跟30个0)
        //原理:用MODE_MASK后30位的0替换掉measureSpec后30位中的1,再保留32和31位的mode值。  
        // 例如10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST),这样就得到了mode的值
            return (measureSpec & MODE_MASK);  
        }  



        /* 通过详细测量结果获得size */   
        public static int getSize(int measureSpec) {  
         // size = measureSpec & ~MODE_MASK;  
        // 原理同上,不过这次是将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),将32,31替换成0也就是去掉mode,保留后30位的size  
            return (measureSpec & ~MODE_MASK);  
        } 

}  

// 可以通过下面方式获取specMode和SpecSize
//获取specMode
int specMode = MeasureSpec.getMode(measureSpec)

//获取SpecSize
int specSize = MeasureSpec.getSize(measureSpec)

//也可以通过这两个值生成新的SpecMode
int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);

2.2.6 MeasureSpec值的确定

  • 上面讲了那么久MeasureSpec,那么,MeasureSpec值到底是如何计算得来的呢?

  • 结论:子View的MeasureSpec值是根据 子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的,具体计算逻辑封装在getChildMeasureSpec()里

如下图:

Android自定义View详解

下面,我们来看getChildMeasureSpec()的源码分析:

//作用:
/ 根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
//即子view的确切大小由两方面共同决定:父view的MeasureSpec 和 子view的LayoutParams属性 


public static int getChildMeasureSpec(int spec, int padding, int childDimension) {  

 //参数说明
 * @param spec 父view的详细测量值(MeasureSpec) 
 * @param padding view当前尺寸的的内边距和外边距(padding,margin) 
 * @param childDimension 子视图的布局参数(宽/高)

    //父view的测量模式
    int specMode = MeasureSpec.getMode(spec);     

    //父view的大小
    int specSize = MeasureSpec.getSize(spec);     

    //通过父view计算出的子view = 父大小-边距(父要求的大小,但子view不一定用这个值)   
    int size = Math.max(0, specSize - padding);  

    //子view想要的实际大小和模式(需要计算)  
    int resultSize = 0;  
    int resultMode = 0;  

    //通过父view的MeasureSpec和子view的LayoutParams确定子view的大小  


    // 当父view的模式为EXACITY时,父view强加给子view确切的值
   //一般是父view设置为match_parent或者固定值的ViewGroup 
    switch (specMode) {  
    case MeasureSpec.EXACTLY:  
        // 当子view的LayoutParams>0,即有确切的值  
        if (childDimension >= 0) {  
            //子view大小为子自身所赋的值,模式大小为EXACTLY  
            resultSize = childDimension;  
            resultMode = MeasureSpec.EXACTLY;  

        // 当子view的LayoutParams为MATCH_PARENT时(-1)  
        } else if (childDimension == LayoutParams.MATCH_PARENT) {  
            //子view大小为父view大小,模式为EXACTLY  
            resultSize = size;  
            resultMode = MeasureSpec.EXACTLY;  

        // 当子view的LayoutParams为WRAP_CONTENT时(-2)      
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
            //子view决定自己的大小,但最大不能超过父view,模式为AT_MOST  
            resultSize = size;  
            resultMode = MeasureSpec.AT_MOST;  
        }  
        break;  

    // 当父view的模式为AT_MOST时,父view强加给子view一个最大的值。(一般是父view设置为wrap_content)  
    case MeasureSpec.AT_MOST:  
        // 道理同上  
        if (childDimension >= 0) {  
            resultSize = childDimension;  
            resultMode = MeasureSpec.EXACTLY;  
        } else if (childDimension == LayoutParams.MATCH_PARENT) {  
            resultSize = size;  
            resultMode = MeasureSpec.AT_MOST;  
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
            resultSize = size;  
            resultMode = MeasureSpec.AT_MOST;  
        }  
        break;  

    // 当父view的模式为UNSPECIFIED时,父容器不对view有任何限制,要多大给多大
    // 多见于ListView、GridView  
    case MeasureSpec.UNSPECIFIED:  
        if (childDimension >= 0) {  
            // 子view大小为子自身所赋的值  
            resultSize = childDimension;  
            resultMode = MeasureSpec.EXACTLY;  
        } else if (childDimension == LayoutParams.MATCH_PARENT) {  
            // 因为父view为UNSPECIFIED,所以MATCH_PARENT的话子类大小为0  
            resultSize = 0;  
            resultMode = MeasureSpec.UNSPECIFIED;  
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
            // 因为父view为UNSPECIFIED,所以WRAP_CONTENT的话子类大小为0  
            resultSize = 0;  
            resultMode = MeasureSpec.UNSPECIFIED;  
        }  
        break;  
    }  
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);  
}
  • 关于getChildMeasureSpec()里对于子View的测量模式和大小的判断逻辑有点复杂;

  • 别担心,我已经帮大家总结好。具体子View的测量模式和大小请看下表:

Android自定义View详解

规律总结:(以子View为标准,横向观察)

  • 当子View采用具体数值(dp / px)时

    无论父容器的测量模式是什么,子View的测量模式都是EXACTLY且大小等于设置的具体数值;

  • 当子View采用match_parent时

    子View的测量模式与父容器的测量模式一致

    若测量模式为EXACTLY,则子View的大小为父容器的剩余空间;若测量模式为AT_MOST,则子View的大小不超过父容器的剩余空间

  • 当子View采用wrap_parent时

    无论父容器的测量模式是什么,子View的测量模式都是AT_MOST且大小不超过父容器的剩余空间。

UNSPECIFIED模式:由于适用于系统内部多次measure情况,很少用到,故此处不讨论

注:区别于顶级View(即DecorView)的计算逻辑

Android自定义View详解

3. Measure过程详解

measure过程根据View的类型分为两种情况:

如果需要重新Measure,应该调用RequestLayout()方法

  1. View类型 = 单一View时:只测量自身一个View;

  2. View类型 = ViewGroup时:对ViewGroup视图中所有的子View都进行测量

    即遍历去调用所有子元素的measure方法,然后各子元素再递归去执行这个流程。

接下来,我将详细分析这两个measure过程。

3.1 单一View的measure过程

  • 应用场景

    在没有现成的View,需要自己实现的时候,就使用自定义View,一般继承自View,SurfaceView或其他的View,不包含子View。

    1.如:制作一个支持加载网络图片的ImageView
    2.特别注意:自定义View在大多数情况下都有替代方案,利用图片或者组合动画来实现,但是使用后者可能会面临内存耗费过大,制作麻烦更诸多问题。
    单一View的measure过程如下图所示:

Android自定义View详解

下面我将一个个方法进行详细分析。

3.1.1 measure()

  • 作用:基本测量逻辑的判断;调用onMeasure()

    属于View.java类 & final类型,即子类不能重写此方法

  • 源码分析如下:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

//参数说明:View的宽 / 高测量规格
    ...
    int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
            mMeasureCache.indexOfKey(key);
    if (cacheIndex < 0 || sIgnoreMeasureCache) {
        // 计算视图大小
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    } else {
        ...

}

measure()最终会调用onMeasure()方法。下面继续看onMeasure()的介绍

3.1.2 onMeasure()

  • 作用:调用 getDefaultSize() 定义对View尺寸的测量逻辑;调用 setMeasuredDimension() 存储测量后的View宽 / 高

  • 源码分析如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
//参数说明:View的宽 / 高测量规格

//setMeasuredDimension()  用于获得View宽/高的测量值
//这两个参数是通过getDefaultSize()获得的
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),  
           getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));  
}

下面继续看 setMeasuredDimension() 的分析

3.1.3 setMeasuredDimension()

  • 作用:存储测量后的View宽 / 高。

    该方法就是我们重写onMeasure()所要实现的最终目的

  • 源码分析如下:

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {  

//参数说明:测量后子View的宽 / 高值

//将测量后子View的宽 / 高值进行传递
    mMeasuredWidth = measuredWidth;  
    mMeasuredHeight = measuredHeight;  

    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;  
}

由于 setMeasuredDimension(int measuredWidth, int measuredHeight) 的参数是从 getDefaultSize() 获得的,下面我们继续看 getDefaultSize() 的介绍

3.1.4 getDefaultSize()

  • 作用:根据View宽/高的测量规格计算View的宽/高值

  • 源码分析如下:

public static int getDefaultSize(int size, int measureSpec) {  

//参数说明:
// 第一个参数size:提供的默认大小
// 第二个参数:宽/高的测量规格(含模式 & 测量大小)

    //设置默认大小
    int result = size; 

    //获取宽/高测量规格的模式 & 测量大小
    int specMode = MeasureSpec.getMode(measureSpec);  
    int specSize = MeasureSpec.getSize(measureSpec);  

    switch (specMode) {  
        // 模式为UNSPECIFIED时,使用提供的默认大小
        // 即第一个参数:size 
        case MeasureSpec.UNSPECIFIED:  
            result = size;  
            break;  
        // 模式为AT_MOST,EXACTLY时,使用View测量后的宽/高值
        // 即measureSpec中的specSize
        case MeasureSpec.AT_MOST:  
        case MeasureSpec.EXACTLY:  
            result = specSize;  
            break;  
    }  

 //返回View的宽/高值
    return result;  
}
  • 上面提到,当模式是UNSPECIFIED时,使用的是提供的默认大小(即第一个参数size)。

    那么,提供的默认大小具体是多少呢?

  • 答: 在onMeasure() 方法中, getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec) 中传入的默认大小是 getSuggestedMinimumWidth()

接下来我们继续看 getSuggestedMinimumWidth() 的源码分析

由于 getSuggestedMinimumHeight() 类似,所以此处仅分析 getSuggestedMinimumWidth()

  • 源码分析如下:
protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth,mBackground.getMinimumWidth());
}

//getSuggestedMinimumHeight()同理

从代码可以看出:

  • 如果View没有设置背景,View的宽度为mMinWidth

    1.mMinWidth = android:minWidth属性所指定的值;
    2.若android:minWidth没指定,则默认为0
  • 如果View设置了背景,View的宽度为 mMinWidthmBackground.getMinimumWidth() 中的最大值

那么, mBackground.getMinimumWidth() 的大小具体是指多少呢?接下来继续看 getMinimumWidth() 的源码分析:

public int getMinimumWidth() {
    final int intrinsicWidth = getIntrinsicWidth();
    //返回背景图Drawable的原始宽度
    return intrinsicWidth > 0 ? intrinsicWidth :0 ;
}

由源码可知: mBackground.getMinimumWidth() 的大小具体是指背景图Drawable的原始宽度。

1.若无原始宽度,则为0;
2.那么Drawable什么情况下有原始宽度?如:ShapeDrawable没有,但BitmapDrawable有。

总结:

对于 getDefaultSize() 计算View的宽/高值的逻辑如下:

Android自定义View详解

至此,单一View的宽/高值已经测量完成,即对于单一View的measure过程已经完成。

3.1.6 总结

  • 对于单一View的measure过程,如下:

Android自定义View详解

  • 对于每个方法的总结如下:

Android自定义View详解

3.2 ViewGroup的measure过程

  • 应用场景

    自定义ViewGroup一般是利用现有的组件根据特定的布局方式来组成新的组件,大多继承自ViewGroup或各种Layout(含有子View)。

  • 如:底部导航条中的条目,一般都是上图标(ImageView)、下文字(TextView),那么这两个就可以用自定义ViewGroup组合成为一个Veiw,提供两个属性分别用来设置文字和图片,使用起来会更加方便。

Android自定义View详解

  • 原理

    通过遍历所有的子View进行子View的测量,然后将所有子View的尺寸进行合并,最终得到ViewGroup父视图的测量值。

Android自定义View详解

这样自上而下、一层层地传递下去,直到完成整个View树的measure()过程

  • ViewGroup的measure过程

    如下图所示:

Android自定义View详解

下面我将一个个方法进行详细分析。

3.2.1 MeasureChildren()

  • 和单一View的measure过程是从 measure() 开始不同,ViewGroup的 measure 过程是从 measureChildren() 开始的。
1.ViewGroup是一个抽象类,自身没有重写View的onMeasure();
2.若需要进行自定义View,则需要对onMeasure()进行重写,下文会提到
  • 作用:遍历子View并调用measureChild()进行下一步测量
  • 源码分析如下:
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
//参数说明:父视图的测量规格(MeasureSpec)

        final int size = mChildrenCount;
        final View[] children = mChildren;

        //遍历所有的子view
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
        //如果View的状态不是GONE就调用measureChild()去进行下一步的测量
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

下面,我们继续看measureChild()的分析。

3.2.2 MeasureChild()

  • 作用:计算单个子View的MeasureSpec;调用子View的measure()进行每个子View最后的宽 / 高测量
  • 源码分析如下:
protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {

        // 获取子视图的布局参数
        final LayoutParams lp = child.getLayoutParams();

        // 调用getChildMeasureSpec(),根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
         // getChildMeasureSpec()请回看上面的解析
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,// 获取 ChildView 的 widthMeasureSpec
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,// 获取 ChildView 的 heightMeasureSpec
                mPaddingTop + mPaddingBottom, lp.height);

        // 将计算好的子View的MeasureSpec值传入measure(),进行最后的测量
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

下面,我们继续看measure()的分析。

3.2.3 measure()

  • 作用:基本测量逻辑的判断;调用onMeasure()

    与单一View measure过程中讲的measure()是一致的。

  • 源码分析如下:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
            mMeasureCache.indexOfKey(key);
    if (cacheIndex < 0 || sIgnoreMeasureCache) {

        // 调用onMeasure()计算视图大小
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    } else {
        ...

}

下面,我们继续看onMeasure()的分析。

3.2.4 onMeasure()

  • 首先明确:ViewGroup是一个抽象类,自身没有重写View的onMeasure();

  • 问:为什么ViewGroup的measure过程不像单一View的measure过程那样对onMeasure()做统一的实现?(如下代码)

//单一View中的onMeasure统一实现
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  

//setMeasuredDimension()  用于获得View宽/高的测量值
//这两个参数是通过getDefaultSize()获得的
//下面继续看setMeasuredDimension()  源码
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),  
           getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));  
}
  • 答:因为不同的ViewGroup子类(LinearLayout、RelativeLayout或自定义ViewGroup子类等)具备不同的布局特性,这导致他们子View的测量方法各有不同;而onMeasure()的作用在于测量View的宽/高值。

    因此,ViewGroup无法对onMeasure()作统一实现。

在自定义View中,关键在于根据你的自定义View去复写onMeasure()从而实现你的子View测量逻辑。复写onMeasure()的模板如下:

//根据自身的测量逻辑复写onMeasure()

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

      //定义存放测量后的View宽/高的变量
      int widthMeasure ;
      int heightMeasure ;


      //定义测量方法
      void measureCarson{

       //定义测量的具体逻辑

                }

//记得!最后使用setMeasuredDimension()  存储测量后View宽/高的值
setMeasuredDimension(widthMeasure,  heightMeasure);  
}

//最终setMeasuredDimension()会像上面单一View的measure过程中提到的,存储好测量后View宽/高的值并进行传递。

上面说的便是单一View的measure过程与ViewGroup过程最大的不同:单一View measure过程的onMeasure()具有统一实现,而ViewGroup则没有。

注:其实,在单一View measure过程中,getDefaultSize()只是简单的测量了宽高值,在实际使用时有时需要进行更精细的测量。所以有时候也需要重写onMeasure()。

3.2.5 总结

  • 对于ViewGroup的measure过程,如下:

Android自定义View详解

  • 对于每个方法的总结如下:

Android自定义View详解

为了让大家更好地理解ViewGroup的measure过程(特别是复写onMeasure()),所以接下来,我将用ViewGroup的子类LinearLayout来分析下ViewGroup的measure过程。

3.2.6 实例解析(LinearLayout)

Android自定义View详解

在上述流程中,前4个方法的实现与上面所说是一样的,这里不作过多阐述,直接进入LinearLayout复写的 onMeasure() 代码分析:

// 详细分析请看代码注释
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    //根据不同的布局属性进行不同的计算
    if (mOrientation == VERTICAL) {
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}

    // 此处只选垂直方向的测量过程,即measureVertical()
    // 该方法代码非常多,此处仅分析重要的逻辑

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {

    // 获取垂直方向上的子View个数
    final int count = getVirtualChildCount();

    // 遍历子View获取其高度,并记录下子View中最高的高度数值
    for (int i = 0; i < count; ++i) {
        final View child = getVirtualChildAt(i);

        // 子View不可见,直接跳过该View的measure过程,getChildrenSkipCount()返回值恒为0
        // 注:若view的可见属性设置为VIEW.INVISIBLE,还是会计算该view大小
        if (child.getVisibility() == View.GONE) {
           i += getChildrenSkipCount(child, i);
           continue;
        }

        // 记录子View是否有weight属性设置,用于后面判断是否需要二次measure
        totalWeight += lp.weight;

        if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
            // 如果LinearLayout的specMode为EXACTLY且子View设置了weight属性,在这里会跳过子View的measure过程
            // 同时标记skippedMeasure属性为true,后面会根据该属性决定是否进行第二次measure
          // 若LinearLayout的子View设置了weight,会进行两次measure计算,比较耗时
            // 这就是为什么LinearLayout的子View需要使用weight属性时候,最好替换成RelativeLayout布局

            final int totalLength = mTotalLength;
            mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
            skippedMeasure = true;
        } else {
            int oldHeight = Integer.MIN_VALUE;

            // 在该方法内部,最终会调用到子View的measure方法,计算出子View的大小
           //  即遍历子View并调用measure(),形成递归
            measureChildBeforeLayout(
                   child, i, widthMeasureSpec, 0, heightMeasureSpec,
                   totalWeight == 0 ? mTotalLength : 0);

            if (oldHeight != Integer.MIN_VALUE) {
               lp.height = oldHeight;
            }


            final int childHeight = child.getMeasuredHeight();
            // mTotalLength用于存储LinearLayout在竖直方向的高度
            final int totalLength = mTotalLength;
            //每测量一个子View的高度, mTotalLength就会增加
            mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                   lp.bottomMargin + getNextLocationOffset(child));
    }

    // 记录LinearLayout占用的总高度
    // 即除了子View的高度,还有本身的padding属性值
    mTotalLength += mPaddingTop + mPaddingBottom;
    int heightSize = mTotalLength;


// 最终调用setMeasuredDimension()  设置测量后View宽/高的值
setMeasureDimension(resolveSizeAndState(maxWidth,width))


  ...
}

至此,自定义View的中最重要、最复杂的measure过程已经讲完了。


以上所述就是小编给大家介绍的《Android自定义View详解》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Tales from Facebook

Tales from Facebook

Daniel Miller / Polity Press / 2011-4-1 / GBP 55.00

Facebook is now used by nearly 500 million people throughout the world, many of whom spend several hours a day on this site. Once the preserve of youth, the largest increase in usage today is amongst ......一起来看看 《Tales from Facebook》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具