1.ViewRoot和DecorView
ViewRoot对应ViewRootImpl,实现了DecorView和WindowManager之间的交互。
View的绘制流程从ViewRoot#performTraversals开始,经过measure、layout、draw最终将一个View绘制出来:
例,measur过程:performMeasure->measure->onMeasure->子View的measure。
measure决定了View的宽高,measure完成后可以通过getMeasureWidth/getMeasureHeight获取测量后的宽高;layout决定了View四个顶点的坐标和实际View的宽高,可调用getLeft/getTop/getRight/getBottom/getWidth/getHeight获取对应属性;draw决定了View的显示,draw完成后View的内容才显示在屏幕上。
Activity中通过setContentView设置的view位于DecorView的content部分,可以通过android.R.id.content索引到该View的父容器,然后通过getChildAt(0)定位到该View:
2.MeasureSpec
- 用于parent向child传递layout要求。真正传递的实际上是一个32位int存储,高2位代表mode,低30位代表size,MeasureSpec只是一个工具类,帮助拼装和拆解这个int。
mode:- UNSPECIFIED
parent不对child强加任何限制。child想要多大就多大。 - EXACTLY
parent已经决定了child准确的大小,child要依据这个大小。 - AT_MOST
child可以想多大就多大,但是被指定了一个上限。
- UNSPECIFIED
- 与LayoutParams的关系
对于一个View,可以设置LayoutParams来指定宽高,系统会综合该LayoutParams和parent施加的MeasureSpec,得出最后应用于该View的MeasureSpec;而对于DecorView,因为其没有parent,所以取而代之的是Window的size,结合自己的LayoutParams得出最后的MeasureSpec。MeasureSpec一旦确定,onMeasure中就可以确定View的宽高。- DecorView的MeasureSpec计算过程:
在ViewRootImpl的measureHierarchy中,计算了DecorView的MeasureSpec。desiredWindow*为window的size:
getRootMeasureSpec中根据window size和DecorView的LayoutParams计算出MeasureSpec。规则很简单,如果是MATCH_PARENT或者固定的值,则spec mode为EXACTLY,同时size设置为相应的值;如果是WRAP_CONTENT,则spec mode为AT_MOST,size为window size:childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
private static int getRootMeasureSpec(int windowSize, int rootDimension) { int measureSpec; switch (rootDimension) { case ViewGroup.LayoutParams.MATCH_PARENT: // Window can't resize. Force root view to be windowSize. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); break; case ViewGroup.LayoutParams.WRAP_CONTENT: // Window can resize. Set max size for root view. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); break; default: // Window wants to be an exact size. Force root view to be that size. measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); break; } return measureSpec; }
- 普通View的MeasureSpec计算过程:
以ViewGroup的measureChildWithMargins为例,在该方法中会计算child的MeasureSpec。计算完成后,会直接对该view进行measure。计算时也会考虑parent的padding,child的margin:
具体的计算过程在getChildMeasureSpec中进行:protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
- child指定确定的size,则遵从child的这个size设置。
- child指定match_parent,如果parent表示可以exactly,则其size为parent size;如果parent表示atmost,即其size也不确定,则其atmost为parent size。
- child指定wrap_content,则此时size由child自己决定,所以只限制其atmost为parent size。
- DecorView的MeasureSpec计算过程:
3.View的渲染流程
measure确定测量宽高->layout确定最终宽高和四个顶点位置->draw绘制到屏幕上
- measure
- View的measure
View的measure过程由其measure方法执行,其中会调用onMeasure,具体的计算在onMeasure中进行。measure用final修饰,所以只有onMeasure可以而且必须被子类复写。onMeasure的默认实现,是通过setMeasuredDimension(onMeasure中一定要调用该方法设置measure出的值)设置测量值为view默认的size:
getSuggestedMinimumWidth中会根据android:minWidth和是否设置了background得出minwidth。getSuggestedMinimumHeight原理一样:protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
默认的size在getDefaultSize中计算。对于parent的spec mode为UNSPECIFIED的情况,最终的size即为minwidth;在AT_MOST和EXACTLY的情况下默认的size就是parent中指定的size:protected int getSuggestedMinimumHeight() { return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight()); } protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); }
所以,onMeasure的默认实现都会将measure size设置为parent size。对于child使用wrap_content的情况,这通常不是符合预期的设置。所以在自定义View的时候,需要重写onMeasure方法,将View的measure size设置为预期的默认值,一般是该View的默认最小值。(这也是为什么一定要重写onMeasure)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中,针对AT_MOST的情况,将对应的size(width或者height)设置为默认最小值。因为在ViewGroup的getChildMeasureSpec方法中,针对child为wrap_content和child为match_parent+parent为wrap_content这两种情况,最终的spec mode都会是AT_MOST,即针对无法由parent决定child的情况,最终都会是AT_MOST。 - ViewGroup的measure
ViewGroup除了完成自身的measure之外,还要遍历子View去执行其measure方法。因为ViewGroup不同的派生类具有不同布局特性,所以测量方式也不同,故没有提供默认的onMeasure方法。但是ViewGroup中提供了简单的measure其child的方法,提供给其派生类使用(在其onMeasure中调用);复杂的情况下,其派生类一般是自己实现。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); } } } 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); } protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { ... }
- 获取View的宽高的tips:
- 因为系统可能多次measure之后,才能确定最终的宽高,所以measure之后的measureWidth和measureHeight可能是不准确的,这个时候就要在onLayout之后去获取宽高。
- 在Activity的生命周期回调中无法直接获取view的宽高,因为View的渲染过程和其声明周期回调不是同步执行的,可以通过如下方法:
- Activity/View#onWindowFocusChanged
该方法会在Activity的窗口获得和失去焦点的时候被调用;伴随着焦点的变化,该方法会被调用多次:@Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus) { int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } }
- view.post(Runnable)
当View初始化完毕之后,Looper就开始执行各个post进去的Runnable:@Override protected void onStart() { super.onStart(); view.post(new Runnable() { @Override public void run() { int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } }); }
- ViewTreeObserver
如果一个View的view tree的layout状态或者view的可见性发生了变化,onGlobalLayout就会被回调;伴随着view tree的变化,该方法会被调用多次:ViewTreeObserver observer = view.getViewTreeObserver(); observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { view.getViewTreeObserver().removeOnGlobalLayoutListener(this); int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } });
- view.measure
手动调用view.measure去测量,然后得到宽高。对于该View的LayoutParams为match_parent的情况,无法使用该方法,因为此时parent的MeasureSpec是不确定的:// dp/px int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY); int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY); view.measure(widthMeasureSpec, heightMeasureSpec);
// wrap_content int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST); int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 < 30) - 1, MeasureSpec.AT_MOST); view.measure(widthMeasureSpec, heightMeasureSpec);
- Activity/View#onWindowFocusChanged
- View的measure
- layout
该步骤用来确定View的位置,依据就是measure之后存储的measure值。由View的layout方法处理:首先会通过setFrame设置自身的l/t/r/b(位置/宽高被确定)然后调用onLayout,在onLayout中需要遍历其所有的子View,计算其layout数据,然后调用其layout方法,直至所有View都layout完成。所以,对于ViewGroup一定要实现onLayout方法。
ViewGroup和View都没有onLayout的默认实现,因为其实现也与具体的布局有关。public void layout(int l, int t, int r, int b) { 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; boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { onLayout(changed, l, t, r, b);
以LinearLayout的layout过程为例来看。首先LinearLayout的layout的起点也是其layout方法,被parent调用之后,设置了l/t/r/b;之后调用自己的onLayout向子View发起layout。根据布局方式的不同,水平和垂直的layout也不同:
对于垂直的情况:childTop会随着child一个个的layout逐渐增大,其表现就是后面child会被放置在更下面;拿到child的measureWidth和measureHeight之后,调用setChildFrame将layout工作传递给该child。@Override protected 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); } }
setChildFrame则直接调用了child的layout方法。而这里的layout方法传递的r和b参数,对应的是l+measureWidth和t+measureHeight计算出来的。所以通常情况下,调用View的getWidth方法(返回的是r-l)和getMeasureWidth方法,其返回值是一致的,即measureWidth的值。而在一些情况下,多次进行measure会导致layout阶段与measure阶段的width不同,但总的来说两者基本上是相等的。height也是一样的情况。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(); final int childHeight = child.getMeasuredHeight(); final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); ... if (hasDividerBeforeChildAt(i)) { childTop += mDividerHeight; } childTop += lp.topMargin; setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight); childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child); i += getChildrenSkipCount(child, i); } } }
private void setChildFrame(View child, int left, int top, int width, int height) { child.layout(left, top, left + width, top + height); }
- draw
- 流程:
- 绘制背景:background.draw(canvas)
- 绘制自己:onDraw
- 绘制children:dispatchDraw->child.draw
- 绘制装饰:onDrawScrollBars
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); drawAutofilledHighlight(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); // Step 7, draw the default focus highlight drawDefaultFocusHighlight(canvas); if (debugDraw()) { debugDrawFocus(canvas); } // we're done... return; }
- tips
setWillNotDraw方法:该方法用于设置表示其是否会进行draw,以便系统进行优化。ViewGroup会默认设置为true,所以如果一个自定义VIewGroup有draw的需求,要将其设置为false。
- 流程:
4.自定义View
- 分类
- 继承View
实现一些特殊的效果,需要重写onDraw。同时需要处理wrap_content和padding。 - 继承ViewGroup
实现自定义布局,实现组合的效果。需要处理自身的measure、layout,以及children的measure、layout。 - 继承特定View
扩展某个已有View的功能。wrap_content和padding不需要自己处理。 - 继承特定ViewGroup
实现组合的效果。不需要处理measure、layout。
- 继承View
- 自定义View须知
- 处理wrap_content
直接继承View或ViewGroup,默认的onMeasure无法正确处理wrap_content。 - 处理padding
直接继承View需要在draw方法中处理padding;直接继承ViewGroup需要在onMeasure和onLayout中考虑padding和margin的影响。 - 尽量不要在View中使用Handler
View内部提供的post系列方法可以满足需求。 - View中的线程和动画,需要及时停止
当View不可见时,需要及时停止,否则可能造成内存泄漏。可以根据回调方法去处理。当View被remove或者VIew所在的Activity退出时,View#onDetachedFromWindow会被调用;当View的Activity启动时,View#onAttactedToWindow会被调用。 - VIew嵌套滚动时,处理好滚动冲突
- 处理wrap_content