美文网首页SurfaceFlinger
View的宽高获取与performTraversals

View的宽高获取与performTraversals

作者: Mr_villain | 来源:发表于2016-08-05 14:54 被阅读282次

    在安卓开发的过程中,可能有时候我们会碰到类似的需求:要求从代码中获取某个View的高度,然后根据这个高度来设置其他的View的高度等等类似的事情。刚接触安卓开发的同学碰到这样的需求,可能会很想当然的在onCreate中写下如下的代码:

    @Override
    protected void onCreate(Bundle arg){
      super.onCreate(arg);
      setContentView(R.layout.layoutId);
      View view = findViewById(R.id.viewId);
      int height = view.getMeasuredHeight();
    }
    

    然后运行代码之后,会发现,获取的View的高度为零。原因很简单,就是当Activity处于onCreate这个阶段时,View还没开始测量高度和布局,setContentView只是简单地把布局文件转为View对象,然后添加到DecorView之中。
    那么该如何才能正确的获取View的高度呢?以下介绍三个方法。

    第一种

    根据View的生命周期,我们知道,View的宽高只有等到setMeasuredDimension函数调用之后,才能被正确获取,而这个函数是在onMeasure函数中被调用的,所以我们可以继承View,然后添加一个接口,在onMeasure中回调这个接口,将宽高传到Activity中。
    缺点:麻烦

    第二种

    使用onGlobalLayoutListener

    view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {    
        @Override    
        public void onGlobalLayout() {        
        int height = view.getMeasuredHeight();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {      
            view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
        }else{    
            view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
        }
      }
    });
    

    缺点:据组里的一个大神说,使用这个接口,改变View的高度时,会发生屏幕闪动的情况(真实性有待考证)。

    第三种

    使用View.post(Runnable)方法。

      view.post(new Runnable() {    
          @Override   
          public void run() {       
              int height = view.getMeasuredHeight();    
          }
      });
    

    这个方法最为简洁,我们只需要在onCreate方法调用这个代码就可以正确获取View的尺寸信息了。

    View.post方法分析

    接下来我们来查看View.post方法的源码

    /** 
     * <p>Causes the Runnable to be added to the message queue. 
     * The runnable will be run on the user interface thread.</p> 
     * 
     * @param action The Runnable that will be executed. 
     * 
     * @return Returns true if the Runnable was successfully placed in to the 
     *         message queue.  Returns false on failure, usually because the 
     *         looper processing the message queue is exiting. 
     * 
     * @see #postDelayed 
     * @see #removeCallbacks 
     */
    public boolean post(Runnable action) {    
        final AttachInfo attachInfo = mAttachInfo;    
        if (attachInfo != null) {        
            return attachInfo.mHandler.post(action);    
        }    // Assume that post will succeed later    
        ViewRootImpl.getRunQueue().post(action);    
        return true;
    }
    

    因为我们是在onCreate中调用这个方法,此时mAttachInfo(这货是啥后面会说到(Maybe))还是null,所以会直接进入getRunQueue().post(action);

    先来看看getRunQueue

    static RunQueue getRunQueue() {    
        RunQueue rq = sRunQueues.get();    
        if (rq != null) {        
            return rq;    
        }    
        rq = new RunQueue();    
        sRunQueues.set(rq);    
        return rq;
    }
    

    这个函数主要做的,就是从sRunQueues这个静态变量中获取RunQueue对象,如果RunQueue对象为空,那么就new一个,并存到sRunQueues中。

    sRunQueues

    static final ThreadLocal<RunQueue> sRunQueues = new ThreadLocal<RunQueue>();
    

    原来它是个ThreadLocal对象。有看过Looper源码的小伙伴一定对这个很熟悉了。Looper类中也有一个ThreadLocal的静态变量,用来存储当前线程的Looper对象。

    RunQueue.post

    /** 
    * The run queue is used to enqueue pending work from Views when no Handler is 
    * attached.  The work is executed during the next call to performTraversals on 
    * the thread. 
    * @hide 
    */
    static final class RunQueue {   
        private final ArrayList<HandlerAction> mActions = new ArrayList<HandlerAction>();   
        void post(Runnable action) {       
            postDelayed(action, 0);   
        }
        ···省略无数代码···
        private static class HandlerAction {    
            Runnable action;    
            long delay;   
         ···省略无数代码···
        }
    }
    

    通过注释给的信息,我们可以知道,这个RunQueue类,是用来当View还没有AttachInfo时,存储Runnable的。RunQueue中存储的Runnable会在下一次performTraversals函数被执行时,被运行。
    接下来我们就跳到performTraversals函数中来看看RunQueue。

    performTraversals

    这个函数在ViewRootImpl类中

    该函数就是android系统View树遍历工作的核心。一眼看去,发现这个函数挺长的,但是逻辑是非常清晰的,其执行过程可简单概括为根据之前所有设置好的状态,判断是否需要计算视图大小(measure)、是否需要重新安置视图的位置(layout),以及是否需要重绘(draw)视图,可以用以下图来表示该流程。——performTraversals源码分析

    部分函数源码
    private void performTraversals() {    
        // cache mView since it is used so much below...
        //mView为DecorView,继承自FrameLayout,所以是个ViewGroup
        final View host = mView;    
        //省略部分源码 
        if (mFirst) {        
            //省略部分源码
            //在这个地方,将调用ViewGroup的dispatch方法,将mAttachInfo递归地传递给子View,在这之后,
            //调用View.post方法时,mAttachInfo才不会为null;
            host.dispatchAttachedToWindow(mAttachInfo, 0);        
       //省略部分源码  
       } else {        
       //省略部分源码   
       }    
       //省略部分源码
       // Execute enqueued actions on every traversal in case a detached view enqueued an action    
       //这里,之前我们post的runnable被取出来执行             
       getRunQueue().executeActions(mAttachInfo.mHandler);    
       if (mFirst || windowShouldResize || insetsChanged ||viewVisibilityChanged || params != null) {        
           if (viewVisibility == View.VISIBLE) {                    
               insetsPending = computesInternalInsets && (mFirst || viewVisibilityChanged);        
           }        
       if (mSurfaceHolder != null) {            
           mSurfaceHolder.mSurfaceLock.lock();           
           mDrawingAllowed = true;        
       }        
       boolean hwInitialized = false;        
       boolean contentInsetsChanged = false;        
       boolean hadSurface = mSurface.isValid();        
       if (!mStopped) {            
           boolean focusChangedDueToTouchMode = ensureTouchModeLocally((relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);            
           if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth() || mHeight !=
               host.getMeasuredHeight() || contentInsetsChanged) {                
               //这里我们第一次碰到了measure相关                
               // Ask host how big it wants to be                
               performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);                
              // Implementation of weights from WindowManager.LayoutParams                
              // We just grow the dimensions as needed and re-measure if                
              // needs be                
              int width = host.getMeasuredWidth();                
              int height = host.getMeasuredHeight();                
              boolean measureAgain = false;                
              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) {          
                  performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);                
              }                
              layoutRequested = true;            
              }        
        }    
    } else {       
    //省略   
    }    
       final boolean didLayout = layoutRequested && !mStopped;    
       boolean triggerGlobalLayoutListener = didLayout || mAttachInfo.mRecomputeGlobalAttributes;    
       if (didLayout) {        
           //省略        
           //这里开始layout        
           performLayout(lp, desiredWindowWidth, desiredWindowHeight);   
       }    
       //省略    
       //这里调用onGlobalLayoutListener回调    
       if (triggerGlobalLayoutListener) {        
           mAttachInfo.mRecomputeGlobalAttributes = false;    
           mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();    
       }   
       //省略部分源码    
       if (!cancelDraw && !newSurface) {       
           if (!skipDraw || mReportNextDraw) {           
               //省略部分源码           
               //这里开始执行Draw操作            
               performDraw();       
           }   
       } else {       
           if (viewVisibility == View.VISIBLE) {            
               // Try again            
               //这里将再次调用一次performTraversals()           
               scheduleTraversals();       
           } else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {           
                    //省略      
           }   
       }   
       //省略
    }
    

    有个十分重要的点是,performTraversals函数的执行,和Activity的生命周期并非是同步的,即我们没法保证在哪个Activity的生命周期函数中,performTraversals函数已经执行完毕了。

    相关文章

      网友评论

        本文标题:View的宽高获取与performTraversals

        本文链接:https://www.haomeiwen.com/subject/vtehsttx.html