Android自定义View系列
View的三大流程指的是measure(测量)、layout(布局)、draw(绘制)。
下面我们来分别看看这三大流程
View的measure(测量)
MeasureSpec
MeasureSpec是View的一个内部静态类
//view.classpublic 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; /** * 精确模式,对应的是match_parent和具体值,比如100dp public static final int EXACTLY = 1 << MODE_SHIFT; /** * 最大模式,对应的就是wrap_content */ 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); } } /** * 获取测量的模式 */ @MeasureSpecMode public static int getMode(int measureSpec) { //noinspection ResourceType return (measureSpec & MODE_MASK); } /** * 获取测量到的尺寸大小 */ public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } ...}复制代码
MeasureSpec总结起来就是:
- 它由2部分数据组成,分别为定义了View测量的模式和View的测量尺寸大小
- 其中EXACTLY精确模式表示的是match_parent和具体值;AT_MOST最大模式表示的是wrap_content的情况
View的measure过程
View的measure过程由其measure方法完成,在measure方法中会调用View的onMeasure方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}复制代码
setMeasuredDimension方法会设置View的测量宽高,所以我们知道getDefaultSize方法返回的就是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; //对应的是wrap_content case MeasureSpec.AT_MOST: //对应的是match_parent和具体值,返回的是测量值 case MeasureSpec.EXACTLY: result = specSize; break; } return result;}复制代码
从getDefaultSize方法我们可以看到,无论是测量模式无论是AT_MOST还是EXACTLY,返回的结果都是specSize这个测量后的大小。当View的测量模式是AT_MOST,也就是我们在布局中给View设置的是wrap_content时,这个specSize实际上是父容器中的可用大小,也就相当于是和match_parent是一样的效果了。所以我们在通过继承View来自定义View时,就需要特别处理wrap_content的情况。
ViewGroup的measure过程
对于ViewGroup来说,除了完成自己的测量,还需要完成子元素的测量。ViewGroup是一个抽象类,为了测量子类,它提供了一个measureChildren方法:
//ViewGroup.classprotected 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(); //用子元素的LayoutParams构建MeasureSpec final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec);}复制代码
可以看出ViewGroup的measureChildren方法最终会循环调用子元素的measure方法完成子元素的测量。
ViewGroup并没有定义自己的测量过程,因为它的测量过程要由子类自己完成,比如LinearLayout和RelativeLayout,显然测量过程是不同的。有兴趣的可以看看LinearLayout的onMearsure方法。
常见的在Activity中获取View的宽高的方法
View的measure过程和Activity生命周期方法是不同步的,需要用特殊的方法才能准确获取View的宽高
(1)onWindowFocusChanged方法中获取
@Overridepublic void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus) { int width = myView.getMeasuredWidth(); int height = myView.getMeasuredHeight(); }}复制代码
需要注意的是onWindowFocusChanged方法会被调用多次
(2)view.post(runnable)
通过view的post方法可以将一个runnable投递到消息队列的尾部,当Looper调用此runnable时,View已经初始化好了
myView.post(new Runnable() { @Override public void run() { int width = myView.getMeasuredWidth(); int height = myView.getMeasuredHeight(); }});复制代码
(3)ViewTreeObserver
利用ViewTreeObserver的OnGlobalLayoutListener回调接口,当View树发生状态改变时会回调这个接口
ViewTreeObserver viewTreeObserver = myView.getViewTreeObserver();viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { myView.getViewTreeObserver().removeOnGlobalLayoutListener(this); int width = myView.getMeasuredWidth(); int height = myView.getMeasuredHeight(); }});复制代码
View的layout(布局)
layout的作用就是ViewGroup用来确定子元素的位置,ViewGroup的位置被确定后,就会调用onLayout方法,遍历所有的子元素并调用其layout方法,在layout方法中又会调用onLayout方法。layout方法确定View本身的位置,而onLayout方法用来确定子元素的位置。
//View.classpublic 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; //确定View的四个顶点的位置 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); ... } ...}protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}复制代码
layout方法主要就做了2件事,一个是调用setFrame方法确定自身的位置,另一个就是调用onLayout方法确定子元素的位置。
我们看到在View中并没有实现onLayout方法,同样的在ViewGroup中也没有实现onLayout方法,这是因为onLayout的具体实现同样和具体的布局有关,所以需要子类根据具体情况去实现。大家有兴趣可以看看LinearLayout的onLayout的实现。
需要注意的是默认情况下测量的宽高和最终的宽高是一样的,也就是getMeasuredWidth和getWidth是一样的。只不过一个获取的是measure过程后得到的宽高,一个是layout过程后的宽高。所以如果measure过程需要进行多次或是认为改变了layout方法,就有可能2者不相等。不过绝大多数都是一样的。
View的draw(绘制)
Draw说白了就是把View的内容绘制到屏幕上
@CallSuperpublic 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); } ... if (!verticalEdges && !horizontalEdges) { // Step 3, draw the content //调用onDraw方法绘制内容 if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children //调用dispatchDraw方法绘制子元素 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; } ...}复制代码
从上面的draw方法中,我们可以看出,绘制过程遵循如下几步:
(1)绘制背景:drawBackground(Canvas canvas)
(2)绘制自身内容:onDraw(canvas)
(3)绘制子元素:dispatchDraw(canvas)
(4) 绘制装饰:onDrawForeground(canvas)
View的三大流程开始的地方---ViewRootImpl
上面这张图是一张很经典的图,很好的描述了View的绘制流程。ViewRootImpl中的performTraversals方法会调用performMeasure、performLayout、performDraw方法,开始View的测量、布局和绘制过程。那ViewRootImpl中的performTraversals方法又是在什么时候被调用的呢?这就需要理解一个窗口的概念,也就是Window。
Android中的Window
Window是一个抽象的概念,每一个Window都对应着一个View和ViewRootImpl,Window通过ViewRootImpl来和View建立联系。Android中所有的视图都是通过都是通过Window来呈现的,不管是Activity、Dialog、还是Toast,它们的View都是附加在Window上的,因此Window实际上是View的直接管理者。比如我们触摸屏幕的事件,就是通过Window传递给DecorView,然后再由DecorView传递给我们的View。我们在Activity、Dialog中设置视图内容的方法setContentView在底层也是通过Window来完成的
Window的添加过程
我们在启动一个Activity或是一个Dialog时,系统都会为我们创建一个Window,并把创建的Window注册到系统的WindowManagerService中。
Window的添加过程需要通过WindowManager的实现类WindowManagerImpl的addView方法来实现。只有在通过addView方法将View添加到Window中后,我们的View才和Window关联起来,才能接收通过Window传递的各种输入信息
//WindowManagerImpl.classprivate final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();@Overridepublic void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) { applyDefaultToken(params); mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);}复制代码
由WindowManagerImpl的源码我们发现,WindowManagerImpl把添加View的工作都交给了WindowManagerGlobal类处理。我们来简单看看WindowManagerGlobal类
//WindowManagerGlobal.class//所有Window所对应的Viewprivate final ArrayListmViews = new ArrayList ();//所有Window对应的ViewRootImplprivate final ArrayList mRoots = new ArrayList ();//多有Window对应的布局参数private final ArrayList mParams = new ArrayList ();//正在被删除的Viewprivate final ArraySet mDyingViews = new ArraySet ();public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { ... //创建ViewRootImpl root = new ViewRootImpl(view.getContext(), display); view.setLayoutParams(wparams); //将Window的一系列对象添加到对应的列表中 mViews.add(view); mRoots.add(root); mParams.add(wparams); // do this last because it fires off messages to start doing things try { //调用ViewRootImpl的setView方法 root.setView(view, wparams, panelParentView); } catch (RuntimeException e) { // BadTokenException or InvalidDisplayException, clean up. if (index >= 0) { removeViewLocked(index, true); } throw e; }}复制代码
源码中已经做了相应的注释了。这里我们看到WindowManagerGlobal在addView方法中创建ViewRootImpl后,最后调用了ViewRootImpl的setView方法。下面我们来看看ViewRootImpl的setView方法
//ViewRootImpl.classpublic void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { synchronized (this) { if (mView == null) { ... //调用了requestLayout进行View的绘制 requestLayout(); ... try { mOrigWindowType = mWindowAttributes.type; mAttachInfo.mRecomputeGlobalAttributes = true; collectViewAttributes(); //这里调用WindowSession的addToDisplay方法注册Window res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mAttachInfo.mOutsets, mInputChannel); } catch (RemoteException e) { mAdded = false; mView = null; mAttachInfo.mRootView = null; mInputChannel = null; mFallbackEventHandler.setView(null); unscheduleTraversals(); setAccessibilityFocus(null, null); throw new RuntimeException("Adding window failed", e); } finally { if (restore) { attrs.restore(); } } ... } }}@Overridepublic void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true; scheduleTraversals(); }}复制代码
在ViewRootImpl的setView方法中主要是做了2件事,一个是调用requestLayout方法启动View的绘制流程;另一个是调用WindowSession的addToDisplay方法请求WindowManagerService添加Window,这是一次IPC调用。
至此,我们就分析完Window的添加过程了,总结如下:
(1)View的展示以及处理触摸点击事件离不开Window,2者通过ViewRootImpl进行关联
(2)我们在启动Activity、创建Dialog或是弹出Toast时都会创建一个Window,然后会通过WindowManagerGlobal类的addView方法来创建ViewRootImpl类,并将Window、View、ViewRootImpl关联起来,
(3)在创建完ViewRootImpl后,接着会调用ViewRootImpl的setView方法,在setView方法中通过requestLayout方法最终调用到performTraversals方法开启View的三大流程;通过WindowSession的addToDisplay方法向WindowManagerService发起远程IPC调用,完成Window的添加。
总结
(1)在通过继承View的方式自定义View时,需要特别处理wrap_content的情况,因为View中默认相当于没处理(和match_parent效果一样)
(2)在Activity中获取View的宽高需要用特殊的方式:onWindowFocusChanged、view.post(runnable)、ViewTreeObserver的OnGlobalLayoutListener
(3)我们的View的显示离不来Window,无论是Activity、Dialog还是Toast,都对应着一个Window。View和Window通过ViewRootImpl来建立关联。我们显示、更新、隐藏界面,比如Dialog的show和dismiss,说到底是Window中添加、更新和删除View的过程。
(4)我们通过setContentView方法添加View,其实是对应Window的添加View的过程,Window会创建ViewRootImpl来执行注册Window、开启View的绘制流程的操作。
(5)所以综上,我们显示一个界面的过程为:创建Window-->创建ViewRootImpl-->添加View-->绘制View、注册Window
欢迎关注我的微信公众号,和我一起每天进步一点点!复制代码