Android进阶——Android视图工作机制之measure、layout、draw
前言
自定义View一直是初学者们最头疼的事情,因为他们并没有了解到真正的实现原理就开始试着做自定义View,碰到很多看不懂的代码只能选择回避,做多了会觉得很没自信。其实只要了解了View的工作机制后,会发现是挺简单的,自定义View就是借助View的工作机制开始将View绘制出来的
Android视图工作机制简介
Android视图工作机制按顺序分为以下三步:
- measure:确定View的宽高
- layout:确定View的位置
- draw:绘制出View的形状
Android视图工作机制的相关概念
Android视图工作机制其实挺人性化的,当你真正理解之后,就跟我们画画是一个道理的,下面为了更好的理解,我将自定义View的过程拟物化
相关概念:
- View(照片框):自定义View
- measure(尺子):测量View大小
- MeasureSpec(尺子刻度):测量View大小的测量单位
- layout(照片框的位置):View的具体位置
- draw(笔):绘制View
画图步骤:
- 首先画一个100 x 100的照片框,需要尺子测量出宽高的长度(measure过程)
- 然后确定照片框在屏幕中的位置(layout过程)
- 最后借助尺子用手画出我们的照片框(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来共同决定的
- 首先要知道LayoutParams有三种情况:MATCH_PARENT、WARP_CONTENT、100dp(精确大小)
- 只要子View的MeasureSpec被确定,那么就可以在measure过程中,测量出子View的宽高
二、通过例子来解释结论
假如父容器LinearLayout的MeasureSpec:EXACTLY、AT_MOST的任意一种
子View的LayoutParams:精确大小(100dp)
也就是说:子View必须是指定大小,不管父容器载不载得下子View
所以返回子View的MeasureSpec:EXACTLY
所以返回子View测量出来的大小:子View自身精确大小假如父容器LinearLayout的MeasureSpec:EXACTLY、AT_MOST的任意一种
子View的LayoutParams:MATCH_PARENT
也就是说:子View必须占满整个父容器,那么父容器多大,子View就多大
所以返回子View的MeasureSpec:跟父容器一致
所以返回子View测量出来的大小:父容器可用大小假如父容器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]; //获取子Viewif ((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 uscase 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 uscase MeasureSpec.AT_MOST:if (childDimension >= 0) {// Child wants a specific size... so be itresultSize = 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 becase MeasureSpec.UNSPECIFIED:if (childDimension >= 0) {// Child wants a specific size... let him have itresultSize = childDimension;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.MATCH_PARENT) {// Child wants to be our size... find out how big it should// beresultSize = 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 beresultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;resultMode = MeasureSpec.UNSPECIFIED;}break;}//noinspection ResourceTypereturn MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
从源码中分析,getChildMeasureSpec()
逻辑其实就是总结的内容,取一例说明
case MeasureSpec.EXACTLY: //如果父View为具体的大小,比如100dpif (childDimension >= 0) { //如果子View的宽高有具体值,比如50dp,就直接用50dpresultSize = childDimension; //50dpresultMode = 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;// 记录标记:表示已经设置过DimensionmPrivateFlags |= 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());
}
getSuggestedMinimumWidth
和getSuggestedMinimumHeight
原理是一样的,如果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操作前进行Measureif ((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); // 调用onLayoutmPrivateFlags &= ~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); // 回调}// 如果当前视图为VISIBILITYif ((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绘制到屏幕上,它有如下几个步骤
- 绘制当前视图的背景:drawBackground(canvas)
- 保存当前画布的堆栈状态,并且在在当前画布上创建额外的图层,以便接下来可以用来绘制当前视图在滑动时的边框渐变效果
- 绘制当前视图的内容:if (!dirtyOpaque) onDraw(canvas)
- 绘制当前视图的子视图的内容:dispatchDraw(canvas)
- 绘制当前视图在滑动时的边框渐变效果
- 绘制当前视图的滚动条: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 neededint 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 childrendispatchDraw(canvas);// Overlay is part of the content and draws beneath Foregroundif (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' layersint 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 artifactsif (verticalEdges && (top + length > bottom - length)) {length = (bottom - top) / 2;}// also clip horizontal fades if necessaryif (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 contentif (!dirtyOpaque) onDraw(canvas);// 第四步主要是绘制当前视图的子视图的内容,这种方法一般实现在ViewGroup中,只有ViewGroup才有子View,而在View中是个空实现// Step 4, draw the childrendispatchDraw(canvas);// 第五步主要是绘制当前视图在滑动时的边框渐变效果,主要还是通过canvas去绘制,这里就不做细节介绍了// Step 5, draw the fade effect and restore layersfinal 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 Foregroundif (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,当然,能结合动画那就更完美了,加油
Android进阶——Android视图工作机制之measure、layout、draw相关推荐
- Android视图工作机制之measure、layout、draw
前言 自定义View一直是初学者们最头疼的事情,因为他们并没有了解到真正的实现原理就开始试着做自定义View,碰到很多看不懂的代码只能选择回避,做多了会觉得很没自信.其实只要了解了View的工作机制后 ...
- Android Binder驱动的工作机制之要旨
最近,看了不少Android内核分析的书籍.文章及Android源程序.感觉自己对Android Binder的工作机制算是有了个彻底的理解. 但是,自己是花了很多时间和精力之后才达到这一点的.对于大 ...
- Android进阶-Android自带APIDemo与震动器
Android进阶-Android自带APIDemo与震动器 API-Demo 在android-sdk\samples\android-14\ApiDemos下有许多Android为他的特性提供的D ...
- measure,layout,draw的相关方法
(1)invalidate():请求重新draw(),但只会绘制调用者本身 (2)setSelection() :请求重新draw(),但只会绘制调用者本身 (3)setVisibility() :I ...
- android view强制重绘_android view 相关方法 layout draw 布局 重绘 | 学步园
http://blog.csdn.net/az44yao/article/details/8208087 ViewGroup用onLayout实现view的自由移动 http://qq18715568 ...
- Android进阶——Android弹窗组件工作机制之Dialog、DialogFragment
前言 Android在DialogFragment推出后,就已经不推荐继续使用Dialog,可替换为DialogFragment,其实DialogFragment只不过是对增加一层看不到的Fragme ...
- Android进阶——Android四大组件启动机制之Activity启动过程
前言 Activity启动过程涉及到的比较多的知识点有Binder的跨进程通讯,建议先看完Binder的跨进程通讯再来阅读本篇文章,在文章阅读开始,我们先要理解Activity启动模型,再者去理解有关 ...
- Android进阶——Android跨进程通讯机制之Binder、IBinder、Parcel、AIDL
前言 Binder机制是Android系统提供的跨进程通讯机制,这篇文章开始会从Linux相关的基础概念知识开始介绍,从基础概念知识中引出Binder机制,归纳Binder机制与Linux系统的跨进程 ...
- Android 进阶——Android 系统的基础术语和编译的相关理论小结
文章大纲 引言 一.Android系统的分区 1./boot 引导分区 2./system 系统分区 3./recovery 恢复分区 刷入RE: 4./data 用户数据区 5./cache 数据缓 ...
最新文章
- delphi 汉字的编码 转换
- 加入域时出现以下错误 登陆失败 该目标账户名称不正确_微信支付踩坑合集:微信小程序支付失败是什么原因?持续更新...
- html离线地图,离线地图三维开发-添加HTML
- [转]Pytest 基础教程
- win7下的的IVF2011+VS2010以及OpenMPI的安装与配置
- 模2运算的原理 模2加法,模2减法,模2乘法,模2除法
- SPEOS | SPEOS HUD 设计功能
- 深入解析Scheduler
- 【C++】运算符重载/函数的返回值为解引用
- 《 Python程序设计项目案例》—学生成绩(信息)管理系统普通版设计要求及部分参考代码(期末大作业、结课项目)
- PandoraBox潘多拉多线多播
- python表格绘制斜线表头_Python之ReportLab绘制条形码和二维码
- Matplotlib不显示中文解决办法
- 网络协议 18 - CDN
- 半圆形进度条(html)
- 微信域名检测php,微信域名检测接口(官方api)——PHP请求示例
- React面试题收集
- MATLAB运动车辆检测系统
- Tomcat应用部署,是否要一个萝卜一个坑?
- 移动端导航的七种设计模式