Android(安卓)View视图绘制
前几篇事件分发中,我们已经了解了在Android中你所看到的界面的的分层情况Activity->DecorView->ViewGroup->View。本篇我们进入视图绘制的世界。
1、界面(View)结构图
看,经典tree结构
2、简看系统View绘制流程
太详细的此处不多说了(主要是不能暴露自己水平太低…),简之,系统是通过ViewRoot(这里主要是ViewRootImpl)来实现DecorView的视图绘制的。
private void performTraversals() { // cache mView since it is used so much below... final View host = mView; ... int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); // Ask host how big it wants to be performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); performLayout(lp, mWidth, mHeight); performDraw(); ...}
performMeasure 内部执行的也是 decorView.measure(childWidthMeasureSpec, childHeightMeasureSpec),而decorView是ViewGroup,又会循环是线子View.measure。在这里,我们脑中先有一个印象,View的绘制是measure->layout->draw。
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure"); try { mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); }}
3、MeasureSpec
public static class MeasureSpec { private static final int MODE_SHIFT = 30; private static final int MODE_MASK = 0x3 << MODE_SHIFT; /** * Measure specification mode: The parent has not imposed any constraint * on the child. It can be whatever size it wants. */ public static final int UNSPECIFIED = 0 << MODE_SHIFT; /** * Measure specification mode: The parent has determined an exact size * for the child. The child is going to be given those bounds regardless * of how big it wants to be. */ public static final int EXACTLY = 1 << MODE_SHIFT; /** * Measure specification mode: The child can be as large as it wants up * to the specified size. */ public static final int AT_MOST = 2 << MODE_SHIFT; /** * Creates a measure specification based on the supplied size and mode. * * The mode must always be one of the following: * * - {@link android.view.View.MeasureSpec#UNSPECIFIED}
* - {@link android.view.View.MeasureSpec#EXACTLY}
* - {@link android.view.View.MeasureSpec#AT_MOST}
*
* * Note: On API level 17 and lower, makeMeasureSpec's * implementation was such that the order of arguments did not matter * and overflow in either value could impact the resulting MeasureSpec. * {@link android.widget.RelativeLayout} was affected by this bug. * Apps targeting API levels greater than 17 will get the fixed, more strict * behavior.
* * @param size the size of the measure specification * @param mode the mode of the measure specification * @return the measure specification based on size and mode */ public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size, @MeasureSpecMode int mode) { if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } } /** * Extracts the mode from the supplied measure specification. * * @param measureSpec the measure specification to extract the mode from * @return {@link android.view.View.MeasureSpec#UNSPECIFIED}, * {@link android.view.View.MeasureSpec#AT_MOST} or * {@link android.view.View.MeasureSpec#EXACTLY} */ @MeasureSpecMode public static int getMode(int measureSpec) { //noinspection ResourceType return (measureSpec & MODE_MASK); } /** * Extracts the size from the supplied measure specification. * * @param measureSpec the measure specification to extract the size from * @return the size in pixels defined in the supplied measure specification */ public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); }}
1、MeasureSpec 本身是表示父视图对子视图的描述、规定。由specSize和specMode共同组成的,其中specSize记录的是大小,specMode记录的是规格(模式)。是一个32位的int,前两位用于表示specMode,后30位用于表示specSize。make和get的过程都是通过位运算生成。
2、specMode的三种类型:
- EXACTLY
父视图已经决定了子视图的确切尺寸。不论子视图期望自己是多大,都会在限定的边界范围内。对应的子视图已设定的属性为 match_parent 或具体值,比如 100dp。 - AT_MOST
父视图已经给了一个最大尺寸。子视图最大不能超过这个specified size。对应属性为wrap_content。 - UNSPECIFIED
父视图不对子视图有任何约束,它可以达到所期望的任意尺寸。比如 ListView、ScrollView,一般自定义 View 中用不到。
4、View的onMeasure方法
先看经典的view绘制过程。
首先进入View的measure方法,你会发现人家是final的,所以不用想什么了,但是里面调用了onMeasure(widthMeasureSpec, heightMeasureSpec),其中widthMeasureSpec、heightMeasureSpec是父视图对子试图(当前View)的测绘结果,规格要求。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}
getSuggestedMinimumWidth是View或background的最小宽度。然后通过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;}
之后通过setMeasuredDimension()方法来设定测量出的大小,这样一次measure过程就结束了。
在ViewGroup中,对应的是measureChildren方法
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); } }}
之后遍历了measureChild方法。
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);}
其中
** * 该方法是 measureChildren 中最繁重的部分,为每一个 ChildView 计算出自己的 MeasureSpec。 * 目标是将 ChildView 的 MeasureSpec 和 LayoutParams 结合起来去得到一个最合适的结果。 * * @param spec 对该 View 的测绘要求 * @param padding 当前 View 在当前唯独上的 paddingand,也有可能含有 margins * * @param childDimension 在当前维度上(height 或 width)的具体指 * @return 子视图的 MeasureSpec */public static int getChildMeasureSpec(int spec, int padding, int childDimension) { ......... // 根据获取到的子视图的测量要求和大小创建子视图的 MeasureSpec return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
5、Layout过程
View中layout的作用是ViewGroup用来确定子视图(子View)的位置,当ViewGroup的位置被确定后,它在onLayout中遍历所有子元素,执行各自的layout的方法;如果子元素是ViewGroup,将在固定好自己的位置后继续调用onlayout方法,一直循环下去。
如下View的Layout方法(简):
public void layout(int l, int t, int r, int b) { *** int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; //1.setFrame()设置此View在父视图中的坐标位置 boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { //2.如果是ViewGroup,执行onLayout onLayout(changed, l, t, r, b); *** } ***}
其实当我们去查看ViewGroup中的onLayout时,发现也是一个抽象方法,那我们可以参考LinearLayout
@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) { if (mOrientation == VERTICAL) { layoutVertical(l, t, r, b); } else { layoutHorizontal(l, t, r, b); }}
来观察下其中的layoutVertical
void layoutVertical(int left, int top, int right, int bottom) { ... final int count = getVirtualChildCount(); ... for (int i = 0; i < count; i++) { final View child = getVirtualChildAt(i); if (child == null) { childTop += measureNullChild(i); } else if (child.getVisibility() != GONE) { final int childWidth = child.getMeasuredWidth();//measure 过程确定的 Width final int childHeight = child.getMeasuredHeight(); final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); ... if (hasDividerBeforeChildAt(i)) { childTop += mDividerHeight; } childTop += lp.topMargin; //确定 childLeft、childTop 的值 setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight); childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child); i += getChildrenSkipCount(child, i); } }}
可以看到,layoutVertical就是遍历所有(非Gone)的view,并调用setChildFrame来确定子元素的位置
private void setChildFrame(View child, int left, int top, int width, int height) { child.layout(left, top, left + width, top + height);}
最终执行子view的layout的方法,子view在layout方法中执行setFrame确定自己的位置,同时如果子View时一个ViewGroup则继续执行onLayout方法。
此处有个点,我们常说getMeasuredWidth“通常”等于getWidth,为什么时“通常”呢,有特殊情况吗?
public final int getWidth() { return mRight - mLeft;}
带getMeasuredWidth是指测量宽高,getWidth指最终宽高。如最终的宽是通过mRight - mLeft得到的,如上方layoutVertical中,setChildFrame(child, childLeft+100, childTop + getLocationOffset(child),
childWidth, childHeight),我们将其中的参数改变下,岂不是就有可能不等于了。
6、Draw过程
View的draw总共6步,但是我们一般只需要知道四步必须的。
1. 绘制背景drawBackground(canvas)
2. 绘制自己onDraw(canvas)
3. 绘制children,dispatchDraw(canvas)
4. 绘制装饰(foreground, scrollbars)
public void draw(Canvas canvas) { ... /* * 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; } ... // Step 2, save the canvas' layers ... // Step 3, draw the content if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children dispatchDraw(canvas); // Step 5, draw the fade effect and restore layers ... // Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas);}
另外
- ViewGroup中没有重写draw,因此所有的子视图都是调用自身的draw方法进行绘制。自定义视图时,也不建议override此方法,一般重写onDraw即可,如果非要使用,则需要在重写draw时,加上super.draw(canvas),先执行系统绘制,然后执行自定义内容。
- dispatchDraw() 发起对子视图的绘制。View 中默认是空实现,ViewGroup 复写了dispatchDraw()来对其子视图进行绘制。自定义的 ViewGroup 不应该对dispatchDraw()进行重写。
- invalidate()
请求重绘 View 树,即 draw 过程,假如视图发生大小没有变化就不会调用layout()过程,并且只绘制那些调用了invalidate()方法的 View。
- requestLayout()
当布局变化的时候,比如方向变化,尺寸的变化,会调用该方法,在自定义的视图中,如果某些情况下希望重新测量尺寸大小,应该手动去调用该方法,它会触发measure()和layout()过程,但不会进行 draw。
- requestLayout()
更多相关文章
- Android:你要的WebView与 JS 交互方式 都在这里了
- android性能优化之布局优化
- [Android] 利用java反射调用隐藏Api
- Android(安卓)中几种更新UI界面的方法
- Android高手进阶教程(十七)之---Android中Intent传递对象的两种
- 安卓(Android)系统的检测方法
- Android(安卓)面试题(1)
- 浅谈Java中Collections.sort对List排序的两种方法
- Python list sort方法的具体使用