View的工作原理
文章目录
1.1 ViewRoot和DecorView简介
1.1.1 ViewRoot相关
View的三大流程:1.View的测量流程;2.布局流程;3.绘制流程。
ViewRoot对应ViewRootImpl类,是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot完成的。
在ActivityThread中,当Activity对象创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联:
root = new ViewRootImpl(view.getContext(),display); root.setView(view,wparams,panelParentView);
View的绘制流程是从ViewRoot的performTraversals方法开始的,经过measure、layout和draw三个流程才将View绘制完成。measure用来测量View的宽和高;layout用来确认View在父容器中的放置位置;draw用来将View绘制在屏幕上。
performTraversals会依次调用 performMeasure、performLayout、performDraw三个方法,这三个方法分别完成顶级View的measure、layout、draw三大流程。其中在performMeasure中会调用measure方法,measure方法又会调用onMeasure方法,在onMeasure方法中会对所有子元素进行measure过程,这样measure流程就从父容器流转到子元素中了。子元素重复父容器过程,反复直到整个View树的遍历。performLayout和performDraw的传递流程和performMeasure类似,不同的是:performDraw的传递过程是在draw方法中通过dispatchDraw实现的,本质上无差异。
measure过程决定了View的宽/高。measure完成后,可以通过getMeasuredWidth和getMeasuredHeight获取View测量后的宽/高。正常情况下measure过程的View的宽/高就是实际的View的宽/高。
layout过程决定了View的四个顶点的坐标和实际的View的宽/高。layout完成后,可以通过getTop、getBottom、getLeft、getRight获取View的四个顶点的位置,且可以通过getWidth、getHeight获取View的最终宽/高。
draw过程决定了View的显示。draw完成后,View的内容呈现在屏幕上。
1.1.2 DecorView相关
DecorView作为顶级View,一般情况下内部包含一个竖直方向的LinearLayout,LinearLayout中有上下两部分,标题栏和内容栏(具体与Android版本主题有关)。
在Activity中通过setContentView设置的布局文件就是被加到内容栏中,而内容栏的id是content,所有Activity指定布局的方法叫做setContentView而非setView。
获取content的方法:
ViewGroup content = findViewById(R.android.id.content)
。获取View的方法:
content.getChildAt(0)
。DecorView实际上是一个FrameLayout,View层的事件都是经过DecorView后传递给View的。
1.2 理解MeasureSpec
- MeasureSpec在很大程度上决定一个View的尺寸规格。
- 一个View的实际尺寸规格,还会受父容器影响。即父容器会影响View的MeasureSpec的创建过程。
- 系统会将View的LayoutParams根据父容器施加的规则转换成对应的MeasureSpec,再根据这个MeasureSpec来测量出View的宽/高。
1.2.1 MeasureSpec
MeasureSpec代表一个32位的int值,高2位代表SpecMode,低30位代表SpecSize。SpecMode指测量模式;SpecSize指某种测量模式下的规格大小。
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; 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); } } 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); } ...
MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,同时提供了打包解包的方法。一组SpecMode和SpecSize可以打包成一个MeasureSpec,一个MeasureSpec也可以拆成原始的SpecMode和SpecSize。(MeasureSpec这里指MeasureSpec代表的int,而非对象本身)
SpecMode三种类型:
- 1.UNSPECIFIED:父容器不对View有任何限制,要多大给多大,该情况一般用于系统内部,表示一种测量状态。
- 2.EXACTLY:父容器已经检测出View所需要的精确大小,这时View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式。
- 3.AT_MOST:父容器指定一个可用大小即SpecSize,View的大小不能大于这个值,具体的值还要看View的具体实现。它对应于LayoutParams中的wrap_content。
1.2.2 MeasureSpec和LayoutParams的对应关系
系统内部是通过MeasureSpec来进行View的测量。正常情况下我们使用View指定的MeasureSpec,但我们也可以给View设置LayoutParams。
在View测量的时候,系统会将LayoutParams在父容器的约束下转换成MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽/高。
注意:MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定View的MeasureSpec,从而决定View的宽/高。另外,对于顶级View(即DecorView)和普通View而言,MeasureSpec的转换过程是不同的:
- DecorView:其MeasureSpec由窗口的尺寸和其自身的LayoutParams共同确定。
- 普通View:其MeasureSpec由父容器的MeasureSpec和其自身的LayoutParams共同确定。
MeasureSpec一旦确定,onMeasure中就可以确定View的测量宽/高。
ViewRootImpl中的measureHierarchy方法中的一段代码,展示了DecorView的MeasureSpec的创建过程:
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
其中desiredWindowHeight和childWidthMeasureSpec是屏幕尺寸。
再看getRootMeasureSpec方法的实现:
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; }
上述代码可见DecorView的MeasureSpec的产生过程。其遵守如下规则(根据LayoutParams中的宽/高的参数来划分):
- LayoutParams.MATCH_PARENT:精确模式,大小就是窗口大小。
- LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小。
- 固定大小(比如100dp):精确模式,大小为LayoutParams中指定的大小。
对于普通布局中的View来说,View的measure过程由ViewGroup传递而来。ViewGroup的measureChildWithMargins方法:
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); }
上述方法会对子元素进行measure,在调用子元素的measure方法之前会先通过getChildMeasureSpec方法来得到子元素的MeasureSpec,可见子元素的MeasureSpec的创建与父容器的MeasureSpec和子元素自身的LayoutParams有关,此外还和View的margin及paddig有关。
getChildMeasureSpec的具体代码:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) { int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); int size = Math.max(0, specSize - padding); 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); }
该方法是根据父容器的MeasureSpec和View自身的LayoutParams确定子元素的MeasureSpec,参数中的padding是指父容器中已占用的空间大小,因此子元素可以的大小为父容器的尺寸减去padding:
int specSize = MeasureSpec.getSize(spec); int size = Math.max(0, specSize - padding);
普通View的MeasureSpec的创建规则:
EXACTLY AT_MOST UNSPECIFIED dp/px EXACTLY(childSize) EXACTLY(childSize) EXACTLY(childSize) match_parent EXACTLY(parentSize) AT_MOST(parentSize) UNSPECIFIED(0) wrap_parent AT_MOST(parentSize) AT_MOST(parentSize) UNSPECIFIED(0) 注:1.横排为childparentSpecmode,纵排为LayoutParams;2.parentSize指父容器中目前可使用的大小。
只要提供了父容器的MeasureSpec和子元素的LayoutParams,就可以快速确定出子元素的MeasureSpec。有了MeasureSpec就可以进一步确定出子元素测量后的大小。
1.3 View的工作流程
1.3.1 measure过程
measure过程要分两种情况:
- 原始的View:通过measure方法完成测量过程。
- ViewGroup:完成自己的测量过程外,遍历去调用所有子元素的measure方法。
View的measure过程:
- View的measure过程由其measure方法完成。measure方法是fianl类型,在其方法内会调用View的onMeasure方法。onMeasure的方法实现:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(),heightMeasureSpec)); }
setMeasuredDimension方法会设置View宽/高的测量值。再看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; }
可见getDefaultSize返回的大小是measureSpec中的specSize,这个specSize就是View测量后的大小。对于UNSPECIFIED情况,View的大小为getDefaultSize第一个参数size,即宽/高为getSuggestedMinimumWidth和getSuggestedMinimumHeight方法的返回值:
protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); } protected int getSuggestedMinimumHeight() { return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight()); }
从getSuggestedMinimumWidth代码可见,如果View没有设置背景,View的宽度为mMinWidth,mMinWidth对应
android:minWidth
属性值,如果该属性值不指定,mMinWidth默认0;如果View设置了背景,View的宽度为max(mMinWidth, mBackground.getMinimumWidth())。再看Drawable的getMinimumWidth方法:public int getMinimumWidth() { final int intrinsicWidth = getIntrinsicWidth(); return intrinsicWidth > 0 ? intrinsicWidth : 0; }
getMinimumWidth返回的就是Drawable的原始宽度(前提是这个Drawable有原始宽度),否则就返回0。ShapeDrawable无原始宽/高,而BitmapDrawable有原始宽/高(图片的尺寸)。
从getDefaultSize方法的实现看,在非UNSPECIFIED情况下,View的宽/高由SpecSize决定。
直接继承View的自定义控件需要重写onMeasure方法,且要设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。这是因为View在布局中使用wrap_content,那它的SpecMode是AT_MOST模式,它的宽/高等于SpecSize,根据之前的普通View的MeasureSpec的创建规则可知,这种情况下View的宽/高就是父容器当前剩余的空间大小,与match_parent表现一致。解决方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecsize = MeasureSpec.getsize(heightMeasureSpec); if(widthSpecMode==MeasureSpec.AT_MOST && heightSpecMode==MeasureSpec.AT_MOST){ setMeasuredDimension(mWidth, mHeight); }else if(widthSpecMode == MeasureSpec. AT_MOST){ setMeasuredDimension(mWidth, heightSpecSize); }else if(heightSpecMode == MeasureSpec.AT MOST) { setMeasuredDimension(widthSpecsize, mHeight); } }
只需要给View指定一个默认的内部宽/高(mWidth和mHeight),并且在wrap_content时设置此宽/高即可。
- 对于非wrap_content情况,我们沿用系统的测量值即可。
ViewGroup的measure过程:
ViewGroup除了完成自己的measure过程以外,还会遍历调用子元素的measure方法,各个子元素再递归去执行这个过程。
与View不同的是,ViewGroup是抽象类,没有重写View的onMeasure方法,而是提供一个叫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); }
measureChild方法做的就是抽出子元素的LayoutParams,然后通过getChildMeasureSpec创建子元素的MeasureSpec,接着将MeasureSpec传递给View的measure方法进行测量。
ViewGroup是抽象类,其测量过程的onMeasure交由各子类去实现,比如LinearLayout、RelativeLayout等。之所以ViewGroup不像View一样统一onMeasure方法,是因为ViewGroup的子类的布局特性差异过大。
注意:在极端情况下,系统可能需要多次measure才能确认最终的测量宽/高,在onMeasure中获取测量宽/高可能不准确。因此,在onLayout方法中取获取View的测量宽/高或者最终宽/高。
如果我们想在Activity启动时获取某个View的宽/高,实际上在onCreat、onStart、onResume中均无法正确获取(未完成测量时,获取为0)。这是因为View的measure过程和Activity的生命周期方法不是同步执行的。解决方法有四个:
1.onWindowFocusChanged:View已经初始化完成,宽/高已经准备完成。注意该方法会被多次调用,当Activity的窗口得到焦点和失去焦点时均会被调用。频繁的进行onResume和onPause,也会被调用。
@Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if(hasFocus){ int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } }
2.view.post(runnable):通过post将一个runnable投递到消息队列尾部,然后等待Looper调用此runnable的时候,View已经初始化好了。
protected void onStart() { super.onStart(); view.post(new Runnable() { @Override public void run() { int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } }); }
3.ViewTreeObserver:ViewTreeObserver的众多回调可以完成这个功能,比如OnGlobalLayoutListener接口,当View树的状态放生改变或者View树内部的View的可见性发生改变时,OnGlobalLayoutListener方法会被调用。
protected void onStart() { super.onStart(); ViewTreeObserver observer = view.getViewTreeObserver(); observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { view.getViewTreeObserver().removeOnDrawListener(this); int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } }); }
4.view.measure(int widthMeasureSpec,int heightMeasureSpec):通过手动对View进行measure来得到View的宽高。比较复杂。
手动对View进行measure来得到View的宽高,要根据View的LayoutParams区分:
match_parent:无法测量。构造此种MeasureSpec需要知道parentSize,无法获取。
具体数值(dp/px):宽高都是100px情况举例:
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)
注意:
(1<<30)-1
,View尺寸使用30位二进制表示,最大是30个1,2^30-1,即(1<<30)-1
。
1.3.2 layout过程
当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用其layout方法,在layout方法中,onLayout方法又会被调用。layout方法确定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); if (shouldDrawRoundScrollbar()) { if(mRoundScrollbarRenderer == null) { mRoundScrollbarRenderer = new RoundScrollbarRenderer(this); } } else { mRoundScrollbarRenderer = null; } 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); } } } final boolean wasLayoutValid = isLayoutValid(); mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; if (!wasLayoutValid && isFocused()) { mPrivateFlags &= ~PFLAG_WANTS_FOCUS; if (canTakeFocus()) { // We have a robust focus, so parents should no longer be wanting focus. clearParentsWantFocus(); } else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) { // This is a weird case. Most-likely the user, rather than ViewRootImpl, called // layout. In this case, there's no guarantee that parent layouts will be evaluated // and thus the safest action is to clear focus here. clearFocusInternal(null, /* propagate */ true, /* refocus */ false); clearParentsWantFocus(); } else if (!hasParentWantsFocus()) { // original requestFocus was likely on this view directly, so just clear focus clearFocusInternal(null, /* propagate */ true, /* refocus */ false); } // otherwise, we let parents handle re-assigning focus during their layout passes. } else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) { mPrivateFlags &= ~PFLAG_WANTS_FOCUS; View focused = findFocus(); if (focused != null) { // Try to restore focus as close as possible to our starting focus. if (!restoreDefaultFocus() && !hasParentWantsFocus()) { // Give up and clear focus once we've reached the top-most parent which wants // focus. focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false); } } } if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) { mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT; notifyEnterOrExitForAutoFillIfNeeded(true); } }
首先会通过setFrame方法来设定View的四个顶点的位置,即初始化mLeft、mRight、mTop和mBottom这四个值。View 的四个顶点确定后,View在父容器中的位置也就确定了;接着会调用onLayout方法,这个方法的用途是父容器确定子元素的位置,onLayout 的具体实现同样和具体的布局有关,所以View和ViewGroup均没有真正实现onLayout方法。
View的getMeasureWidth(getMeasureHeight)和getWidth(getHeight)的区别:
public final int getWidth() { return mRight - mLeft; } public final int getHeight() { return mBottom - mTop; }
在View的默认实现中, View的测量宽/高和最终宽/高是相等的,只不过测量宽/高形成于View的measure过程,而最终宽/高形成于View的layout过程。
特殊情况1:(重写layout)
public void layout(int l,int t,int ,int b){ super.layout(l,t,r + 100,b + 100); }
上述代码会导致在View的最终宽/高总是比测量宽/高大100px,但没有实际意义。
特殊情况2:(多次measure)
过程中可能View的最终宽/高和测量宽/高不一致,但是最终会一致。
1.3.3 draw过程
draw的作用是将View绘制到屏幕上。
过程:
- 1.绘制背景:background.draw(canvas)
- 2.绘制自己:onDraw
- 3.绘制children :dispatchDraw
- 4.绘制装饰:onDrawScrollBars
View绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw 会遍历调用所有子元素的draw方法,重复直至绘制完成。
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; } ...
View 有一个特殊的方法setWillNotDraw:
public void setWillNotDraw(boolean willNotDraw) { setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK); }
如果一个View不需要绘制任何内容,那么设置这个标记位为true以后,系统会进行相应的优化。默认View 没有启用这个优化标记位,但是ViewGroup会默认启用这个优化标记位。注意:VicwGroup需要通过onDraw来绘制内容时,我们需要显式地关闭WILL_NOT_DRAW这个标记位。
1.4 自定义View
1.4.1 自定义View的分类
1.继承View重写onDraw方法:
主要用于实现一些不规则的效果。预期效果不方便通过布局的组合方式来达到,往往需要静态或者动态地显示一些不规则的图形。采用这种方式需要自己支持wrap_content,padding也需要自己处理。
2.继承ViewGroup派生特殊的Layout:
主要用于实现自定义布局。需要合适地处理ViewGroup的测量、布局这两个过程;同时处理子元素的测量和布局过程。
3.继承特定的View(比如TextView):
一般用于扩展某种已有的Viewdev功能。不需要自己支持wrap_content和padding 等。
4.继承特定的ViewGroup:
不需要自己处理ViewGroup的测量和布局这两个过程。与2的区别:方法2更接近View的底层。
1.4.2 自定义View须知
让View支持wrap_content:
直接继承View或者ViewGroup的控件,如果不在onMeasure中对wrap_content做特殊处理,那么当外界布局中使用wrap_content时就无法达到预期效果。
让View支持padding:
直接继承View的控件,如果不在draw方法中处理padding,那么padding属性是无法起作用的。直接继承ViewGroup的控件要在onMeasure和onLayout中考虑padding和子元素的margin对其造成的影响,不然将导致padding和子元素的margin失效。
不要在View中使用Handler:
View内部本身就提供了post系列的方法,可以替代handler的作用。
View中如果有线程或者动画,需要及时停止:
onDetachedFromWindow是停止线程或者动画的好时机。当包含此View的Activity退出或者当前View被remove时,View 的onDetachedFromWindow方法会被调用;当包含View的Activity启动时,View 的onAttachedToWindow方法会被调用。View变得不可见时我们也需要停止线程和动画,如果不及时处理这种问题,有可能会造成内存泄漏。
View带有滑动嵌套情形时,需要处理好滑动冲突:
如果有滑动冲突,要合适地处理滑动冲突,否则将会严重影响View的效果。
1.4.3 添加自定义属性
1.在values目录下面创建自定义属性的XML,比如atrs.xml。
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="CircleView"> <attr name="circle_color" format="color"/> </declare-styleable> </resources>
除了color,还可以指定reference(资源id)、dimension(尺寸)、string、interger、boolean等。
2.在View的构造函数中解析自定义属性的值并处理。
public CircleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); //预加载自定义属性集合CircleView TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.CircleView); //解析CircleView属性集合中的circle_color属性,并设置默认颜色值 mColor=typedArray.getColor(R.styleable.CircleView_circle_color,Color.RED); //实现资源 typedArray.recycle(); init(); }
3.在布局文件中使用自定义属性。
<com.virtual.testview.CircleView android:id="@+id/circleView1" android:layout_width="wrap_content" android:layout_height="100dp" app:circle_color="#F00A0A" android:background="#000000"/>
注意:使用自定义属性,必须在布局文件中添加schemas声明:
xmlns:app="http://schemas.android.com/apk/res-auto"
。app是自定义前缀,可以更换其他名字
1.5 参考资料
- Android开发艺术探索