zoukankan      html  css  js  c++  java
  • Android开发之漫漫长途 Ⅴ——Activity的显示之ViewRootImpl的PreMeasure、WindowLayout、EndMeasure、Layout、Draw

    该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列。该系列引用了《Android开发艺术探索》以及《深入理解Android 卷Ⅰ,Ⅱ,Ⅲ》中的相关知识,另外也借鉴了其他的优质博客,在此向各位大神表示感谢,膜拜!!!另外,本系列文章知识可能需要有一定Android开发基础和项目经验的同学才能更好理解,也就是说该系列文章面向的是Android中高级开发工程师。


    第五篇了,,接着上一篇说


    终于到了我们的猪脚ViewRootImpl出场的时候了。ViewRootImpl类比较复杂,如果要把这个类全部解释清楚那需要很多章节,并且该类涉及了许多其他知识,如Android进程间通信的Binder了,还有其他许多本文以及前文没有讲到的概念。所以我们只分析其中的一部分。


    我们来看ViewRootImpl的构造函数

     public ViewRootImpl(Context context, Display display) {
          ...
    	  //① 从WindowManagerGlobal 中获取一个IWindowSession的实例。它是ViewRootImpl和WindowManagerService(以下简称WMS)进行通信的代理
          mWindowSession = WindowManagerGlobal.getWindowSession();
          //② FallbackEventHandler是一个处理未经任何人消费的输入事件的场所。
    	  mFallbackEventHandler = new PhoneFallbackEventHandler(context);
         ...
        }
    
    

    注:

    1. 关于IWindowSession
      它是一个Binder对象,真正的实现类是Session,也就是说下文setView方法中关于它的操作其实是一次IPC过程。关于IPC(进程间通信)的方式,以及Android操作系统中最主要的IPC方式Binder会在以后的文章中介绍。
    2. 关于FallbackEventHandler
      关于FallbackEventHandler具体我会在下一章介绍。

    我们再来看ViewRootImpl的setView函数

     public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
            synchronized (this) {
                if (mView == null) {
    				`//保存了控件的根
                    mView = view;
                    ...
    
                    mFallbackEventHandler.setView(view);
    
                    ...
                // ① 在添加窗口之前,先通过requestLayout方法在主线程上安排一次“遍历”。所谓“遍历”是指ViewRootImpl中的核心方法performTraversal()。这个方法实现对控件树进行测量、布局、向WMS申请修改窗口属性以及重绘的所有工作。
                    requestLayout();
    			// ② 初始化mInputChanel。InputChannel是窗口接收来自InputDispatcher的输入事件的管道。这部分内容我们将在下一篇介绍。
                    if ((mWindowAttributes.inputFeatures
                            & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
                        mInputChannel = new InputChannel();
                    }
                    ...
    
                    try {
    				//上文刚讲过mWindowSession是个Binder类,它的实现类是Session,将通过IPC远程调用(即调用另一个进程中的)Session的addToDisplay方法把窗口添加进WMS中。完成这个操作后,mWindow已经被添加到指定对象中而且mInputChannel(如果不为空)已经准备好接收事件
                        res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                                getHostVisibility(), mDisplay.getDisplayId(),
                                mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                                mAttachInfo.mOutsets, mInputChannel);
                    } catch (RemoteException e) {
    
                    } finally {
    
                    }
    
                    ...
    
                    if (res < WindowManagerGlobal.ADD_OKAY) {// 错误处理。窗口添加失败的原因通常是是权限问题、重复添加或者token无效
    
                    }
    
                    ...
    			// ③ 如果mInputChannel不为空,则创建mInputEventReceiver用于接收输入事件。
                    if (mInputChannel != null) {
    
                        mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
                                Looper.myLooper());
                    }
    
    				...
    
                    view.assignParent(this);
    
    	            ...
                }
            }
        }
    

    接着我们来一个个分析,先来最重要的,也是本章的最主要内容,另外两个将会在下一章分析。

    1. requestLayout()
    @Override
        public void requestLayout() {
            if (!mHandlingLayoutInLayoutRequest) {
                checkThread();
                mLayoutRequested = true;
                scheduleTraversals();
            }
        }
    

    scheduleTraversals();函数声明如下

    void scheduleTraversals() {
            if (!mTraversalScheduled) {
                mTraversalScheduled = true;
                mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
                mChoreographer.postCallback(
                        Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
                if (!mUnbufferedInputDispatch) {
                    scheduleConsumeBatchedInput();
                }
                notifyRendererOfFramePending();
                pokeDrawLockIfNeeded();
            }
        }
    

    其中mTraversalRunnable的定义是这样的

    final class TraversalRunnable implements Runnable {
            @Override
            public void run() {
                doTraversal();
            }
        }
    

    doTraversal()函数声明如下;

    void doTraversal() {
            if (mTraversalScheduled) {
                mTraversalScheduled = false;
                mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
    
                if (mProfile) {
                    Debug.startMethodTracing("ViewAncestor");
                }
    
                performTraversals();
    
                if (mProfile) {
                    Debug.stopMethodTracing();
                    mProfile = false;
                }
            }
        }
    

    注:以下文章多次摘抄于张大伟老师的《深入理解Android卷Ⅲ》,请支持原创,读者也可去看张大伟老师的这本书籍
    终于看到了我们的猪脚performTraversals();,ViewRootImpl中接收的各种变化,如来自WMS的窗口属性变化、来自控件树的尺寸变化以及重绘请求等都引发performTraversals();的调用,并在其中完成处理。View类及其子类的onMeasure()、onLayout()、onDraw()等回调也都是在该方法执行的过程中直接或间接的引发。该函数可谓是是ViewRootImpl的“心跳”。我们就来看一下这个方法把。
    先上源码:(注:源码很长,具体的分析在下方)

    private void performTraversals() {
            final View host = mView;
            /**
    	        第1阶段 预测量
            */
            boolean windowSizeMayChange = false;
            boolean newSurface = false;
            boolean surfaceChanged = false;
            WindowManager.LayoutParams lp = mWindowAttributes;
    		......
    		//声明本阶段的猪脚,这两个变量将是mView的SPEC_SIZE分量的候选
            int desiredWindowWidth;
            int desiredWindowHeight;
           ......
            Rect frame = mWinFrame;
           ......
            if (mFirst) {
                mFullRedrawNeeded = true;
                mLayoutRequested = true;
    
                final Configuration config = mContext.getResources().getConfiguration();
                if (shouldUseDisplaySize(lp)) {
                   //为状态栏设置desiredWindowWidth/height 其取值是屏幕尺寸
                    Point size = new Point();
                    mDisplay.getRealSize(size);
                    desiredWindowWidth = size.x;
                    desiredWindowHeight = size.y;
                } else {
                // ① 第1次“遍历”的测量,采用了应用可以使用的最大尺寸作为SPEC_SIZE的候选
                    desiredWindowWidth = dipToPx(config.screenWidthDp);
                    desiredWindowHeight = dipToPx(config.screenHeightDp);
                }
    			......
            } else {
    	        // ② 在非第1次遍历的情况下,会采用窗口的最新尺寸作为SPEC_SIZE的候选
                desiredWindowWidth = frame.width();
                desiredWindowHeight = frame.height();
                //如果窗口的最新尺寸与ViewRootImpl中的现有尺寸不同,说明WMS单方面改变了窗口的尺寸,将导致一下三个结果
                if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
                //需要完整的重绘以适应新的窗口尺寸
                    mFullRedrawNeeded = true;
                //需要对控件树重新布局
                    mLayoutRequested = true;
                //控件树可能拒绝接受新的窗口尺寸,可能需要窗口在布局阶段尝试设置新的窗口尺寸,,只是尝试
                    windowSizeMayChange = true;
                }
            }
    		......
            boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
            if (layoutRequested) {
    
                final Resources res = mView.getContext().getResources();
    
                if (mFirst) {
                  ......
                } else {
    		        ......//检查WMS是否单方面改变了一些参数,标记下来,然后作为后文是否进行控件布局的条件之一
    			//如果窗口的width或height被指定为WRAP_CONTENT时。表示该窗口为悬浮窗口。
                    if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
                            || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                        //悬浮窗口的尺寸取决于测量结果。因此有可能向WMS申请改变窗口的尺寸
                        windowSizeMayChange = true;
    
                        if (shouldUseDisplaySize(lp)) {
                           //一样的设置状态栏的desiredWindowWidth/height
                            Point size = new Point();
                            mDisplay.getRealSize(size);
                            desiredWindowWidth = size.x;
                            desiredWindowHeight = size.y;
                        } else {
                        // ③ 设置悬浮窗口的SPEC_SIZE的候选为应用可以使用的最大尺寸
                            Configuration config = res.getConfiguration();
                            desiredWindowWidth = dipToPx(config.screenWidthDp);
                            desiredWindowHeight = dipToPx(config.screenHeightDp);
                        }
                    }
                }
    
                // ④ 进行测量
                windowSizeMayChange |= measureHierarchy(host, lp, res,
                        desiredWindowWidth, desiredWindowHeight);
    	       
            }
    		......
            
           
            if (layoutRequested) {
               
                mLayoutRequested = false;
            }
    		......
    		//⑤ 判断窗口是否需要改变尺寸
            boolean windowShouldResize = 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));
            
    		......
            
            /**
    	         第1阶段 预测量到这里结束
    	    */
    
    		/**
    	         第2阶段 窗口布局阶段从这里开始
    	    */
          if (/*进入窗口布局的几个条件*/) {
    			......
    			 boolean hadSurface = mSurface.isValid();
    			 ......
    			  try {
    			      relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
    			      }catch(...){
    			      ......
    			      }finally{
    			      ......
    			      }
    		/**
    	         第2阶段 窗口布局阶段到这里结束。关于窗口布局的部分涉及太多,我们不具体分析源码,后文会有总结
    	    */
    	    /**
    	         第3阶段 最终测量阶段从这里开始
    	    */
                if (!mStopped || mReportNextDraw) {
                    ......
                        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    					//① 可以看到与与测量中调用的performMeasure
                        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                     
                        int width = host.getMeasuredWidth();
                        int height = host.getMeasuredHeight();
                        boolean measureAgain = false;
    					//② 判断LayoutParams.horizontalWeight和lp.verticalWeight ,以作为是否再次测量的依据
                        if (lp.horizontalWeight > 0.0f) {
                            width += (int) ((mWidth - width) * lp.horizontalWeight);
                            childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
                                    MeasureSpec.EXACTLY);
                            measureAgain = true;
                        }
                        if (lp.verticalWeight > 0.0f) {
                            height += (int) ((mHeight - height) * lp.verticalWeight);
                            childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
                                    MeasureSpec.EXACTLY);
                            measureAgain = true;
                        }
    
                        if (measureAgain) {
                            if (DEBUG_LAYOUT) Log.v(mTag,
                                    "And hey let's measure once more: width=" + width
                                    + " height=" + height);
                            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                        }
    
                        layoutRequested = true;
                    }
                }
            } else {
              
            }
    		/**
    	         第3阶段 最终测量阶段到这里结束
    	    */
    	    /**
    	         第4阶段 控件布局阶段从这里开始
    	    */
    	    //① 布局阶段的判断条件
            final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
          ......
            if (didLayout) {
    	        ......
    	        //② 通过performLayout对控件进行布局
                performLayout(lp, mWidth, mHeight);
    
               ......
               //③ 如果有必要,计算窗口的透明区域并把该区域设置给WMS
                if ((host.mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0) {
                    
                    host.getLocationInWindow(mTmpLocation);
                    mTransparentRegion.set(mTmpLocation[0], mTmpLocation[1],
                            mTmpLocation[0] + host.mRight - host.mLeft,
                            mTmpLocation[1] + host.mBottom - host.mTop);
    
                    host.gatherTransparentRegion(mTransparentRegion);
                    if (mTranslator != null) {
                        mTranslator.translateRegionInWindowToScreen(mTransparentRegion);
                    }
    
                    if (!mTransparentRegion.equals(mPreviousTransparentRegion)) {
                        mPreviousTransparentRegion.set(mTransparentRegion);
                        mFullRedrawNeeded = true;
                        
                        try {
                            mWindowSession.setTransparentRegion(mWindow, mTransparentRegion);
                        } catch (RemoteException e) {
                        }
                    }
                }
    
             
    		/**
    	         第4阶段 控件布局阶段到这里结束
    	    */
    		/**
    	         第5阶段 绘制阶段从这里开始
    	    */
    		......
            boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
    
            if (!cancelDraw && !newSurface) {
                if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                    for (int i = 0; i < mPendingTransitions.size(); ++i) {
                        mPendingTransitions.get(i).startChangingAnimations();
                    }
                    mPendingTransitions.clear();
                }
    
                performDraw();
            } else {
                if (isViewVisible) {
                    // Try again
                    scheduleTraversals();
                } else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                    for (int i = 0; i < mPendingTransitions.size(); ++i) {
                        mPendingTransitions.get(i).endChangingAnimations();
                    }
                    mPendingTransitions.clear();
                }
            }
    
            mIsInTraversal = false;
        }
    

    由于该方法是Android源代码中最庞大的方法之一,所以我们对其进行分阶段分析。在源码中有标注1,2,3,4,5,对每一阶段再细分为①②...,对照上文注释

    1. 预测量阶段(PreMeasure)。这是进入performTraversals();的第一个阶段。它会对控件树进行第一次测量。在此阶段中将会计算出控件树为显示其内容所需的尺寸,即期望的窗口尺寸。在这个阶段中View及其子类的onMeasure()方法将会沿着控件树依次得到回调。
    预测量和测量原理

    (1)预测量参数的候选(对应第1阶段①②③)


    预测量也是一次完整的测量过程,它与最终测量的区别仅在于参数不同而已。实际的测量工作是在View或其子类的onMeasure()方法中完成,并且其测量结果需要受限于来自其父控件的指示。这个指示由onMeasure()方法中的两个参数进行传达:widthSpec和heightSpec。它们是被称为MeasureSpec的复合整型变量,用于指导控件对自身进行测量。她又两个分量,结构如图
    这里写图片描述
    由①、②、③可知预测量时的SPEC_SIZE按照如下原则进行取值:
    - 第一次“遍历”时,使用可用的最大尺寸作为SPEC_SIZE的候选
    - 此窗口是一个悬浮窗口时,即LayoutParams.width/height其中之一被指定为WRAP_CONTENT时,使用可用的最大尺寸作为SPEC_SIZE的候选
    - 其他情况下,使用窗口最新尺寸作为SPEC_SIZE的候选

    (2)测量协商(对应第1阶段④)


    在第1阶段第④步时,我们看到了measureHierarchy方法,该方法用于测量整个控件树。传入的参数desiredWindowWidth,desiredWindowHeight在前述代码中做了精心的挑选。控件树本可以按照这两个参数完成测量,但是measureHierarchy有自己的考量,即如何将窗口布局的尽可能优雅。measureHierarchy如何做到这一步呢,通过跟控件树的协商。但是协商只发生在LayoutParams.width被指定为WRAP_CONTENT时,如果LayoutParams.width被指定为MATCH_PARENT或者固定数值时。该协商过程不会发生。我们来看一下measureHierarchy的源码。

      private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
                final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
    int childWidthMeasureSpec;
    int childHeightMeasureSpec;
    boolean windowSizeMayChange = false;//表示是否可能导致窗口的尺寸变化
    
    
    boolean goodMeasure = false;//表示侧脸是否能满足控件树充分显示内容的要求
    if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
       /**
       ① 第一次协商 measureHierarchy使用它期望的宽度限制进行测量,
       */
       final DisplayMetrics packageMetrics = res.getDisplayMetrics();
       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);
       }
       //如果宽度限制不为0并且传入的desiredWindowWidth 大于measureHierarchy期望的限制宽度,
       if (baseSize != 0 && desiredWindowWidth > baseSize) {
           childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
           childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
           //② 第一次测量 使用measureHierarchy期望的限制宽度 并得到状态
           performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
           //判断状态
           if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
               goodMeasure = true;//控件树对测量结果满意
           } else {
              //③ 控件树对测量结果不满意,进行第二次协商,这次把限制宽度放大为期望宽度baseSize和最大宽度desiredWindowWidth和的一半
             baseSize = (baseSize+desiredWindowWidth)/2;
             
              childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
              //④ 第2次测量
              performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
             
    		if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                  goodMeasure = true;
              }
           }
       }
    }
    //如果两次协商测量均不能让控件树满意,那么measureHierarchy不再对宽度进行限制,使用最大宽度进行测量
     if (!goodMeasure) {
         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;
    }
    
    
    我们再来看performMeasure方法,performMeasure方法的实现非常简单,它直接调用了mView.measure的方法
    
    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
            if (mView == null) {
                return;
            }
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
            try {
                mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
        }
    

    终于到了View的measure方法,在该方法内部会调用我们熟悉的onMeasure方法,我们来看View.measure方法的实现

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    .......//初始化操作
    if (forceLayout || needsLayout) {
    	  //① 准备工作
         mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
         .......
          //② 对本控价进行测量
         onMeasure(widthMeasureSpec, heightMeasureSpec);
    	.......
    	//③ 检查onMeasure的实现是否调用了setMeasuredDimension()
    	if ((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()");
         }
    	//④ 将PFLAG_LAYOUT_REQUIRED加入mPrivateFlags ,这一操作会对之后的布局操作放行
         mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
     }
    
     mOldWidthMeasureSpec = widthMeasureSpec;
     mOldHeightMeasureSpec = heightMeasureSpec;
    
    }
    

    (3)确定是否需要改变窗口尺寸(对应第1阶段⑤)

    前文多次设置了windowSizeMayChange 为true,但是windowSizeMayChange 为true尽是窗口是否需要改变尺寸的条件之一,我们来看第1阶段⑤对应代码。
    boolean windowShouldResize = 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));
    

    可以看到windowShouldResize 的判断较为复杂,我们来总结一下
    必要条件:
    layoutRequested为true。表示ViewRootImpl的requestLayout方法被调用过。在View中也有requestLayout方法。当控件内容发生变化从而需要调整其尺寸时,会调用自身的requestLayout(),并且此方法会沿着控件树向根部回溯,最终调用到ViewRootImpl的requestLayout,从而引发一次performTraversals()调用。之所以这是一个必要条件,是因为performTraversals()还有可能因为重绘时调用,当控件仅需要重绘而不需要重新布局时(例如背景色或者前景色发生变化时)。会通过invalidate()方法回溯到ViewRootImpl,此时不会通过requestLayout触发performTraversals()调用,而是通过scheduleTraversals()方法进行触发。这种情况下不需要进行布局窗口阶段
    windowSizeMayChange为true,该变量前文中已有详细描述。

    在上述条件满足的条件下,以下条件满足其一即触发布局窗口阶段
    ①测量结果与ViewRootImpl中所保存的当前尺寸有差异

    ②悬浮窗口的测量结果与窗口的最新尺寸有差异

    2. 布局窗口阶段(WindowLayout)。根据预测量的结果,通过IWindowSession.relayout()方法向WMS请求调整窗口的尺寸等属性,这将引发WMS对窗口重新布局,并将布局结果返回给ViewRootImpl.
    总结:布局窗口得以进行的原因是控件系统有修改窗口属性的需求,如第一次“遍历”需要确定窗口的尺寸以及一块Surface,预测量结果与窗口当前尺寸不一致需要进行窗口尺寸更改,mView可见性发生变化需要将窗口隐藏或显示等。

    3. 最终测量阶段(EndMeasure)。预测量的结果是控件树所期望的窗口尺寸。然而由于在WMS中影响布局的因素很多,WMS不一定会将窗口的准确的布局为控件树所要求的尺寸,而迫于WMS作为系统服务的强势地位,控件树不得不接受WMS的布局结果。在这个阶段中View及其子类的onMeasure()方法将会沿着控件树依次被回调。最终测量阶段直接调用performMeasure而不是measureHierarchy,是因为measureHierarchy有个协商过程,而到了最终测量阶段控件树已经没有了协商的余地,无论控件树乐意与否,他只能被迫接受WMS的布局结果

    4. 布局控件树阶段(Layout)。将上一步完成的最终测量的结果作为依据进行布局。测量确定的是控件的尺寸,而布局确定的是控件的位置。在这个阶段中View及其子类的onLayout()方法将会被回调。

    总体来说4. 布局控件树阶段(Layout)做了两件事。

    ① 进行控件树布局

    调用了performLayout函数,虽然我们还没看到该函数,但猜测想必和performMeasure差不多。我们来看一下
    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
                int desiredWindowHeight) {
          ......
            try {
            //一样是调用View.layout函数
                host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    
             ......
    
            } finally {
                
            }
            ......
    
        }
    

    我们再来看View.layout。布局阶段把测量结果转化为控件的实际位置与尺寸。而控件的实际位置与尺寸由Veiw的mLeft、mTop、mRight、mBottom 这4个成员变量存储的坐标值。即控件树的布局过程就是根据测量结果为每一个控件设置这4个成员变量的过程。其中mLeft、mTop、mRight、mBottom 是相对于父控件的坐标值。

    public void layout(int l, int t, int r, int b) {
    //如果设置了PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT标志位,那么在布局之前先进行测量,调用onMeasure函数
            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);
    //应该还记得上文View.measure方法中的最后设置了PFLAG_LAYOUT_REQUIRED吧
            if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            //调用onLayout方法。如果该Vie是个ViewGroup。onLayout中需要依次调用子控件的layout方法
                onLayout(changed, l, t, r, b);
    		......
    		//清除PFLAG_LAYOUT_REQUIRED标记
                mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
    		//通知每一个对此控件布局变化有兴趣的Listener
                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);
                    }
                }
            }
            ......
            }
        }
    

    我们来对比测量和布局阶段以便更好的理解

    • 测量确定的是控件的尺寸,并在一定程度上确定了子控件的位置。而布局是针对测量结果来实施,并最终确定子控件的位置
    • 测量结果对布局过程并没有约束力。虽说子控件在onMeasure方法中计算出了自己应有的尺寸,但是由于layout方法是由父控件调用,因此控件的位置尺寸的最终决定权在父控件手中,测量结果仅仅是一个参考。
    • 一般来说,子控件的测量结果影响父控件的测量结果,因此测量过程是后根遍历。而父控件的布局结果影响子控件的布局结果。所以布局过程是先根遍历

    ② 设置透明区域

    布局阶段的另一个工作是计算并设置窗口的透明区域。这一功能主要是为SurfaceView服务。关于SurfaceView的相关知识我们后文介绍 **5. 绘制阶段(Draw)**。这是**performTraversals();**的最后阶段。确定控件的尺寸和位置后。便进行对控件树的绘制。在这个阶段中View及其子类的onDraw()方法将会被回调。 我们在开发Android自定义控件时,往往都需要重写View.onDraw()方法以绘制内容到一个给定的Canvas中。 我们来看一下Canvas。Canvas是一个绘图工具类,其API提供了一系列绘图指定供开发者使用。这些指令可以分为两个部分:
    • 绘制指令。这些最常用的指令由一系列名为drawXXX()的方法提供。它们用来实现实际的绘制行为,例如绘制点、线、圆以及方块等
    • 辅助指令。这些用于提供辅助功能的指令将会影响后续指令的效果。如变换、裁剪区域等。这些辅助指令不如上面的绘制指令那么直观,但是在Android的绘制过程中大量使用了辅助指令。在这些辅助指令中,最常用的莫过于变换指令了。变换指令包括translate(平移坐标系),rotate(旋转坐标系),scale(缩放坐标系)等,这些指令很大的帮助了控件树的绘制。其实只要想一想我们在重写onDraw()函数时从未考虑过控件的位置、旋转、缩放等状态。这说明在onDraw()方法执行之前,这些状态都已经以变换的方式设置到Canvas中了。因此onDraw()方法中Canvas使用的是控件自身的坐标系。

    本篇总结
    本篇文章详细分析了ViewRootImpl的五大过程,ViewRootImpl比较复杂,尤其是它的“心跳”performTraversals();。希望读者能多看几遍上面的分析。相信你一定会有收获的


    下篇预告
    在下一篇文章中我们将进行实战项目,也是对我们前几篇文章的实际应用。老话说的好,纸上得来终觉浅,绝知此事要躬行。下一篇甚至几篇我们就来自定义View


    此致,敬礼

  • 相关阅读:
    freenas的踩坑记录:群晖CIFS挂载freeNas的smb共享目录
    【ikuai】爱快软路由上手
    Nacos集成Spring Cloud Gateway使用第三章:nacos配置中心
    Nacos集成Spring Cloud Gateway使用第二章:上手demo
    Nacos集成Spring Cloud Gateway使用第一章:理解解释
    函数指针数组
    回调函数
    使用C语言实现strcpy函数和字符串大小写交换
    Qt : Setting应用程序的数据保存和读取
    Qt 文件操作以及字体颜色选择
  • 原文地址:https://www.cnblogs.com/wangle12138/p/7839036.html
Copyright © 2011-2022 走看看