android 自定义View 详解


读书笔记: 《Android 开发艺术探索》 ——第四章:View 的工作原理

经过上一节 Android View 的事件体系的介绍,对 View 的架构及相关的事件分发有了解,本章主要介绍自定义 View 的相关知识。

对于自定义 View ,主要有: 直接继承View 和 ViewGroup,或者继承现有控件,如 ListView 等。不管使用哪种方式,都要先了解View 的工作原理 ,才能更好的进行自定义 View。

一、理解 MeasureSpec

MeasureSpec 意思是 “度量规格”,它是View 的一个静态内部类,封装了父view传递给子View 的布局要求,
在很大程度上确定了一个View 的尺寸。在测量过程中,系统会将 View 的LayoutParams 根据父容器所施加的规则转换成相应的 MeasureSpec,然后通过它测量 View 的宽高。

MeasureSpec 是一个32 位的int值,高2位代表 SpecModel,低30位代表 SpecSize。 SpecModel 指测量模式,SpecSize指在某种测量模式下的规格大小。这种将来两个个值打包成一个int值,可以避免过多的对象内存分配。对于 SpecModel 主要有如下三种模式:

  • UNSPECIFIED
    该模式下,父容器不对View 有任何限制,要多大给多大,一般用于系统内部。
  • EXACTLY
    该模式下,父容器已经检测出 View 所需的精确大小,此时 View 的最终大小就是 SpecSize,它对应 LayoutParams 中的 match_parent具体数值
  • AT_MOST
    View 的大小不能超过父容器指定的可用大小 (SpecSize) ,它对应 LayoutParams 中的 wrap_parent

上面提到了 View 的绘制还会和 LayoutParams 相关,对于 DecorView ,他的规则如下:

  • LayoutParams.MATCH_PARENT:精确模式,大小就是窗口大小;
  • LayoutParams.WARO_CONTENT:最大模式,大小不定,但不能超过窗口大小;
  • 固定大小:如100dp,精确模式,LayoutParams 中指定的大小。

对于普通的 View 它的 MeasureSpec 创建规则如下:

parentSpecMode
childLayoutParamsl
EXACTLY AT_MOST
UNSPECIFIED
dp/px EXACTLY
childSize
EXACTLY
childSize
EXACTLY
childSize
match_parent EXACTLY
parentSize
AT_MOST
parentSize
UNSPECIFIED
0
wrap_content AT_MOST
parentSize
AT_MOST
parentSize
UNSPECIFIED
0

说明:
对于普通 View 的 MeasureSpec 是由它父容器的 MeasureSpec 和 其本身的 LayoutParams 决定的。

当View 采用固定宽高时,其 MeasureSpec 是精确的,大小是 LayoutParams 指定的大小;
当View 的宽高是 match_parent 时,若其父容器是 精确的,则它也是精确的,大小为父布局的剩余空间;若父容器是最大模式,则view也是最大模式且大小不会超过父容器的剩余空间;
当 view 的宽高都是 wrap_content时,不管父容器是精确还是最大模式,他都是最大模式,大小不超过父容器的剩余空间。
对于 UNSPECIFIED 模式,主要用于系统内部,一般情况下我们不用关注。

二、View 的工作流程

对于View 它的工作流程主要指测量(measure)、布局(layout)、绘制(draw)这三大流程,其中 measure 确定 view 的测量宽高, layout 确定view 的最终宽高和四个顶点的位置, draw 将 view 绘制在屏幕上。

2.1 measure 过程

对于View 的测量,是由 measure方法完成的,而该方法是一个final 类型的,其中调用 了 onMeasure 方法,如下 View中 onMeasure方法源码:

protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec)
{
    //设置view 的测量值
    setMeasuredDimension (getDefaultSize (getSuggestedMinimumWidth(), widthMeasureSpec),
                          getDefaultSize (getSuggestedMinimumHeight(), heightMeasureSpec) );
}

其中,setMeasuredDimension 方法是设置测量值,而 getDefaultSize 方法是获得测量尺寸,如下源码:

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;
}

注意 MeasureSpec.AT_MOSTMeasureSpec.EXACTLY 两个分支语句返回相同结果,他们都是 MeasureSpec 中获取的测量结果。从这里可见 View 的宽高由 spaceSize 决定,所以自定义控件时直接继承view 需要重写 onMeasure 方法,设置 wrap_content 时的大小,否则 使用 wrap_content 就相当于 match_parent了,都是精确模式。

在 onMeasure 中用到了 getSuggestedMinimumWidth 方法,如下源码:

/**
 * 如果无背景,返回mMinWidth(为 android:minWidth 指定的值);
 * 否则,返回 minWidth 指定的值和背景最小宽度两者的最大值
 */
protected int getSuggestedMinimumWidth()
{
    return (mBackground == null) ? mMinWidth :
           max (mMinWidth, mBackground.getMinimumWidth() );
}
//获取背景最小宽度,即 Drawable 的原始宽度,如果没有就返回0
public int getMinimumWidth()
{
    final int intrinsicWidth = getIntrinsicWidth();
    return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

对于ViewGroup 的测量过程,它可以包含多个 View ,所以除了调用自己的测量法法外,还要遍历所有子元素的测量方法。它是一个抽象类,没有onMeasure 方法,但也提供了 measureChildren 方法,在该方法中调用 measureChild 方法,分别测量子view 的宽高。

protected void measureChildren (int widthMeasureSpec, int heightMeasureSpec)
{
    final int size = mChildrenCount;
    final View[] children = mChildren;
    //遍历子view ,测量所有不是 GONE 状态的 view
    for (int i = 0; i < size; ++i)
    {
        final View child = children[i];
        if ( (child.mViewFlags & VISIBILITY_MASK) != GONE)
        {
            measureChild (child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}
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);

    //调用 子view 的测量方法
    child.measure (childWidthMeasureSpec, childHeightMeasureSpec);
}

由于 ViewGroup 的子布局有不同的特性,这里通过调用子布局的 测量方法来测量每一个具体的View 的宽高,最终将他他们累加在一起,在计算具体的 View 时要考虑到 他的 padding 值。

由于view 的测量和 activity 的生命周期不是同的,如果要在 activity 中获取 view 的宽高,不能在 onCreate onResume 等方法中获取,可通过下面几种方式获取:

  • 重写 onWindowFocusChanged(boolean hasFocus) 方法,在 hasFocus 为 true时获取
  • 使用view.post(Runnable runnable) 发送消息队列
  • 使用ViewTreeObserver ,添加 addOnGlobalLayoutListener 监听。

到此,view的测量完成了,接下来就是对其进行布局。

2.2 layout 过程

layout 方法确定 view 本身的位置,而ViewGroup 的 onLayout 方法确定所有子view 的位置。对于View 的layout 方法,首先是 调用 setFrame方法设置四个点的坐标,然后调用父容器的 onLayout 方法,确定子view 的位置。在布局过程中 view的最终宽高被确定,通常和测量宽高相等,他们只是在赋值的过程中不同。

2.3 draw 过程

绘制过程,主要是将view 绘制到屏幕上显示。调用 draw(Canvas canvas)方法,如下源码:

public void draw (Canvas canvas)
{
    final int privateFlags = mPrivateFlags;
    final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
    mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

    /*
     * Draw traversal performs several drawing steps which must be executed
     * in the appropriate order:
     *
     *      1. Draw the background
     *      2. If necessary, save the canvas' layers to prepare for fading
     *      3. Draw view's content
     *      4. Draw children
     *      5. If necessary, draw the fading edges and restore layers
     *      6. Draw decorations (scrollbars for instance)
     */

    // Step 1, draw the background, if needed
    int saveCount;

    if (!dirtyOpaque)
    {
        drawBackground (canvas);
    }

    // skip step 2 & 5 if possible (common case)
    final int viewFlags = mViewFlags;
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
    if (!verticalEdges && !horizontalEdges)
    {
        // Step 3, draw the content
        if (!dirtyOpaque)
        {
            onDraw (canvas);
        }

        // Step 4, draw the children
        dispatchDraw (canvas);

        // Overlay is part of the content and draws beneath Foreground
        if (mOverlay != null && !mOverlay.isEmpty() )
        {
            mOverlay.getOverlayView().dispatchDraw (canvas);
        }

        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground (canvas);

        // we're done...
        return;
    }

主要有四个步骤:

  • 绘制背景:drawBackground (canvas);
  • 绘制自己 :onDraw (canvas);
  • 绘制children:dispatchDraw (canvas);
  • 绘制装饰: onDrawForeground(canvas);

view 通过 dispatchDraw 方法分发绘制的过程,而该方法会遍历所有子vied 的draw方法。如果View 是继承ViewGroup的并且自身不具备绘制功能时,可以调用 setWillNotDraw 设置标记位,使系统对其进行优化。

view 的大致工作流程就是这样的,自定义view涉及到View 的层次结构、事件分发和相关工作原理,尽管挺复杂,掌握它对我们的开发有很大的帮助。

三、自定义View

3.1 View 的分类

常见的自定义view的方式主要有如下几种:

  1. 继承view 重写 onDraw方法;
    这种方式主要用于实现不规则效果,需要自己支持 wrap_content 和 padding的处理
  2. 继承 ViewGroup 派生出特殊的Layout
    自定义布局,需要合适的处理ViewGroup 的测量和布局。
  3. 继承特定的View(如TextView)
    扩展现有控件,需要自己支持 wrap_content 和 padding的处理
  4. 继承特定的ViewGroup(如LinearLayout)
    这种方式和2类似,但不需要自己测量和布局过程。

3.2 自定义view的注意事项

  1. 让View 支持 wrap_content
    在 onMeasure 中对其进行处理,否则控件不支持 wrap_content属性
  2. 让View 支持 padding
    在draw方法中处理 padding,如果是继承自ViewGroup,需要在 onMeasure 中处理 padding 和 margin
  3. 尽量不要使用 Handler ,View 本身提供的有 post方法
  4. view中如果有线程和动画需要及时停止。
  5. 对于嵌套滑动,要处理好滑动冲突

至此,View 的相关知识介绍完毕,接下来就是进行具体自定义操作了。


文章作者: imtianx
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC 4.0 许可协议。转载请注明来源 imtianx !
评论
 上一篇
android 消息机制及其原理 android 消息机制及其原理
读书笔记: 《Android 开发艺术探索》 ——第十章:android 消息机制 对于 android 中的消息机制,主要是指 Handler 的运行机制。在我们平时的开发中 ,对它并不陌生。由于android 是 单线程(UI线程)
下一篇 
Android View 的事件体系 Android View 的事件体系
本文为读书笔记: 《Android 开发艺术探索 》——第三章 View 的事件体系 android 系统虽然提供了很多基本的控件,如Button、TextView等,但是很多时候系统提供的view不能满足我们的需求,此时就需
  目录