Activtiy完全解析(三、View的显示过程measure、layout、draw)

2024-05-07 19:38

本文主要是介绍Activtiy完全解析(三、View的显示过程measure、layout、draw),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

版权声明:本文为openXu原创文章【openXu的博客】,未经博主允许不得以任何形式转载

  在Activity完全解析的第一篇文章 Activtiy完全解析(一、Activity的创建过程)中,我们分析了从调用startActivtiy()Activtiy创建完成的整个过程。其中**step20:ActivtiyThread.handleLaunchActivity(r, null)**这一步中有两个重要的步骤,第一步就是调用performLaunchActivtiy(r, customIntent),这个方法中首先创建了需要打开的activity对象,然后调用其activtiy.attach()方法完成activtiy对象的初始化,最后调用其onCreate()方法,这时候activity的声明周期就开始了。而在上一篇文章 Activtiy完全解析(二、layout的inflate过程)中,我们分析了在onCreate()方法中,从调用setContentView()到layout布局树加载完毕的过程,到此为止,Activtiy并不会展示到用户眼前,因为layout只是被填充成一个框架了,就像修房子,首先得画出设计图纸,布局树就像是房子的设计图,设计图画好了还得按照设计图纸一砖一瓦的盖房,所有有了布局树,还需要为上面的每一个控件测量大小,安排摆放的位置并画到屏幕上。本文我们一起分析Activity窗口的测量、布局和绘制的过程。

  在讲解之前,我们先通过关键源码了解一些重要的类之间的关系:ActivityWindowPhoneWindowViewManagerWindowManagerWindowManagerImplWindowManagerGlobalWindowmanagerService

/**Window系列*/
public abstract class Window {private WindowManager mWindowManager;public void setWindowManager(android.view.WindowManager wm, IBinder appToken, String appName,boolean hardwareAccelerated) {...//调用WindowManagerImpl的静态方法new一个WindowManagerImpl对象mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);}public android.view.WindowManager getWindowManager() {return mWindowManager;}
}
public class PhoneWindow extends Window implements MenuBuilder.Callback {private DecorView mDecor;
}/**WindowManager系列,用于管理窗口视图的显示、添加、移除和状态更新等*/
public interface ViewManager{public void addView(View view, ViewGroup.LayoutParams params);public void updateViewLayout(View view, ViewGroup.LayoutParams params);public void removeView(View view);
}
public interface WindowManager extends ViewManager {...
}
public final class WindowManagerImpl implements WindowManager {//每个WindowManagerImpl对象都保存了一个WindowManagerGlobal的单例private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();...public WindowManagerImpl createLocalWindowManager(android.view.Window parentWindow) {return new WindowManagerImpl(mDisplay, parentWindow);}@Overridepublic void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {applyDefaultToken(params);//调用WindowManagerGlobal对象的addView()方法mGlobal.addView(view, params, mDisplay, mParentWindow);}...
}
public final class WindowManagerGlobal {private static WindowManagerGlobal sDefaultWindowManager;private static IWindowManager sWindowManagerService;private static IWindowSession sWindowSession;//单例private WindowManagerGlobal() {}public static WindowManagerGlobal getInstance() {synchronized (WindowManagerGlobal.class) {if (sDefaultWindowManager == null) {sDefaultWindowManager = new WindowManagerGlobal();}return sDefaultWindowManager;}}/**WindowManagerGlobal类被加载时,就获取到WindowManagerService对象*/public static void initialize() {getWindowManagerService();}public static IWindowManager getWindowManagerService() {//线程安全的synchronized (WindowManagerGlobal.class) {if (sWindowManagerService == null) {sWindowManagerService = IWindowManager.Stub.asInterface(ServiceManager.getService("window"));}return sWindowManagerService;}}public static IWindowSession getWindowSession() {//线程安全的synchronized (WindowManagerGlobal.class) {if (sWindowSession == null) {try {InputMethodManager imm = InputMethodManager.getInstance();IWindowManager windowManager = getWindowManagerService();sWindowSession = windowManager.openSession(new IWindowSessionCallback.Stub() {@Overridepublic void onAnimatorScaleChanged(float scale) {ValueAnimator.setDurationScale(scale);}},imm.getClient(), imm.getInputContext());} catch (RemoteException e) {Log.e(TAG, "Failed to open window session", e);}}return sWindowSession;}}
}
/**Activity*/
public class Activity ...{private Window mWindow;              //mWindow是PhoneWindow类型private WindowManager mWindowManager;//mWindowManager是WindowManagerImpl类型/*** Activity被创建后,执行attach()初始化的时候,就会创建PhoneWindow对象和WindowManagerImpl对象*/final void attach(...) {...//①.为activity创建一个PhoneWindow对象mWindow = new PhoneWindow(this);mWindow.setWindowManager((WindowManager)context.getSystemService(Context.WINDOW_SERVICE),mToken, mComponent.flattenToString(),(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);if (mParent != null) {mWindow.setContainer(mParent.getWindow());}//②.调用Window的getWindowManager()方法初始化mWindowManager,其实就是new了一个WindowManagerImpl对象mWindowManager = mWindow.getWindowManager();}
}

PhoneWindow:在上一篇文章中就讲过,这其实理解起来也比较容易,我们知道activity就是用于展示界面,控制用户交互的,所以activity中一定维护了用于描述界面的对象,还有就是控制窗口显示的管理器对象,实际上这两个对象都是由PhoneWindow维护的,而每个activity都拥有一个PhoneWindow对象mWindow

WindowManager:用于管理窗口的一些状态、属性以及窗口的添加、删除、更新、顺序等,WindowManagerViewManager的子接口,ViewManager接口中只有三个抽象方法(addViewupdateViewLayoutremoveView),可显而知,这个管理器的作用就是控制窗口View的添加、删除以及状态更新的。WindowManager也是一个接口,他并没有实现ViewManager中的抽象方法,其实真正的实现类是WindowManagerImpl。在activity被创建后调用attach()方法初始化的时候,首先创建了PhoneWindow对象mWindow,然后调用mWindowsetWindowManager((WindowManager)context.getSystemService(Context.WINDOW_SERVICE) ...)为其mWindowManager赋值。

WindowManagerGlobal:由于WindowManagerGlobal的关键代码比较长,稍后讲解的时候再贴代码。WindowManagerGlobal是单例模式,Activity在创建完成调用attach()方法初始化时,会实例化mWindowManagerWindowManager的引用指向WindowManagerImpl 的对象),WindowManagerImpl创建之后,就维护了WindowManagerGlobal的单例对象,WindowManagerGlobal中有一个IWindowSession类型的变量sWindowSession,他就是用来和远程服务WindowManagerService通信的代理。

WindowManagerService:跟之前讲解Activity启动过程中提到的ActivityManagerService(用于管理Android组件的启动、关闭和状态信息等)一样,WMS也是一个复杂的系统服务。
  在了解WMS之前,首先要了解窗口(Window)是什么,Android系统中的窗口是只屏幕上一块用于绘制UI元素并能相应用户交互的矩形区域,从原理上讲,窗口是独自占有一个Surface实例的显示区域,例如DialogActivity界面、比值、状态栏、Toast等都是窗口。Surface就是一块画布,应用可以通过Cancas或者OpenGL在其上面作画,然后通过urfaceFlinger将多块Surface的内容按照特定的顺序进行混合并输出到FrameBuffer,从而将应用界面显示给用户。
  既然每个窗口都有一块Surface供自己涂鸦,必然需要一个角色对所有窗口的Surface进行协调管理,WMS就是用来做这个事情的,WMS掌管Surface的显示顺序(Z-order)以及位置尺寸,控制窗口动画,并且还是输入系统的重要中转站。

通过上面的分析,我们知道这几个关键类的大概作用,以及他们之间的嵌套关系,接下来,我们就开始分析Activity的绘制过程:

step1:ActivityThread.handleResumeActivity()

首先接着第一篇文章step20handleResumeActivity()方法分析:

//获取activity的顶层视图decor
View decor = r.window.getDecorView();
//让decor显示出来
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (a.mVisibleFromClient) {a.mWindowAdded = true;//将decor放入WindowManager(WindowManagerImpl对象)中,这样activity就显示出来了wm.addView(decor, l);

wm.addView(decor, l)实际上就是调用WindowManagerImpladdView()方法将Activity的根窗口mDecor添加到窗口管理器中的,而WindowManagerImpladdView()方法又调用的是WindowManagerGlobal单例的addView()方法。接下来我们就分析WindowManagerGlobal.addView()

step2:WindowManagerGlobal.addView()

public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {...ViewRootImpl root;View panelParentView = null;synchronized (mLock) {...int index = findViewLocked(view, false);if (index >= 0) {if (mDyingViews.contains(view)) {// Don't wait for MSG_DIE to make it's way through root's queue.mRoots.get(index).doDie();} else {throw new IllegalStateException("View " + view+ " has already been added to the window manager.");}// The previous removeView() had not completed executing. Now it has.}...//创建一个视图层次结构的顶部,ViewRootImpl实现所需视图和窗口之间的协议。root = new ViewRootImpl(view.getContext(), display);view.setLayoutParams(wparams);mViews.add(view);mRoots.add(root);mParams.add(wparams);}try {//将activity的根窗口mDecor添加到root中root.setView(view, wparams, panelParentView);} catch (RuntimeException e) {...throw e;}}
}

在这个方法中,首先创建了一个ViewRootImpl的对象root,然后调用root.setView(),将activity的根窗口视图设置给root,下面我们看看ViewRootImpl类:

step3:ViewRootImpl.setView()

public final class ViewRootImpl implements ViewParent,View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {final IWindowSession mWindowSession;public ViewRootImpl(Context context, Display display) {mWindowSession = WindowManagerGlobal.getWindowSession();...}public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {synchronized (this) {if (mView == null) {mView = view;...int res; /* = WindowManagerImpl.ADD_OKAY; *///请求对应用程序窗口视图的UI作第一次布局,应用程序窗口的绘图表面的创建过程requestLayout();...try {...res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,getHostVisibility(), mDisplay.getDisplayId(),mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,mAttachInfo.mOutsets, mInputChannel);} catch (RemoteException e) {...throw new RuntimeException("Adding window failed", e);}...}}@Overridepublic void requestLayout () {if (!mHandlingLayoutInLayoutRequest) {//检查当前线程是否是主线程,对视图的操作只能在UI线程中进行,否则抛异常checkThread();//应用程序进程的UI线程正在被请求执行一个UI布局操作mLayoutRequested = true;scheduleTraversals();}}void checkThread() {if (mThread != Thread.currentThread()) {throw new CalledFromWrongThreadException("Only the original thread that created a view hierarchy can touch its views.");}}void scheduleTraversals() {if (!mTraversalScheduled) {mTraversalScheduled = true;mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();//Choreographer类型,用于发送消息mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);...}}final TraversalRunnable mTraversalRunnable = new TraversalRunnable();final class TraversalRunnable implements Runnable {@Overridepublic void run() {doTraversal();}}void doTraversal() {if (mTraversalScheduled) {mTraversalScheduled = false;mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);if (mProfile) {Debug.startMethodTracing("ViewAncestor");}performTraversals();if (mProfile) {Debug.stopMethodTracing();mProfile = false;}}}
}

  ViewRootImpl实现了ViewParent,它并不是真正的View(没有继承View),它定义了顶层视图的职责API,比如requestLayout()请求布局等等,在setView()方法中,调用了requestLayout()方法,请求对Activity根窗口做第一次布局。
  requestLayout()方法中首先调用checkThread()检查当前线程是否为UI线程,如果不是则抛出异常,然后调用scheduleTraversals()方法,scheduleTraversals()方法中调用了mChoreographer.postCallback()mChoreographer中维护了一个Handler,通过handler机制实现消息调度,并传入一个回调mTracersalRunnablemTracersalRunnable的run方法中调用了doTraversal()方法,doTraversal()方法继续调用performTraversals():

step4:ViewRootImpl.performTraversals()

private void performTraversals() {final View host = mView;   //mView就是Activity的根窗口mDecor...//下面代码主要确定Activity窗口的大小boolean windowSizeMayChange = false;//Activity窗口的宽度desiredWindowWidth和高度desiredWindowHeightint desiredWindowWidth;int desiredWindowHeight;...//Activity窗口当前的宽度和高度是保存ViewRoot类的成员变量mWinFrame中的Rect frame = mWinFrame;.../** 接下来的两段代码都是在满足下面的条件之一的情况下执行的:* 1. Activity窗口是第一次执行测量、布局和绘制操作,即ViewRoot类的成员变量mFirst的值等于true* 2. 前面得到的变量windowShouldResize的值等于true,即Activity窗口的大小的确是发生了变化。* 3. 前面得到的变量insetsChanged的值等于true,即Activity窗口的内容区域边衬发生了变化。* 4. Activity窗口的可见性发生了变化,即变量viewVisibilityChanged的值等于true。* 5. Activity窗口的属性发生了变化,即变量params指向了一个WindowManager.LayoutParams对象。*/if (mFirst || windowShouldResize || insetsChanged ||viewVisibilityChanged || params != null) {...try {//★请求WindowManagerService服务计算Activity窗口的大小以及内容区域边衬大小和可见区域边衬大小relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);...} catch (RemoteException e) {}...//将计算得到的Activity窗口的宽度和高度保存在ViewRoot类的成员变量mWidth和mHeight中if (mWidth != frame.width() || mHeight != frame.height()) {mWidth = frame.width();mHeight = frame.height();}...if (!mStopped || mReportNextDraw) {boolean focusChangedDueToTouchMode = ensureTouchModeLocally((relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()|| mHeight != host.getMeasuredHeight() || contentInsetsChanged) {int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);// 一、测量控件大小(根窗口和其子控件树)performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);int width = host.getMeasuredWidth();int height = host.getMeasuredHeight();boolean measureAgain = false;if (lp.horizontalWeight > 0.0f) {width += (int) ((mWidth - width) * lp.horizontalWeight);childWidthMeasureSpec = View.MeasureSpec.makeMeasureSpec(width,View.MeasureSpec.EXACTLY);measureAgain = true;}if (lp.verticalWeight > 0.0f) {height += (int) ((mHeight - height) * lp.verticalWeight);childHeightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height,View.MeasureSpec.EXACTLY);measureAgain = true;}if (measureAgain) {// 一、测量控件大小(根窗口和其子控件树)performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);}layoutRequested = true;}}}else{...}final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);boolean triggerGlobalLayoutListener = didLayout|| mAttachInfo.mRecomputeGlobalAttributes;if (didLayout) {//二、布局过程performLayout(lp, desiredWindowWidth, desiredWindowHeight);...}...boolean skipDraw = false;boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() ||viewVisibility != View.VISIBLE;if (!cancelDraw && !newSurface) {if (!skipDraw || mReportNextDraw) {...//三、绘制过程performDraw();}} else {if (viewVisibility == View.VISIBLE) {// Try againscheduleTraversals();} else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {...}}mIsInTraversal = false;
}

  performTraversals()方法中,分为四部分,上面很大一段代码主要作用是确定Activity窗口的大小,也就是通过各种判断,是否是第一次?是否需要重新计算大小?最终确定Activity根窗口的大小并保存起来。第二步就是遍历测量子控件的大小;第三步就是布局;第四部就是绘制。对于Activity窗口大小的计算,这里就不深入探讨,有兴趣可以看看。接下来我们重点分析后面三个步骤,也就是控件的测量、布局、绘制三个过程。

step5: ViewRootImpl.performMeasure()控件测量过程

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);}
}

  performMeasure()方法中调用了mView.measure()方法,mView就是Activity的根窗口,他是DecorView类型(FrameLayout的子类),这个类定义在PhoneWindow类中,接下来我们看看DecorViewmeasure()方法(measure()方法是View中final修饰的方法,不能覆盖):

#step6: View.measure()

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {...//如果View的mPrivateFlags的PFLAG_FORCE_LAYOUT位不等于0时,就表示当前视图正在请求执行一次布局操作;//或者当前的宽高约束条件不等于视图上一次的约束条件时,需要重新测量View的大小。if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||widthMeasureSpec != mOldWidthMeasureSpec ||heightMeasureSpec != mOldHeightMeasureSpec) {//首先清除所测量的维度标志mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;...int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :mMeasureCache.indexOfKey(key);//如果需要强制布局操作,或者忽略测量历史,将会调用onMeasure对控件进行一次测量//sIgnoreMeasureCache是一个boolean值,初始化为sIgnoreMeasureCache = targetSdkVersion < KITKAT;//意思是如果Android版本低于19,每次都会调用onMeasure(),而19以上时sIgnoreMeasureCache==falseif (cacheIndex < 0 || sIgnoreMeasureCache) {//调用onMeasure来真正执行测量宽度和高度的操作onMeasure(widthMeasureSpec, heightMeasureSpec);mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;} else {...}// flag not set, setMeasuredDimension() was not invoked, we raise// an exception to warn the developerif ((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()onMeasure()measure()方法是final的,不能被重写。measure()方法中调用了onMeasure()方法,onMeasure()方法在View中的默认实现是测量自身的大小,所以在我们自定义ViewGroup的时候,如果我们不重写onMeasure()测量子控件的大小,子控件将会显示不出来的。ViewGroup中没有重写onMeasure()方法,在ViewGroup的子类(LinearLayoutRelativeLayout等)中会根据容器自身的布局特性,比如LinearLayout有两种布局方式,水平或者垂直,分别对onMeasure()方法进行不同的重写。

  总结一下就是measure()方法不能被重写,它会调用onMeasure()方法测量控件自身的大小,如果控件是一个容器,它需要重写onMeasure()方法遍历测量所有子控件的大小。还有当我们继承View自定义控件时,在布局文件中明明使用了wrap_content可结果还是填充父窗体,这是因为ViewonMeasure()方法的默认实现,如果要按照我们的需求正确的测量控件大小也需要重写onMeasure()方法。

  由于View.measure()中调用了onMeasure()方法,所以就调用到了DecorViewonMeasure ()方法,DecorView.onMeasure()中首先对宽高约束做了一些处理后,接着调用了super.onMeasure(),也就是FrameLayout中的onMeasure()方法:

step7: FrameLayout.onMeasure()

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {//获得一个视图容器所包含的子视图的个数int count = getChildCount();final boolean measureMatchParentChildren =View.MeasureSpec.getMode(widthMeasureSpec) != View.MeasureSpec.EXACTLY ||View.MeasureSpec.getMode(heightMeasureSpec) != View.MeasureSpec.EXACTLY;//mMatchParentChildren用于缓存子控件中布局参数设置为填充父窗体的控件mMatchParentChildren.clear();int maxHeight = 0;int maxWidth = 0;int childState = 0;//①、遍历根窗口下所有的子控件,挨个测量大小for (int i = 0; i < count; i++) {final View child = getChildAt(i);if (mMeasureAllChildren || child.getVisibility() != GONE) {//调用measureChildWithMargins来测量每一个子视图的宽度和高度measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);final LayoutParams lp = (LayoutParams) child.getLayoutParams();//由于FrameLayout的布局特性(Z轴帧布局,向上覆盖),将子控件中,最大的宽高值记录下来maxWidth = Math.max(maxWidth,child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);maxHeight = Math.max(maxHeight,child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);childState = combineMeasuredStates(childState, child.getMeasuredState());if (measureMatchParentChildren) {//由于现在本容器的宽高都还没有确定下来,子控件设置为填充父窗体肯定没法计算,所以先缓存起来if (lp.width == LayoutParams.MATCH_PARENT ||lp.height == LayoutParams.MATCH_PARENT) {mMatchParentChildren.add(child);}}}}//②、设置FrameLayout的宽高//上面已经得到子控件中宽高的最大值,然后加上容器(FrameLayout)设置的padding值// Account for padding toomaxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();//判断是否设置有最小宽度和高度,如果有设置,需要和上面计算的值比较,选择较大的值作为容器宽高// Check against our minimum height and widthmaxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());//是否设置有前景图,如果有,需要考虑背景的宽高// Check against our foreground's minimum height and widthfinal Drawable drawable = getForeground();if (drawable != null) {maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());}//调用resolveSizeAndState()方法根据计算出的宽高值和宽高约束参数,计算出正确的宽高值;//然后调用setMeasuredDimension()方法设置当前容器的宽高值setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),resolveSizeAndState(maxHeight, heightMeasureSpec,childState << MEASURED_HEIGHT_STATE_SHIFT));//③、计算子控件中设置为填充父窗体的控件的大小count = mMatchParentChildren.size();if (count > 1) {for (int i = 0; i < count; i++) {final View child = mMatchParentChildren.get(i);final ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) child.getLayoutParams();final int childWidthMeasureSpec;if (lp.width == LayoutParams.MATCH_PARENT) {final int width = Math.max(0, getMeasuredWidth()- getPaddingLeftWithForeground() - getPaddingRightWithForeground()- lp.leftMargin - lp.rightMargin);childWidthMeasureSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);} else {childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,getPaddingLeftWithForeground() + getPaddingRightWithForeground() +lp.leftMargin + lp.rightMargin,lp.width);}final int childHeightMeasureSpec;if (lp.height == LayoutParams.MATCH_PARENT) {final int height = Math.max(0, getMeasuredHeight()- getPaddingTopWithForeground() - getPaddingBottomWithForeground()- lp.topMargin - lp.bottomMargin);childHeightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY);} else {childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,getPaddingTopWithForeground() + getPaddingBottomWithForeground() +lp.topMargin + lp.bottomMargin,lp.height);}child.measure(childWidthMeasureSpec, childHeightMeasureSpec);}}
}

代码中注释已经写的比较清楚了,简单描述一遍,FrameLayoutonMeasure方法可以分为三个步骤:
①. 遍历所有子控件,并调用measureChildWithMargin()方法测量子控件的大小,measureChildWithMargin() 方法是从ViewGroup中继承下来的,它会调用child.measure(),如果子控件又是一个容器就会继续遍历child的子控件测量,最后FrameLayout的所有子控件(包括子容器中的子控件…)都完成了测量;
②. 根据第①步中记录的子控件宽高的最大值,然后加上padding值以及最小宽高的比较,最终确定FrameLayout容器的宽高;
③. 重新计算子控件宽高设置为MATCH_PARENT(填充父窗体 )的宽高

  FrameLayoutonMeasure()方法执行完毕后,整个Activity窗体重所有的控件都完成了测量,这时调用view.getMeasuredWidth()/getMeasuredHeight()就可以获取到控件测量的宽高值了。测量的部分到此就结束了,如果想进一步深入了解控件测量的相关知识可以参考 Android自定义View(三、深入解析控件测量onMeasure)。下面我们接着step4中的第二步**performLayout()**分析窗口的布局过程。

step8:ViewRootImpl.performLayout()请求布局过程

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,int desiredWindowHeight) {mLayoutRequested = false;mScrollMayChange = true;mInLayout = true;final View host = mView;...Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");try {host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());...} finally {Trace.traceEnd(Trace.TRACE_TAG_VIEW);}mInLayout = false;}

  ViewRootImpl.performLayout()方法中会调用mViewActivity根窗口mDecor)的layout()方法,为窗口中所有的子控件安排显示的位置,由于不同的容器有不同的布局策略,所以在布局之前首先要确定所有子控件的大小,才能适当的为子控件安排位置,这就是为什么测量过程需要在布局过程之前完成。接着我们看看DecorViewlayout()方法(layout方法继承自View):

step9:View.layout()

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()方法在View中是一个空实现,各种容器需要重写onLayout()方法,为子控件布局onLayout(changed, l, t, r, b);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);}}}mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}

  从上面的代码中,我们注意到关于布局有两个重要的方法,View.layout()View.onLayout(),这两个方法有什么关系?各自的作用是什么呢?他们都是定义在View中的,不同的是layout()方法中有很长一段实现的代码,而onLayout()确实一个空的实现,里面什么事也没做。
  首先我们要明确布局的本质是什么,布局就是为View设置四个坐标值,这四个坐标值保存在View的成员变量mLeft、mTop、mRight、mBottom中,方便View在绘制(onDraw)的时候知道应该在那个区域内绘制控件。而我们看到layout()方法中实际上就是为这几个成员变量赋值的,所以到底真正设置坐标的是layout()方法,那onLayout()的作用是什么呢?
  onLayout()都是由ViewGroup的子类实现的,他的作用就是确定容器中每个子控件的位置,由于不同的容器有不容的布局策略,所以每个容器对onLayout()方法的实现都不同,onLayout()方法会遍历容器中所有的子控件,然后计算他们左上右下的坐标值,最后调用child.layout()方法为子控件设置坐标;由于layout()方法中又调用了onLayout()方法,如果子控件child也是一个容器,就会继续为它的子控件计算坐标,如果child不是容器,onLayout()方法将什么也不做,这样下来,只要Activity根窗口mDecorlayout()方法执行完毕,窗口中所有的子容器、子控件都将完成布局操作。

  其实布局过程的调用方式和测量过程是一样的,ViewGroup的子类都要重写onMeasure()方法遍历子控件调用他们的measure()方法,measure()方法又会调用onMeasure()方法,如果子控件是普通控件就完成了测量,如果是容器将会继续遍历其孙子控件。

继续查看DecorView.onLayout()方法:

step10:DecorView .onLayout()

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {super.onLayout(changed, left, top, right, bottom);getOutsets(mOutsets);if (mOutsets.left > 0) {offsetLeftAndRight(-mOutsets.left);}if (mOutsets.top > 0) {offsetTopAndBottom(-mOutsets.top);}
}

  DecorViewFrameLayout的子类,FrameLayout又是ViewGroup的子类,FrameLayout重写了onLayout()方法,DecorView也重写了onLayout()方法,但是调用的是super.onLayout(),然后做了一些边界判断,下面我们看FrameLayout.onLayout()

step11:FrameLayout .onLayout()

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}void layoutChildren(int left, int top, int right, int bottom,boolean forceLeftGravity) {//获取子控件数量final int count = getChildCount();//获取padding值final int parentLeft = getPaddingLeftWithForeground();final int parentRight = right - left - getPaddingRightWithForeground();final int parentTop = getPaddingTopWithForeground();final int parentBottom = bottom - top - getPaddingBottomWithForeground();//遍历子控件,为其计算左上右下坐标,由于不同容器的布局特性,下面的计算过程都是根据容器的布局特性计算的for (int i = 0; i < count; i++) {final View child = getChildAt(i);if (child.getVisibility() != GONE) {final LayoutParams lp = (LayoutParams) child.getLayoutParams();final int width = child.getMeasuredWidth();final int height = child.getMeasuredHeight();int childLeft;int childTop;int gravity = lp.gravity;if (gravity == -1) {gravity = DEFAULT_CHILD_GRAVITY;}final int layoutDirection = getLayoutDirection();final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {case Gravity.CENTER_HORIZONTAL:childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +lp.leftMargin - lp.rightMargin;break;case Gravity.RIGHT:if (!forceLeftGravity) {childLeft = parentRight - width - lp.rightMargin;break;}case Gravity.LEFT:default:childLeft = parentLeft + lp.leftMargin;}switch (verticalGravity) {case Gravity.TOP:childTop = parentTop + lp.topMargin;break;case Gravity.CENTER_VERTICAL:childTop = parentTop + (parentBottom - parentTop - height) / 2 +lp.topMargin - lp.bottomMargin;break;case Gravity.BOTTOM:childTop = parentBottom - height - lp.bottomMargin;break;default:childTop = parentTop + lp.topMargin;}//调用其layout()方法为子控件设置坐标child.layout(childLeft, childTop, childLeft + width, childTop + height);}}
}

  所有的布局容器的onLayout方法都是一样的流程,都是先遍历子控件,然后计算子控件的坐标,最后调用子控件的layout()方法设置布局坐标,但是不同的布局容器有不同的布局策略,所以区别就在于计算子控件坐标时的差异。比如LinearLayout线性布局,如果是水平布局,第一个子控件的l值是0,r是100,那第二个子控件的l就是101(只是打个比方),而FrameLayout,如果没有设置padding,子控件也没设置margin,第一个子控件的l值就是0,第二个子控件的l还是0,这就是不同容器的计算区别。

  FrameLayout.onLayout()方法执行完毕后,整个Activity的根窗口的布局过程也就完成了。接下来进入第三个过程–绘制过程:

step12:ViewRootImpl.performDraw()控件绘制过程

private void performDraw() {...mIsDrawing = true;Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");try {draw(fullRedrawNeeded);} finally {mIsDrawing = false;Trace.traceEnd(Trace.TRACE_TAG_VIEW);}...
}
private void draw(boolean fullRedrawNeeded) {Surface surface = mSurface;final Rect dirty = mDirty;...if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) {...//使用硬件渲染mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this);} else {...// 通过软件渲染.if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {return;}}}...
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,boolean scalingRequired, Rect dirty) {final Canvas canvas;try {...canvas = mSurface.lockCanvas(dirty);...} catch (Surface.OutOfResourcesException e) {return false;} catch (IllegalArgumentException e) {return false;}...try {...try {...mView.draw(canvas);...} finally {...}} finally {...}return true;
}

  ViewRootImplperformDraw()方法调用draw(boolean),在这个过程中主要完成一些条件判断,surface的设置准备,以及判断使用硬件渲染还是软件渲染等操作,由于我们主要研究绘制代码流程层面,所以直接看drawSoftware()方法,对于硬件渲染具体是怎样的有兴趣可以跟踪一下。drawSoftware()方法中,通过mSurface.locakCanvas(dirty)拿到画布,然后调用mView.draw(canvas),这里的mView就是Activity的根窗口DecorView类型的对象。

step13:DecorView.draw()

public void draw(Canvas canvas) {super.draw(canvas);if (mMenuBackground != null) {mMenuBackground.draw(canvas);}
}

  DecorView重写了Viewdraw()方法,增加了绘制菜单背景的内容,因为Activity根窗口上会有一些菜单按钮(比如屏幕下方的返回键等),draw()方法中调用了super.draw(cancas),所以我们看看Viewdraw()方法:

step14:View.draw()

public void draw(Canvas canvas) {.../** 绘制遍历执行几个绘图步骤,必须以适当的顺序执行:* 1.绘制背景* 2.如果有必要,保存画布的图层,以准备失效* 3.绘制视图的内容* 4.绘制子控件* 5.如果必要,绘制衰落边缘和恢复层* 6.绘制装饰(比如滚动条)*/// Step 1, 绘制背景int saveCount;if (!dirtyOpaque) {drawBackground(canvas);}// 通常情况请跳过2和5步final int viewFlags = mViewFlags;boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;if (!verticalEdges && !horizontalEdges) {// Step 3, 绘制本控件的内容if (!dirtyOpaque) onDraw(canvas);// Step 4, 绘制子控件dispatchDraw(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;}//下面的代码是从第一步到第六步的完整流程...
}

  Viewdraw()方法中有六个步骤,其中最为重要的就是第1步绘制背景、第2步绘制内容、第3步绘制子控件;绘制背景我们就不看了,我们看看绘制内容,调用的是onDraw()方法,View中的onDraw()方法是一个空的实现,可以想象,因为View是所有控件的超类,它并不知道他的孩子要怎样画自己,所以绘制的具体操作由孩子重写onDraw()方法自己绘制,所以DecorView重写了onDraw()方法来绘制Activity的跟窗口,具体怎么画的请跟踪DecorViewonDraw()方法:

step15:DecorView.onDraw()

@Override
public void onDraw(Canvas c) {super.onDraw(c);mBackgroundFallback.draw(mContentRoot, c, mContentParent);
}

  mView(根窗口)把自己的内容绘制完成之后,紧接着开始遍历绘制他的孩子,View中的dispatchDraw()也是一个空的实现,因为超类的后代是不一定有孩子的(比如Button,TextView等),dispatchDraw()是在ViewGroup中实现的,我们看看ViewGroup.dispatchDraw():

#step16:ViewGroup.dispatchDraw()

protected void dispatchDraw(Canvas canvas) {...final int childrenCount = mChildrenCount;final View[] children = mChildren;if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {...for (int i = 0; i < childrenCount; i++) {while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {final View transientChild = mTransientViews.get(transientIndex);if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||transientChild.getAnimation() != null) {more |= drawChild(canvas, transientChild, drawingTime);}transientIndex++;if (transientIndex >= transientCount) {transientIndex = -1;}}int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;final View child = (preorderedList == null)? children[childIndex] : preorderedList.get(childIndex);if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {more |= drawChild(canvas, child, drawingTime);}}}...
}protected boolean drawChild(Canvas canvas, View child, long drawingTime) {return child.draw(canvas, this, drawingTime);
}

  dispatchDraw()方法中首先获取子控件的数量childrenCount,然后遍历所有子控件,调用drawChild()方法,drawChild()方法中只是简单的调用child.draw(),这样就完成了子控件的绘制。细心的可以发现child的draw()方法又会执行之前DecorView.draw()的六步(draw()是在View里面实现的),所以说,所有的控件在绘制的时候都会调用draw()方法,draw()方法中会先调用onDraw()方法绘制自己,然后调用dispatchDraw()绘制它的子控件,如果此控件不是ViewGroup的子类,也就是说是叶子控件,dispatchDraw()`什么也不做。

  到此为止,Activity的整个显示过程就已经分析完毕,这篇文章主要讲解了View测量、布局、绘制流程,这三个流程一共涉及到六个重要的方法measure()onMeasure()layout()onLayout()draw()onDraw()onMeasure()onLayout()onDraw()一般都由View的子类(具体的控件或者布局)重写,measure()layout()draw()在View中有默认的实现,他们都是xxx()调用onXxx(),这种形式就能依次完成整个布局树的绘制流程。将这篇文章的流程整理如下图:

        这里写图片描述

##参考源码工程:
https://github.com/openXu/AndroidActivityLaunchProgress

这篇关于Activtiy完全解析(三、View的显示过程measure、layout、draw)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/968204

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

作业提交过程之HDFSMapReduce

作业提交全过程详解 (1)作业提交 第1步:Client调用job.waitForCompletion方法,向整个集群提交MapReduce作业。 第2步:Client向RM申请一个作业id。 第3步:RM给Client返回该job资源的提交路径和作业id。 第4步:Client提交jar包、切片信息和配置文件到指定的资源提交路径。 第5步:Client提交完资源后,向RM申请运行MrAp

第10章 中断和动态时钟显示

第10章 中断和动态时钟显示 从本章开始,按照书籍的划分,第10章开始就进入保护模式(Protected Mode)部分了,感觉从这里开始难度突然就增加了。 书中介绍了为什么有中断(Interrupt)的设计,中断的几种方式:外部硬件中断、内部中断和软中断。通过中断做了一个会走的时钟和屏幕上输入字符的程序。 我自己理解中断的一些作用: 为了更好的利用处理器的性能。协同快速和慢速设备一起工作

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

安卓链接正常显示,ios#符被转义%23导致链接访问404

原因分析: url中含有特殊字符 中文未编码 都有可能导致URL转换失败,所以需要对url编码处理  如下: guard let allowUrl = webUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {return} 后面发现当url中有#号时,会被误伤转义为%23,导致链接无法访问

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

【机器学习】高斯过程的基本概念和应用领域以及在python中的实例

引言 高斯过程(Gaussian Process,简称GP)是一种概率模型,用于描述一组随机变量的联合概率分布,其中任何一个有限维度的子集都具有高斯分布 文章目录 引言一、高斯过程1.1 基本定义1.1.1 随机过程1.1.2 高斯分布 1.2 高斯过程的特性1.2.1 联合高斯性1.2.2 均值函数1.2.3 协方差函数(或核函数) 1.3 核函数1.4 高斯过程回归(Gauss

HDU 2159 二维完全背包

FATE 最近xhd正在玩一款叫做FATE的游戏,为了得到极品装备,xhd在不停的杀怪做任务。久而久之xhd开始对杀怪产生的厌恶感,但又不得不通过杀怪来升完这最后一级。现在的问题是,xhd升掉最后一级还需n的经验值,xhd还留有m的忍耐度,每杀一个怪xhd会得到相应的经验,并减掉相应的忍耐度。当忍耐度降到0或者0以下时,xhd就不会玩这游戏。xhd还说了他最多只杀s只怪。请问他能

zoj 1721 判断2条线段(完全)相交

给出起点,终点,与一些障碍线段。 求起点到终点的最短路。 枚举2点的距离,然后最短路。 2点可达条件:没有线段与这2点所构成的线段(完全)相交。 const double eps = 1e-8 ;double add(double x , double y){if(fabs(x+y) < eps*(fabs(x) + fabs(y))) return 0 ;return x + y ;