Android进阶——Android视图工作机制之measure、layout、draw

前言

自定义View一直是初学者们最头疼的事情,因为他们并没有了解到真正的实现原理就开始试着做自定义View,碰到很多看不懂的代码只能选择回避,做多了会觉得很没自信。其实只要了解了View的工作机制后,会发现是挺简单的,自定义View就是借助View的工作机制开始将View绘制出来的

Android视图工作机制简介

Android视图工作机制按顺序分为以下三步:

  1. measure:确定View的宽高
  2. layout:确定View的位置
  3. draw:绘制出View的形状

Android视图工作机制的相关概念

Android视图工作机制其实挺人性化的,当你真正理解之后,就跟我们画画是一个道理的,下面为了更好的理解,我将自定义View的过程拟物化

相关概念:

  • View(照片框):自定义View
  • measure(尺子):测量View大小
  • MeasureSpec(尺子刻度):测量View大小的测量单位
  • layout(照片框的位置):View的具体位置
  • draw(笔):绘制View

画图步骤:

  1. 首先画一个100 x 100的照片框,需要尺子测量出宽高的长度(measure过程)
  2. 然后确定照片框在屏幕中的位置(layout过程)
  3. 最后借助尺子用手画出我们的照片框(draw过程)

Android视图工作机制之MeasureSpec

自定义View第一步是测量,而测量需要测量规格(或测量标准)才能知道View的宽高,所以在测量之前需要认识MeasureSpec类

MeasureSpec类是决定View的measure过程的测量规格(比喻:尺子),它由以下两部分组成

  • SpecMode:测量模式(比喻:直尺、三角尺等不同类型)
  • SpecSize:测量模式下的规格大小(比喻:尺子的刻度)

MeasureSpec的表示形式是32位的int值

  • 高2位(前面2位):表示测量模式,即SpecMode
  • 低30位(后面30位):表示在测量模式下的测量规格大小,即SpecSize

MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配

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 makeSafeMeasureSpec(int size, int mode) {
        if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
            return 0;
        }
        return makeMeasureSpec(size, mode);
    }

    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }

    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
}

我们都知道SpecMode的尺子类型有很多,不同的尺子有不同的功能,而SpecSize刻度是固定的一种,所以SpecMode又分为三种模式

  • UNSPECIFIED:未定义模式。父容器不对View有任何大小的限制,这种情况一般用于系统内部,表示一种测量状态
  • EXACTLY:精确模式。父容器检测出View所需要的精确大小,这时候View的值就是SpecSize
  • AT_MOST:最大值模式。父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值

一、结论:子View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定的

  1. 首先要知道LayoutParams有三种情况:MATCH_PARENT、WARP_CONTENT、100dp(精确大小)
  2. 只要子View的MeasureSpec被确定,那么就可以在measure过程中,测量出子View的宽高

二、通过例子来解释结论

  1. 假如父容器LinearLayout的MeasureSpec:EXACTLY、AT_MOST的任意一种
    子View的LayoutParams:精确大小(100dp)
    也就是说:子View必须是指定大小,不管父容器载不载得下子View
    所以返回子View的MeasureSpec:EXACTLY
    所以返回子View测量出来的大小:子View自身精确大小

  2. 假如父容器LinearLayout的MeasureSpec:EXACTLY、AT_MOST的任意一种
    子View的LayoutParams:MATCH_PARENT
    也就是说:子View必须占满整个父容器,那么父容器多大,子View就多大
    所以返回子View的MeasureSpec:跟父容器一致
    所以返回子View测量出来的大小:父容器可用大小

  3. 假如父容器LinearLayout的MeasureSpec:EXACTLY、AT_MOST的任意一种
    子View的LayoutParams:WARP_CONTENT
    也就是说:子View必须自适应父容器,父容器不管多小,你都不能超过它,只能自适应的缩小
    所以返回子View的MeasureSpec:AT_MOST(不能超过父容器本身)
    所以返回子View测量出来的大小:父容器可用大小

至于第4种情况,父容器是UNSPECIFIED的时候,由于父容器不知道自己多大,而子View又采用MATCH_PARENT、WARP_CONTENT的时候,子View肯定也不知道自己多大,所以只有当子View采用EXACTLY的时候,才知道自己多大

三、通过图片分析结论结果

通过上面的例子总结,我们可以通过父容器的测量规格和子View的布局参数来确定子View的MeasureSpec,这样便确立了子View的宽高,下面是父容器测量规格和子View布局参数确立子ViewMeasureSpec的结果图

Android视图工作机制之measure过程

measure过程其实和事件分发有点类似,也包括ViewGroup和View,我们通过各自的源码来分析其measure的过程

一、ViewGroup的measure过程

ViewGroup源码中,提供了一个measureChildren的方法来遍历调用子View的measure方法,而各个子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]; //获取子View
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { //如果是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的measure过程
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

getChildMeasureSpec()表示获取子View的MeasureSpec,从参数中可以看出,子View的MeasureSpec确实是通过父容器的MeasureSpec和子View自身的LayoutParams决定的,这也就印证了结论所说的话。只不过这里的LayoutParams只是取宽和高,而且还要另外算上父View的内边距padding的距离,因为子View的可以容纳的最大空间 = 父View的宽高 - 父View的padding距离,具体体现在getChildMeasureSpec()注释上。至于marging的测量,ViewGroup里面有measureChildWithMargins()用来测量,其实现只是在measureChild()的基础上增加marging的参数

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    //父View的模式和尺寸
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);
    
    //子View真实可以容纳最大的大小 = 父View的宽高 - 父View的padding距离
    int size = Math.max(0, specSize - padding);
    
    //子View的模式和尺寸
    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
    // Parent has imposed an exact size on us
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size. So be it.
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent has imposed a maximum size on us
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            // Child wants a specific size... so be it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size, but our size is not fixed.
            // Constrain child to not be bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent asked to see how big we want to be
    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
            // Child wants a specific size... let him have it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size... find out how big it should
            // be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size.... find out how
            // big it should be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    //noinspection ResourceType
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

从源码中分析,getChildMeasureSpec()逻辑其实就是总结的内容,取一例说明

case MeasureSpec.EXACTLY: //如果父View为具体的大小,比如100dp
    if (childDimension >= 0) { //如果子View的宽高有具体值,比如50dp,就直接用50dp
        resultSize = childDimension; //50dp
        resultMode = MeasureSpec.EXACTLY; //精确模式
    } else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子View的宽高是MATCH_PARENT,就用子View真实可以容纳最大的大小
        resultSize = size; //子View真实可以容纳最大的大小
        resultMode = MeasureSpec.EXACTLY; //精确模式
    } else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子View的宽高是WRAP_CONTENT,就用子View真实可以容纳最大的大小,但是不能超过父View的大小
        resultSize = size;  //子View真实可以容纳最大的大小
        resultMode = MeasureSpec.AT_MOST; //最大值模式:不能超过父View的大小
    }
    break;

二、View的measure过程

View的源码中,由于measure方法是个final类型的,所以子类不能重写此方法

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
	......
	
	// 比较标记:当前正需要布局操作,包括measure和layout两个操作,这个标记会在layout()中被清除
    if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
            widthMeasureSpec != mOldWidthMeasureSpec ||
            heightMeasureSpec != mOldHeightMeasureSpec) {

        int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
                mMeasureCache.indexOfKey(key);
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            // 无缓存的情况
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            // 清除标记:需要在Layout操作前进行Measure,即说明当前的操作表示已经测量完成
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        } else {
            // 有缓存的情况
            long value = mMeasureCache.valueAt(cacheIndex);
            // 获取缓存的宽和高是由mMeasureCache存储的结构决定的
            setMeasuredDimensionRaw((int) (value >> 32), (int) value);
            // 记录标记:需要在Layout操作前进行Measure,即说明当前的操作不需要测量就执行Layout操作,只需设置缓存的值即可
            mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }
        
        // 必须强制执行设置Dimension操作后才不会报错
        if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
            throw new IllegalStateException("View with id " + getId() + ": "
                    + getClass().getName() + "#onMeasure() did not set the"
                    + " measured dimension by calling"
                    + " setMeasuredDimension()");
        }

        mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
    }
	// 记录
	mOldWidthMeasureSpec = widthMeasureSpec;
    mOldHeightMeasureSpec = heightMeasureSpec;
    // 缓存
    mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
            (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}

可以发现,View的measure方法中,mPrivateFlags不为0的时候,表示View当前正在进行某种操作。在执行的过程中,会调用自身的onMeasure()(平时,自定义View重写这个方法,就是对自定义的View根据自己定的规则来确定测量大小),或者调用setMeasuredDimensionRaw(),这两个操作都会将mPrivateFlags设置为PFLAG_MEASURED_DIMENSION_SET,否则会抛出IllegalStateException,也就是说这一步是系统强制要我们执行的,通过注释也能看出来,系统要求必须setMeasuredDimension()执行后,才不会报错

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

setMeasuredDimension()会额外计算insets属性,然后调用setMeasuredDimensionRaw()去记录当前的测量结果,然后将记录下标记

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
        Insets insets = getOpticalInsets();
        int opticalWidth  = insets.left + insets.right;
        int opticalHeight = insets.top  + insets.bottom;

        measuredWidth  += optical ? opticalWidth  : -opticalWidth;
        measuredHeight += optical ? opticalHeight : -opticalHeight;
    }
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;
    // 记录标记:表示已经设置过Dimension
    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

从onMeasure方法中,有getDefaultSize()、getSuggestedMinimumWidth()、getSuggestedMinimumHeight(),它们之间又是什么呢,继续追踪

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

很显然,如果你自定义不重写onMeasure()的话,那么系统就会采用默认的测量模式来确定你的测量大小,即getDefaultSize(),它的逻辑很简单,不去看UNSPECIFIED模式,它就是返回specSize,即View测量后的大小

2、getSuggestedMinimumWidth()和getMinimumHeight()

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

getSuggestedMinimumWidthgetSuggestedMinimumHeight原理是一样的,如果View没有设置背景,那么View的宽度为mMinWidth,而mMinWidth对应的就是android:minWidth这个属性的值,如果这个属性不指定,那么mMinWidth默认为0;如果指定了背景,那么View的宽度就是max(mMinWidth, mBackground.getMinimumWidth()),而这里的getMinimumWidth()又是什么,继续追踪

public int getMinimumWidth() {
    final int intrinsicWidth = getIntrinsicWidth();
    return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

getMinimumWidth是在Drawable类中的,它返回的是Drawable的原始宽度,如果没有Drawable,则返回0

到这里measure过程就结束了,如果是自定义View的话,就重写onMeasure方法,将其默认的测量方式改为我们自己规定的测量方式,最后获得我们的宽高

Android视图工作机制之layout过程

layout过程就比measure过程简单多了,因为它不用什么规格之类的东西,下面是View的layout源码

public void layout(int l, int t, int r, int b) {
    
    // 比较标记:需要在Layout操作前进行Measure
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
        // 重新测量
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
        // 清除标记
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }
    
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;

    // 当前View的位置和上次相比较,是否发生改变
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    // 如果发生改变或者当前处于PFLAG_LAYOUT_REQUIRED为1的时候
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b); // 调用onLayout
        mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED; // 清除标记

        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLayoutChangeListeners != null) {
            ArrayList<OnLayoutChangeListener> listenersCopy =
                    (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();// 克隆,保证数据安全
            int numListeners = listenersCopy.size();
            for (int i = 0; i < numListeners; ++i) {
                listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);// 回调
            }
        }
    }
    
    // 清除标记:当前正需要布局操作,包括measure和layout两个操作,这里表示两个操作已经完成
    mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}

View只需要4个点即可确定一个矩形,参数l、t、r和b分别用来描述当前视图的左上右下四条边与其父视图的左上右下四条边的距离,就是View的相对位置,然后调用onLayout()

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

onLayout()方法其实就是一个空方法,当我们在自定义View时重写onLayout()方法,其实就是让我们重新设置View的位置。回到setFrame()去挖掘它是如何判断View已经发生位置改变的

1、setFrame()

protected boolean setFrame(int left, int top, int right, int bottom) {
    boolean changed = false;

    if (DBG) {
        Log.d("View", this + " View.setFrame(" + left + "," + top + ","
                + right + "," + bottom + ")");
    }

    // 本质就是View的上一次位置和这一次位置发生改变时,就应该记录数值并重新绘制
    if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
        changed = true;

        // 将DRAWN记录在变量drawn中
        int drawn = mPrivateFlags & PFLAG_DRAWN;

        // 计算旧的宽高
        int oldWidth = mRight - mLeft;
        int oldHeight = mBottom - mTop;
        // 计算的宽高
        int newWidth = right - left;
        int newHeight = bottom - top;
        boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

        // 请求重绘
        invalidate(sizeChanged);

        // 记录数值
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
        mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
        // 记录状态:表示已经确定了大小
        mPrivateFlags |= PFLAG_HAS_BOUNDS;

        // 如果新的宽高已经发生了变化
        if (sizeChanged) {
            sizeChange(newWidth, newHeight, oldWidth, oldHeight); // 回调
        }

        // 如果当前视图为VISIBILITY
        if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
            // If we are visible, force the DRAWN bit to on so that
            // this invalidate will go through (at least to our parent).
            // This is because someone may have invalidated this view
            // before this call to setFrame came in, thereby clearing
            // the DRAWN bit.
            // 记录状态
            mPrivateFlags |= PFLAG_DRAWN;
            // 请求重绘
            invalidate(sizeChanged);
            // parent display list may need to be recreated based on a change in the bounds
            // of any child
            // 可能需要基于任何子元素边界的更改重新创建父显示列表
            invalidateParentCaches();
        }

        // Reset drawn bit to original value (invalidate turns it off)
        // 记录标记:回到当前方法开始前的标记,因为在invalidate()中会重新设置PFLAG_DRAWN标记,这里相当于layout操作已经完成
        mPrivateFlags |= drawn;

        mBackgroundSizeChanged = true;
        mDefaultFocusHighlightSizeChanged = true;
        if (mForegroundInfo != null) {
            mForegroundInfo.mBoundsChanged = true;
        }

        notifySubtreeAccessibilityStateChangedIfNeeded();
    }
    return changed;
}

setFrame()表示如果当前视图的大小或者位置与上次相比发生了变化,函数就会返回true。这里会执行两次invalidate(),在invalidate()不一定就是符合条件就能重绘,只不过在这里,你要是符合了,你就可以提前绘制它,实在不行的时候,就在mPrivateFlags被记录为PFLAG_DRAWN时再重绘一次

2、invalidate()

public void invalidate(boolean invalidateCache) {
    invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
        boolean fullInvalidate) {
    if (mGhostView != null) {
        mGhostView.invalidate(true);
        return;
    }

    if (skipInvalidate()) {
        return;
    }

    // 各种标记的判断,符合操作时才可以重绘
    if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
            || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
            || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
            || (fullInvalidate && isOpaque() != mLastIsOpaque)) {
        
        // 各种标记的处理
        if (fullInvalidate) {
            mLastIsOpaque = isOpaque();
            mPrivateFlags &= ~PFLAG_DRAWN; // 清除标记,防止多次绘制
        }
        mPrivateFlags |= PFLAG_DIRTY;
        if (invalidateCache) {
            mPrivateFlags |= PFLAG_INVALIDATED;
            mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
        }

        // Propagate the damage rectangle to the parent view.
        final AttachInfo ai = mAttachInfo;
        final ViewParent p = mParent;
        // 条件必须要符合才可以重绘
        if (p != null && ai != null && l < r && t < b) {
            final Rect damage = ai.mTmpInvalRect;
            damage.set(l, t, r, b);
            p.invalidateChild(this, damage);
        }

        // Damage the entire projection receiver, if necessary.
        if (mBackground != null && mBackground.isProjected()) {
            final View receiver = getProjectionReceiver();
            if (receiver != null) {
                receiver.damageInParent();
            }
        }
    }
}

Android视图工作机制之draw过程

draw过程也很简单,就是将View绘制到屏幕上,它有如下几个步骤

  1. 绘制当前视图的背景:drawBackground(canvas)
  2. 保存当前画布的堆栈状态,并且在在当前画布上创建额外的图层,以便接下来可以用来绘制当前视图在滑动时的边框渐变效果
  3. 绘制当前视图的内容:if (!dirtyOpaque) onDraw(canvas)
  4. 绘制当前视图的子视图的内容:dispatchDraw(canvas)
  5. 绘制当前视图在滑动时的边框渐变效果
  6. 绘制当前视图的滚动条:onDrawForeground(canvas)

在上面的6个操作中,有些地方是可以优化的,在代码中,作者的注释也是做了解释

  • 如果视图本身是透明的,则不需要绘制背景和绘制本身,即跳过第1个和第3个操作
  • 如果视图本身不处于滑动状态,则不需要滚动边框的渐变效果和滚动条,即跳过第2个和第5个操作
public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    // 获取View是否为透明背景
    final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
            (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
    // 清除PFLAG_DIRTY_MASK标记,记录PFLAG_DRAWN标记
    mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
    // Step 1, draw the background, if needed
    int saveCount;

    // 如果透明,则跳过第1个操作
    if (!dirtyOpaque) {
        // 通过canvas的Api绘制出背景图片
        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
        // 如果透明,则跳过第3个操作
        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;
    }
    
    /*
     * Here we do the full fledged routine...
     * (this is an uncommon case where speed matters less,
     * this is why we repeat some of the tests that have been
     * done above)
     */

    // 代码从在这里开始,会完整的完成第2到~第5步的操作
    
    boolean drawTop = false;
    boolean drawBottom = false;
    boolean drawLeft = false;
    boolean drawRight = false;

    float topFadeStrength = 0.0f;
    float bottomFadeStrength = 0.0f;
    float leftFadeStrength = 0.0f;
    float rightFadeStrength = 0.0f;

    // 第二步主要是创建额外的图层来绘制当前视图在滑动时的边框渐变效果,都是有关位置的计算和Canvas的操作,这里就不做细节介绍了
    
    // Step 2, save the canvas' layers
    int paddingLeft = mPaddingLeft;

    final boolean offsetRequired = isPaddingOffsetRequired();
    if (offsetRequired) {
        paddingLeft += getLeftPaddingOffset();
    }

    int left = mScrollX + paddingLeft;
    int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
    int top = mScrollY + getFadeTop(offsetRequired);
    int bottom = top + getFadeHeight(offsetRequired);

    if (offsetRequired) {
        right += getRightPaddingOffset();
        bottom += getBottomPaddingOffset();
    }

    final ScrollabilityCache scrollabilityCache = mScrollCache;
    final float fadeHeight = scrollabilityCache.fadingEdgeLength;
    int length = (int) fadeHeight;

    // clip the fade length if top and bottom fades overlap
    // overlapping fades produce odd-looking artifacts
    if (verticalEdges && (top + length > bottom - length)) {
        length = (bottom - top) / 2;
    }

    // also clip horizontal fades if necessary
    if (horizontalEdges && (left + length > right - length)) {
        length = (right - left) / 2;
    }

    if (verticalEdges) {
        topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
        drawTop = topFadeStrength * fadeHeight > 1.0f;
        bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
        drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
    }

    if (horizontalEdges) {
        leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
        drawLeft = leftFadeStrength * fadeHeight > 1.0f;
        rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
        drawRight = rightFadeStrength * fadeHeight > 1.0f;
    }

    saveCount = canvas.getSaveCount();

    int solidColor = getSolidColor();
    if (solidColor == 0) {
        final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;

        if (drawTop) {
            canvas.saveLayer(left, top, right, top + length, null, flags);
        }

        if (drawBottom) {
            canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
        }

        if (drawLeft) {
            canvas.saveLayer(left, top, left + length, bottom, null, flags);
        }

        if (drawRight) {
            canvas.saveLayer(right - length, top, right, bottom, null, flags);
        }
    } else {
        scrollabilityCache.setFadeColor(solidColor);
    }

    // 第三步主要是绘制当前视图的内容,如果是自定义View的话,我们是需要实现onDraw方法来绘制我们想要的视图

    // Step 3, draw the content
    if (!dirtyOpaque) onDraw(canvas);

    // 第四步主要是绘制当前视图的子视图的内容,这种方法一般实现在ViewGroup中,只有ViewGroup才有子View,而在View中是个空实现

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

    // 第五步主要是绘制当前视图在滑动时的边框渐变效果,主要还是通过canvas去绘制,这里就不做细节介绍了

    // Step 5, draw the fade effect and restore layers
    final Paint p = scrollabilityCache.paint;
    final Matrix matrix = scrollabilityCache.matrix;
    final Shader fade = scrollabilityCache.shader;

    if (drawTop) {
        matrix.setScale(1, fadeHeight * topFadeStrength);
        matrix.postTranslate(left, top);
        fade.setLocalMatrix(matrix);
        p.setShader(fade);
        canvas.drawRect(left, top, right, top + length, p);
    }

    if (drawBottom) {
        matrix.setScale(1, fadeHeight * bottomFadeStrength);
        matrix.postRotate(180);
        matrix.postTranslate(left, bottom);
        fade.setLocalMatrix(matrix);
        p.setShader(fade);
        canvas.drawRect(left, bottom - length, right, bottom, p);
    }

    if (drawLeft) {
        matrix.setScale(1, fadeHeight * leftFadeStrength);
        matrix.postRotate(-90);
        matrix.postTranslate(left, top);
        fade.setLocalMatrix(matrix);
        p.setShader(fade);
        canvas.drawRect(left, top, left + length, bottom, p);
    }

    if (drawRight) {
        matrix.setScale(1, fadeHeight * rightFadeStrength);
        matrix.postRotate(90);
        matrix.postTranslate(right, top);
        fade.setLocalMatrix(matrix);
        p.setShader(fade);
        canvas.drawRect(right - length, top, right, bottom, p);
    }

    canvas.restoreToCount(saveCount);

    drawAutofilledHighlight(canvas);

    // Overlay is part of the content and draws beneath Foreground
    if (mOverlay != null && !mOverlay.isEmpty()) {
        mOverlay.getOverlayView().dispatchDraw(canvas);
    }
    
    // 第六步主要是绘制当前视图的滚动条,也是通过canvas的Api绘制矩形和线条
    // 这里如果给你提供上下左右的位置,我相信大家都能画出滚动条效果来,这里就不做细节介绍了

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

    if (debugDraw()) {
        debugDrawFocus(canvas);
    }
}

Android视图工作机制中的重绘

一、invalidate()和requestLayout()

invalidate()和requestLayout(),常用于View重绘和更新,其主要区别如下

  • invalidate方法只会执行onDraw方法
  • requestLayout方法只会执行onMeasure方法和onLayout方法,并不会执行onDraw方法。

所以当我们进行View更新时,若仅View的显示内容发生改变且新显示内容不影响View的大小、位置,则只需调用invalidate方法;若View宽高、位置发生改变且显示内容不变,只需调用requestLayout方法;若两者均发生改变,则需调用两者,按照View的绘制流程,推荐先调用requestLayout方法再调用invalidate方法

二、invalidate()和postInvalidate()

  • invalidate方法用于UI线程中重新绘制视图
  • postInvalidate方法用于非UI线程中重新绘制视图,省去使用handler

结语

Android的自定义其实很简单,对于初学者,可能就是measure过程比较难以理解,不过不要紧,每个人初学都是这样的,建议多多实践,花点时间去研究,你会更加熟能生巧,根本不用死记硬背,只要有思路便可以画出你想要的自定义View,当然,能结合动画那就更完美了,加油

  • 7
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

许英俊潇洒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值