a.获取WindowManager实例b.深入理解 ViewRootImplc.window测量
final WindowManager wm = (WindowManager)getSystemService(Context.WINDOW_SERVICE);
public Object getSystemService(String name) { // 获取WINDOW_SERVICE所对应的ServiceFetcher ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name); // 调用fetcher.getService()获取一个实例 returnfetcher == null ? null : fetcher.getService(this); }
registerService(WINDOW_SERVICE, newServiceFetcher() { public Object getService(ContextImpl ctx) { // ① 获取Context中所保存的Display对象 Display display = ctx.mDisplay; /* ② 倘若Context中没有保存任何Display对象,则通过DisplayManager获取系统 **主屏幕所对应的Display对象** */ if (display == null) { DisplayManager dm = (DisplayManager)ctx.getOuterContext().getSystemService( Context.DISPLAY_SERVICE); display = dm.getDisplay(Display.DEFAULT_DISPLAY); } // ③ 使用Display对象作为构造函数创建一个WindowManagerImpl对象并返回 return new WindowManagerImpl(display); }}); ....... }
public WindowManagerImpl(Display display) { this(display, null); } private WindowManagerImpl(Display display, Window parentWindow) { mDisplay = display; mParentWindow = parentWindow; }
public ViewRootImpl(Context context, Displaydisplay) { /* ① 从WindowManagerGlobal中获取一个IWindowSession的实例。它是ViewRootImpl和 WMS进行通信的代理 */ mWindowSession= WindowManagerGlobal.getWindowSession(context.getMainLooper()); // **②保存参数display**,在后面setView()调用中将会把窗口添加到这个Display上 mDisplay= display; CompatibilityInfoHolder cih = display.getCompatibilityInfo(); mCompatibilityInfo = cih != null ? cih : new CompatibilityInfoHolder(); /* **③ 保存当前线程到mThread。**这个赋值操作体现了创建ViewRootImpl的线程如何成为UI主线程。 在ViewRootImpl处理来自控件树的请求时(如请求重新布局,请求重绘,改变焦点等),会检 查发起请求的thread与这个mThread是否相同。倘若不同则会拒绝这个请求并抛出一个异常*/ mThread= Thread.currentThread(); ...... /* **④ mDirty用于收集窗口中的无效区域。**所谓无效区域是指由于数据或状态发生改变时而需要进行重绘 的区域。举例说明,当应用程序修改了一个TextView的文字时,TextView会将自己的区域标记为无效 区域,并通过invalidate()方法将这块区域收集到这里的mDirty中。当下次绘制时,TextView便 可以将新的文字绘制在这块区域上 */ mDirty =new Rect(); mTempRect = new Rect(); mVisRect= new Rect(); /* **⑤ mWinFrame,描述了当前窗口的位置和尺寸。**与WMS中WindowState.mFrame保持着一致 */ mWinFrame = new Rect(); /* ⑥ 创建一个W类型的实例,W是IWindow.Stub的子类。即它将在WMS中作为新窗口的ID,以及接 收来自WMS的回调*/ mWindow= new W(this); ...... /* **⑦ 创建mAttachInfo。**mAttachInfo是控件系统中很重要的对象。它存储了此当前控件树所以贴附 的窗口的各种有用的信息,并且会派发给控件树中的每一个控件。这些控件会将这个对象保存在自己的 mAttachInfo变量中。mAttachInfo中所保存的信息有WindowSession,窗口的实例(即mWindow), ViewRootImpl实例,窗口所属的Display,窗口的Surface以及窗口在屏幕上的位置等等。所以,当 要需在一个View中查询与当前窗口相关的信息时,非常值得在mAttachInfo中搜索一下 */ mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display,this, mHandler, this); /* **⑧ 创建FallbackEventHandler。**这个类如同PhoneWindowManger一样定义在android.policy 包中,其实现为PhoneFallbackEventHandler。FallbackEventHandler是一个处理未经任何人 消费的输入事件的场所。在6.5.4节中将会介绍它 */ mFallbackEventHandler =PolicyManager.makeNewFallbackEventHandler(context); ...... /* ⑨ 创建一个依附于当前线程,即主线程的Choreographer,用于通过VSYNC特性安排重绘行为 */ mChoreographer= Choreographer.getInstance(); ...... }
//初始化 final ViewRootHandler mHandler = new ViewRootHandler(); final class ViewRootHandler extends Handler { @Override public String getMessageName(Message message) { // 设置message名称 switch (message.what) { case MSG_INVALIDATE://message无效 return "MSG_INVALIDATE"; ........ case MSG_WINDOW_MOVED://移除 window return "MSG_WINDOW_MOVED"; case MSG_SYNTHESIZE_INPUT_EVENT: // 系统输入事件 return "MSG_SYNTHESIZE_INPUT_EVENT"; ........ } return super.getMessageName(message); } @Override public boolean sendMessageAtTime(Message msg, long uptimeMillis) { if (msg.what == MSG_REQUEST_KEYBOARD_SHORTCUTS && msg.obj == null) { // Debugging for b/27963013 throw new NullPointerException( "Attempted to call MSG_REQUEST_KEYBOARD_SHORTCUTS with null receiver:"); } return super.sendMessageAtTime(msg, uptimeMillis); } @Override public void handleMessage(Message msg) { switch (msg.what) { //处理各种Message ..... } } }
final Surface mSurface = new Surface();
// These are accessed by multiple threads. final Rect mWinFrame; // frame given by window manager. final Rect mPendingOverscanInsets = new Rect(); final Rect mPendingVisibleInsets = new Rect(); final Rect mPendingStableInsets = new Rect(); final Rect mPendingContentInsets = new Rect(); final Rect mPendingOutsets = new Rect(); final Rect mPendingBackDropFrame = new Rect();
int mWidth; int mHeight;
这几个成员存储了窗口布局相关的信息。其中mWinFrame、mPendingConentInsets、mPendingVisibleInsets与窗口在WMS中的Frame、ContentInsets、VisibleInsets是保持同步的。这是因为这3个成员不仅会作为 relayoutWindow()的传出参数,而且ViewRootImpl在收到来自WMS的回调IWindow.Stub.resized()时,立即更新这3个成员的取值。因此这3个成员体现了窗口在WMS中的最新状态。与mWinFrame中的记录窗口在WMS中的尺寸不同的是,mWidth/mHeight记录了窗口在ViewRootImpl中的尺寸,二者在绝大多数情况下是相同的。当窗口在WMS中被重新布局而导致尺寸发生变化时,mWinFrame会首先被IWindow.Stub.resized()回调更新,此时mWinFrame便会与mWidth/mHeight产生差异。此时ViewRootImpl即可得知需要对控件树进行重新布局以适应新的窗口变化。在布局完成后,mWidth/mHeight会被赋值为mWinFrame中所保存的宽和高,二者重新统一。在随后分析performTraversals()方法时,读者将会看到这一处理。另外,与mWidth/mHeight类似,ViewRootImpl也保存了窗口的位置信息Left/Top以及ContentInsets/VisibleInsets供控件树查询,不过这四项信息被保存在了mAttachInfo中。接下来我们看看源码怎么实现更新的:
初始化:
mWidth = -1; mHeight = -1;
重新布局mWidth/mHeight会被赋值为mWinFrame中所保存的宽和高:
private void performTraversals() { ...... if (mWidth != frame.width() || mHeight != frame.height()) { mWidth = frame.width(); mHeight = frame.height(); } ...... }
IWindow.Stub#resized:
@Override public void resized(Rect frame, Rect overscanInsets, Rect contentInsets, Rect visibleInsets, Rect stableInsets, Rect outsets, boolean reportDraw, Configuration newConfig, Rect backDropFrame, boolean forceLayout, boolean alwaysConsumeNavBar) { final ViewRootImpl viewAncestor = mViewAncestor.get(); if (viewAncestor != null) { viewAncestor.dispatchResized(frame, overscanInsets, contentInsets, visibleInsets, stableInsets, outsets, reportDraw, newConfig, backDropFrame, forceLayout, alwaysConsumeNavBar); } }
ViewRootImpl#dispatchResized:
public void dispatchResized(Rect frame, Rect overscanInsets, Rect contentInsets, Rect visibleInsets, Rect stableInsets, Rect outsets, boolean reportDraw, Configuration newConfig, Rect backDropFrame, boolean forceLayout, boolean alwaysConsumeNavBar) { ...... Message msg = mHandler.obtainMessage(reportDraw ? MSG_RESIZED_REPORT : MSG_RESIZED); ...... SomeArgs args = SomeArgs.obtain(); final boolean sameProcessCall = (Binder.getCallingPid() == android.os.Process.myPid()); args.arg1 = sameProcessCall ? new Rect(frame) : frame; args.arg2 = sameProcessCall ? new Rect(contentInsets) : contentInsets; args.arg3 = sameProcessCall ? new Rect(visibleInsets) : visibleInsets; args.arg4 = sameProcessCall && newConfig != null ? new Configuration(newConfig) : newConfig; args.arg5 = sameProcessCall ? new Rect(overscanInsets) : overscanInsets; args.arg6 = sameProcessCall ? new Rect(stableInsets) : stableInsets; args.arg7 = sameProcessCall ? new Rect(outsets) : outsets; args.arg8 = sameProcessCall ? new Rect(backDropFrame) : backDropFrame; args.argi1 = forceLayout ? 1 : 0; args.argi2 = alwaysConsumeNavBar ? 1 : 0; msg.obj = args; mHandler.sendMessage(msg); }
看来是通过Message把要更新的数据给mHandler处理。
更新mWinFrame、mPendingConentInsets、mPendingVisibleInsets:
@Override public void handleMessage(Message msg) { switch (msg.what) { ..... case MSG_RESIZED: { // Recycled in the fall through... SomeArgs args = (SomeArgs) msg.obj; if (mWinFrame.equals(args.arg1) && mPendingOverscanInsets.equals(args.arg5) && mPendingContentInsets.equals(args.arg2) && mPendingStableInsets.equals(args.arg6) && mPendingVisibleInsets.equals(args.arg3) && mPendingOutsets.equals(args.arg7) && mPendingBackDropFrame.equals(args.arg8) && args.arg4 == null && args.argi1 == 0) { break; } } // fall through... case MSG_RESIZED_REPORT: if (mAdded) { SomeArgs args = (SomeArgs) msg.obj; Configuration config = (Configuration) args.arg4; if (config != null) { updateConfiguration(config, false); } final boolean framesChanged = !mWinFrame.equals(args.arg1) || !mPendingOverscanInsets.equals(args.arg5) || !mPendingContentInsets.equals(args.arg2) || !mPendingStableInsets.equals(args.arg6) || !mPendingVisibleInsets.equals(args.arg3) || !mPendingOutsets.equals(args.arg7); mWinFrame.set((Rect) args.arg1); mPendingOverscanInsets.set((Rect) args.arg5); mPendingContentInsets.set((Rect) args.arg2); mPendingStableInsets.set((Rect) args.arg6); mPendingVisibleInsets.set((Rect) args.arg3); mPendingOutsets.set((Rect) args.arg7); mPendingBackDropFrame.set((Rect) args.arg8); mForceNextWindowRelayout = args.argi1 != 0; mPendingAlwaysConsumeNavBar = args.argi2 != 0; args.recycle(); if (msg.what == MSG_RESIZED_REPORT) { mReportNextDraw = true; } if (mView != null && framesChanged) { forceLayout(mView); } requestLayout(); } break; ..... }
}
可以看出更新mWinFrame、mPendingConentInsets、mPendingVisibleInsets最终是在上面提到mHandler的handleMessage方法中进行的。
public void setView(View view,WindowManager.LayoutParams attrs, View panelParentView) { synchronized (this) { if (mView == null) { // **① mView保存了控件树的根** mView = view; ...... // ②mWindowAttributes保存了窗口所对应的LayoutParams mWindowAttributes.copyFrom(attrs); ...... /* 在添加窗口之前,先通过requestLayout()方法在主线程上安排一次“遍历”。所谓 “遍历”是指ViewRootImpl中的核心方法performTraversals()。这个方法实现了对 控件树进行测量、布局、向WMS申请修改窗口属性以及重绘的所有工作。由于此“遍历” 操作对于初次遍历做了一些特殊处理,而来自WMS通过mWindow发生的回调会导致一些属性 发生变化,如窗口的尺寸、Insets以及窗口焦点等,从而有可能使得初次“遍历”的现场遭 到破坏。因此,需要在添加窗口之前,先发送一个“遍历”消息到主线程。 在主线程中向主线程的Handler发送消息如果使用得当,可以产生很精妙的效果。例如本例 中可以实现如下的执行顺序:添加窗口->初次遍历->处理来自WMS的回调 */ requestLayout(); /***③ 初始化mInputChannel。**参考第五章,InputChannel是窗口接受来自InputDispatcher 的输入事件的管道。 注意,仅当窗口的属性inputFeatures不含有 INPUT_FEATURE_NO_INPUT_CHANNEL时才会创建InputChannel,否则mInputChannel 为空,从而导致此窗口无法接受任何输入事件 */ if ((mWindowAttributes.inputFeatures & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) { mInputChannel = new InputChannel(); } try { ...... /* 将窗口添加到WMS中。完成这个操作之后,mWindow已经被添加到指定的Display中去 而且mInputChannel(如果不为空)已经准备好接受事件了。只是由于这个窗口没有进行 过relayout(),因此它还没有有效的Surface可以进行绘制 */ res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mAttachInfo.mContentInsets, mInputChannel); } catch (RemoteException e) {......} finally { ...... } ...... if (res < WindowManagerGlobal.ADD_OKAY) { // 错误处理。窗口添加失败的原因通常是权限问题,重复添加,或者tokeen无效 } ...... /*④ 如果mInputChannel不为空,则创建mInputEventReceiver,用于接受输入事件。 注意第二个参数传递的是Looper.myLooper(),即mInputEventReceiver将在主线程上 触发输入事件的读取与onInputEvent()。这是应用程序可以在onTouch()等事件响应中 直接进行UI操作等根本原因。 */ if (mInputChannel != null) { ...... mInputEventReceiver = new WindowInputEventReceiver(mInputChannel, Looper.myLooper()); } /* ViewRootImpl将作为参数view的parent。所以,ViewRootImpl可以从控件树中任何一个 控件开始,通过回溯getParent()的方法得到 */ view.assignParent(this); ...... } } }
@Override public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true; scheduleTraversals(); } }
void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); if (!mUnbufferedInputDispatch) { scheduleConsumeBatchedInput(); } notifyRendererOfFramePending(); pokeDrawLockIfNeeded(); } }
final class TraversalRunnable implements Runnable { @Override public void run() { doTraversal(); } } final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void doTraversal() { if (mTraversalScheduled) { mTraversalScheduled = false; mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); if (mProfile) { Debug.startMethodTracing("ViewAncestor"); } performTraversals(); if (mProfile) { Debug.stopMethodTracing(); mProfile = false; } } }
这是进入performTraversals()方法后的第一个阶段,它会对控件树进行第一次测量。测量结果可以通过mView. getMeasuredWidth()/Height()获得。在此阶段中将会计算出控件树为显示其内容所需的尺寸,即窗口所期望的尺寸。在这个阶段中,View及其子类的onMeasure()方法将会沿着控件树依次得到回调。
根据预测量的结果,通过IWindowSession.relayout()方法向WMS请求调整窗口的尺寸等属性,这将引发WMS对窗口进行重新布局,并将布局结果返回给ViewRootImpl。
预测量的结果是控件树所期望的窗口尺寸。然而由于在WMS中影响窗口布局的因素很多(以后补充,可以参见这篇《深入理解WindowManagerService》),WMS不一定会将窗口准确地布局为控件树所要求的尺寸,而迫于WMS作为系统服务的强势地位,控件树不得不接受WMS的布局结果。因此在这一阶段,performTraversals()将以窗口的实际尺寸对控件进行最终测量。在这个阶段中,View及其子类的onMeasure()方法将会沿着控件树依次被回调。
完成最终测量之后便可以对控件树进行布局了。测量确定的是控件的尺寸,而布局则是确定控件的位置。在这个阶段中,View及其子类的onLayout()方法将会被回调。
这是performTraversals()的最终阶段。确定了控件的位置与尺寸后,便可以对控件树进行绘制了。在这个阶段中,View及其子类的onDraw()方法将会被回调。
/** * 这个方法需要被重写,应该由子类去决定测量的宽高值, */ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
private void performTraversals() { // 将mView保存在局部变量host中,以此提高对mView的访问效率 finalView host = mView; ...... // 声明本阶段的主角,这两个变量将是mView的宽度、高度SPEC_SIZE分量的候选 int desiredWindowWidth; int desiredWindowHeight; ....... Rect frame = mWinFrame; // 如上一节所述,mWinFrame表示了窗口的最新尺寸 if(mFirst) { /*mFirst表示了这是第一次遍历,此时窗口刚刚被添加到WMS,此时窗口尚未进行relayout,因此 mWinFrame中没有存储有效地窗口尺寸 */ if(lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL) { ......// 为状态栏设置desiredWindowWidth/Height,其取值是屏幕尺寸 }else { //① 第一次“遍历”的测量,采用了应用可以使用的最大尺寸作为SPEC_SIZE的候选 DisplayMetrics packageMetrics = mView.getContext().getResources().getDisplayMetrics(); desiredWindowWidth = packageMetrics.widthPixels; desiredWindowHeight = packageMetrics.heightPixels; } /* 由于这是第一次进行“遍历”,控件树即将第一次被显示在窗口上,因此接下来的代码填充了 mAttachInfo中的一些字段,然后通过mView发起了dispatchAttachedToWindow()的调用 之后每一个位于控件树中的控件都会回调onAttachedToWindow() */ ...... } else { // ② 在非第一次遍历的情况下,会采用窗口的最新尺寸作为SPEC_SIZE的候选 desiredWindowWidth = frame.width(); desiredWindowHeight = frame.height(); /* 如果窗口的最新尺寸与ViewRootImpl中的现有尺寸不同,说明WMS侧单方面改变了窗口的尺寸 这将产生如下三个结果 */ if(desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) { // 需要进行完整的重绘以适应新的窗口尺寸 mFullRedrawNeeded = true; // 需要对控件树进行重新布局 mLayoutRequested = true; /* 控件树有可能拒绝接受新的窗口尺寸,比如在随后的预测量中给出了不同于窗口尺寸的测量结果 产生这种情况时,就需要在窗口布局阶段尝试设置新的窗口尺寸 */ windowSizeMayChange = true; } } ...... /* 执行位于RunQueue中的回调。RunQueue是ViewRootImpl的一个静态成员,即是说它是进程唯一 的,并且可以在进程的任何位置访问RunQueue。在进行多线程任务时,开发者可以通过调用View.post() 或View.postDelayed()方法将一个Runnable对象发送到主线程执行。这两个方法的原理是将 Runnable对象发送到ViewRootImpl的mHandler去。当控件已经加入到控件树时,可以通过 AttachInfo轻易获取这个Handler。而当控件没有位于控件树中时,则没有mAttachInfo可用,此时 执行View.post()/PostDelay()方法,Runnable将会被添加到这个RunQueue队列中。 在这里,ViewRootImpl将会把RunQueue中的Runnable发送到mHandler中,进而得到执行。所以 无论控件是否显示在控件树中,View.post()/postDelay()方法都是可用的,除非当前进程中没有任何 处于活动状态的ViewRootImpl */ getRunQueue().executeActions(attachInfo.mHandler); boolean layoutRequested = mLayoutRequested && !mStopped; /* 仅当layoutRequested为true时才进行预测量。 layoutRequested为true表示在进行“遍历”之前requestLayout()方法被调用过。 requestLayout()方法用于要求ViewRootImpl进行一次“遍历”并对控件树重新进行测量与布局 */ if(layoutRequested) { final Resources res = mView.getContext().getResources(); if(mFirst) { ......// 确定控件树是否需要进入TouchMode,本章将在6.5.1节介绍 TouchMode }else { /*检查WMS是否单方面改变了ContentInsets与VisibleInsets。注意对二者的处理的差异, ContentInsets描述了控件在布局时必须预留的空间,这样会影响控件树的布局,因此将 insetsChanged标记为true,以此作为是否进行控件布局的条件之一。而VisibleInsets则 描述了被遮挡的空间,ViewRootImpl在进行绘制时,需要调整绘制位置以保证关键控件或区域, 如正在进行输入的TextView等不被遮挡,这样VisibleInsets的变化并不会导致重新布局, 所以这里仅仅是将VisibleInsets保存到mAttachInfo中,以便绘制时使用 */ if (!mPendingContentInsets.equals(mAttachInfo.mContentInsets)) { insetsChanged = true; } if (!mPendingVisibleInsets.equals(mAttachInfo.mVisibleInsets)) { mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets); } /*当窗口的width或height被指定为WRAP_CONTENT时,表示这是一个悬浮窗口。 此时会对desiredWindowWidth/Height进行调整。在前面的代码中,这两个值被设置 被设置为窗口的当前尺寸。而根据MeasureSpec的要求,测量结果不得大于SPEC_SIZE。 然而,如果这个悬浮窗口需要更大的尺寸以完整显示其内容时,例如为AlertDialog设置了 一个更长的消息内容,如此取值将导致无法得到足够大的测量结果,从而导致内容无法完整显示。 因此,对于此等类型的窗口,ViewRootImpl会调整desiredWindowWidth/Height为此应用 可以使用的最大尺寸 */ if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) { // 悬浮窗口的尺寸取决于测量结果。因此有可能需要向WMS申请改变窗口的尺寸。 windowSizeMayChange = true; if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL) { // } else { // ③ 设置悬浮窗口SPEC_SIZE的候选为应用可以使用的最大尺寸 DisplayMetrics packageMetrics = res.getDisplayMetrics(); desiredWindowWidth = packageMetrics.widthPixels; desiredWindowHeight = packageMetrics.heightPixels; } } } // **④ 进行预测量。**通过measureHierarchy()方法以desiredWindowWidth/Height进行测量 windowSizeMayChange |=measureHierarchy(host, lp, res, desiredWindowWidth, desiredWindowHeight); } // 其他阶段的处理 ...... }
a.第一次遍历,使用应用的最大值(一般是屏幕的宽度)作为SPEC_SIZE的候选(值)。b.如果是悬浮窗口,其属性LayoutParam.width/height之一设置为 WRAP_CONTENT,那么将会 使用应用的最大值(一般是屏幕的宽度)作为SPEC_SIZE的候选(值)。c.在其他的情况下,使用窗口的最新尺寸(这个值一般存储在mWinFrame中 )作为 SPEC_SIZE的候选(值)。
private boolean measureHierarchy(final View host,final WindowManager.LayoutParams lp, final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) { int childWidthMeasureSpec; // 合成后的用于描述子控件宽度的MeasureSpec int childHeightMeasureSpec; // 合成后的用于描述子控件高度的MeasureSpec boolean windowSizeMayChange = false; // 表示测量结果是否可能导致窗口的尺寸发生变化 boolean goodMeasure = false; // goodMeasure表示了测量是否能满足控件树充分显示内容的要求 // 测量协商仅发生在LayoutParams.width被指定为WRAP_CONTENT的情况下 if(lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) { /* **① 第一次协商。**measureHierarchy()使用它最期望的宽度限制进行测量。这一宽度限制定义为 一个系统资源。可以在frameworks/base/core/res/res/values/config.xml找到它的定义 */ res.getValue(com.android.internal.R.dimen.config_prefDialogWidth,mTmpValue, true); int baseSize = 0; // 宽度限制被存放在baseSize中 if(mTmpValue.type == TypedValue.TYPE_DIMENSION) { baseSize = (int)mTmpValue.getDimension(packageMetrics); } if(baseSize != 0 && desiredWindowWidth > baseSize) { // 使用getRootMeasureSpec()函数组合SPEC_MODE与SPEC_SIZE为一个MeasureSpec childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight,lp.height); //**②第一次测量。**由performMeasure()方法完成 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); /* 控件树的测量结果可以通过mView的getmeasuredWidthAndState()方法获取。如果 控件树对这个测量结果不满意,则会在返回值中添加MEASURED_STATE_TOO_SMALL位 */ if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) ==0) { goodMeasure = true; // 控件树对测量结果满意,测量完成 } else { // **③ 第二次协商。**上次测量结果表明控件树认为measureHierarchy()给予的宽度太小, 在此适当地放宽对宽度的限制,使用最大宽度与期望宽度的中间值作为宽度限制 */ baseSize = (baseSize+desiredWindowWidth)/2; childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); // **④ 第二次测量** performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); // 再次检查控件树是否满足此次测量 if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) { goodMeasure = true; // 控件树对测量结果满意,测量完成 } } } } if(!goodMeasure) { /* **⑤ 最终测量。**当控件树对上述两次协商的结果都不满意时,measureHierarchy()放弃所有限制 做最终测量。这一次将不再检查控件树是否满意了,因为即便其不满意,measurehierarchy()也没 有更多的空间供其使用了 */ childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth,lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight,lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); /* 最后,如果测量结果与ViewRootImpl中当前的窗口尺寸不一致,则表明随后可能有必要进行窗口 尺寸的调整 */ if(mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) { windowSizeMayChange = true; } } // 返回窗口尺寸是否可能需要发生变化 return windowSizeMayChange; }
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure"); try {// mView 是父控件 mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } }
public final void measure(int widthMeasureSpec,int heightMeasureSpec) { /* 仅当给予的MeasureSpec发生变化,或要求强制重新布局时,才会进行测量。 所谓强制重新布局,是指当控件树中的一个子控件的内容发生变化时,需要进行重新的测量和布局的情况 在这种情况下,这个子控件的父控件(以及其父控件的父控件)所提供的MeasureSpec必定与上次测量 时的值相同,因而导致从ViewRootImpl到这个控件的路径上的父控件的measure()方法无法得到执行 进而导致子控件无法重新测量其尺寸或布局。因此,当子控件因内容发生变化时,从子控件沿着控件树回溯 到ViewRootImpl,并依次调用沿途父控件的requestLayout()方法,在这个方法中,会在 mPrivateFlags中加入标记PFLAG_FORCE_LAYOUT,从而使得这些父控件的measure()方法得以顺利 执行,进而这个子控件有机会进行重新测量与布局。这便是强制重新布局的意义 */ if ((mPrivateFlags& PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT || widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) { /* **① 准备工作。**从mPrivateFlags中将PFLAG_MEASURED_DIMENSION_SET标记去除。 PFLAG_MEASURED_DIMENSION_SET标记用于检查控件在onMeasure()方法中是否通过 调用setMeasuredDimension()将测量结果存储下来 */ mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET; ...... /* **② 对本控件进行测量** 每个View子类都需要重载这个方法以便正确地对自身进行测量。 View类的onMeasure()方法仅仅根据背景Drawable或style中设置的最小尺寸作为 测量结果*/ onMeasure(widthMeasureSpec, heightMeasureSpec); /* ③ 检查onMeasure()的实现是否调用了setMeasuredDimension() setMeasuredDimension()会将PFLAG_MEASURED_DIMENSION_SET标记重新加入 mPrivateFlags中。之所以做这样的检查,是由于onMeasure()的实现可能由开发者完成, 而在Android看来,开发者是不可信的 */ if((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) !=PFLAG_MEASURED_DIMENSION_SET) { throw new IllegalStateException(......); } // ④ 将PFLAG_LAYOUT_REQUIRED标记加入mPrivateFlags。这一操作会对随后的布局操作放行 mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; } // 记录父控件给予的MeasureSpec,用以检查之后的测量操作是否有必要进行 mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; }
/** * 这个方法需要被重写,应该由子类去决定测量的宽高值, */ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
getDefaultSize???看下源码:
View的模式为MeasureSpec.UNSPECIFIED,返回建议尺寸,否则返回specSize。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; }
protected final void setMeasuredDimension(intmeasuredWidth, int measuredHeight) { // ① 测量结果被分别保存在成员变量mMeasuredWidth与mMeasuredHeight中 mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; // ② 向mPrivateFlags中添加PFALG_MEASURED_DIMENSION_SET,以此证明onMeasure()保存了测量结果 mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; }
onMeasure()算法的一些实现原则:a.保存测量结果,测量结果是通过 g etMeasuredWidthAndState()与getMeasuredHeightAndState()两个方法获得。b.向mPrivateFlags中添加PFALG_MEASURED_DIMENSION_SET,以此证明onMeasure()保存了测量结果
c.控件在进行测量时,控件需要将它的 Padding 尺寸计算在内,因为Padding是其尺寸的一部分。d.ViewGroup在进行测量时,需要将子控件的 Margin 尺寸计算在内。因为子控件的Margin尺寸是父控件尺寸的一部分。e.ViewGroup为子控件准备MeasureSpec时, SPEC_MODE应取决于子控件的LayoutParams.width/height的取值 。取值为MATCH_PARENT或一个确定的尺寸时应为EXACTLY,WRAP_CONTENT时应为AT_MOST。至于SPEC_SIZE,应理解为ViewGroup对子控件尺寸的限制,即ViewGroup按照其实现意图所允许子控件获得的最大尺寸。并且需要扣除子控件的Margin尺寸。f.虽然说测量的目的在于确定尺寸,与位置无关。但是子控件的 位置 是ViewGroup进行测量时必须要首先 考虑 的。因为子控件的位置即决定了子控件可用的剩余尺寸,也决定了父控件的尺寸(当父控件的LayoutParams.width/height为WRAP_CONTENT时)。g.在测量结果中 添加MEASURED_STATE_TOO_SMALL需要做到实事求是 。当一个方向上的空间不足以显示其内容时应考虑利用另一个方向上的空间,例如对文字进行换行处理,因为添加这个标记有可能导致父控件对其进行重新测量从而降低效率。h.当子控件的测量结果中包含MEASURED_STATE_TOO_SMALL标记时,只要有可能, 父控件就应当调整给予子控件的MeasureSpec,并进行重新测量 。倘若没有调整的余地,父控件也应当将MEASURED_STATE_TOO_SMALL加入到自己的测量结果中,让它的父控件尝试进行调整。i.ViewGroup在 测量子控件时必须调用子控件的measure()方法 ,而不能直接调用其onMeasure()方法。直接调用onMeasure()方法的最严重后果是子控件的PFLAG_LAYOUT_REQUIRED标识无法加入到mPrivateFlag中,从而导致子控件无法进行布局。
接下来回到performTraversals()方法。在ViewRootImpl.measureHierarchy()执行完毕之后,ViewRootImpl了解了控件树所需的空间。于是便可确定是否需要改变窗口窗口尺寸以便满足控件树的空间要求。前述的代码中多处设置windowSizeMayChange变量为true。windowSizeMayChange仅表示有可能需要改变窗口尺寸。而接下来的这段代码则用来确定窗口是否需要改变尺寸。
private void performTraversals() { ......// 测量控件树的代码 /* 标记mLayoutRequested为false。因此在此之后的代码中,倘若控件树中任何一个控件执行了 requestLayout(),都会重新进行一次“遍历” */ if (layoutRequested) { mLayoutRequested = false; } // 确定窗口是否确实需要进行尺寸的改变 booleanwindowShouldResize = layoutRequested && windowSizeMayChange && ((mWidth != host.getMeasuredWidth() || mHeight !=host.getMeasuredHeight()) || (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT && frame.width() < desiredWindowWidth && frame.width() !=mWidth) || (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT && frame.height() < desiredWindowHeight && frame.height() !=mHeight)); }
确定窗口尺寸是否确实需要改变的条件看起来比较复杂,这里进行一下总结,先介绍必要条件:
a: layoutRequested为true ,即ViewRootImpl.requestLayout()方法被调用过。View中也有requestLayout()方法。当控件内容发生变化从而需要调整其尺寸时,会调用其自身的requestLayout(),并且此方法会沿着控件树向根部回溯,最终调用到ViewRootImp.requestLayout(),从而引发一次performTraversals()调用。之所以这是一个必要条件,是因为performTraversals()还有可能因为控件需要重绘时被调用。当控件仅需要重绘而不需要重新布局时(例如背景色或前景色发生变化时),会通过invalidate()方法回溯到ViewRootImpl,此时不会通过performTraversals()触发performTraversals()调用,而是通过scheduleTraversals()进行触发。在这种情况下layoutRequested为false,即表示窗口尺寸不需发生变化。b:windowSizeMayChange为true ,如前文所讨论的,这意味着WMS单方面改变了窗口尺寸而控件树的测量结果与这一尺寸有差异,或当前窗口为悬浮窗口,其控件树的测量结果将决定窗口的新尺寸。在满足上述两个条件的情况下,以下两个条件满足其一:c: 测量结果与ViewRootImpl中所保存的当前尺寸有差异。d: 悬浮窗口的测量结果与窗口的最新尺寸有差异。
注意ViewRootImpl对是否需要调整窗口尺寸的判断是非常小心的。第4章介绍WMS的布局子系统时曾经介绍过,调整窗口尺寸所必须调用的performLayoutAndPlaceSurfacesLocked()函数会导致WMS对系统中的所有窗口新型重新布局,而且会引发至少一个动画帧渲染,其计算开销相当之大。因此ViewRootImpl仅在必要时才会惊动WMS。
至此,预测量阶段完成了。
如果不同意见,尽情联系小编, 如要转载,请注明来源, 小石头的博客 : http://blog.csdn.net/lu1024188315