View的宽高获取与performTraversals

在安卓开发的过程中,可能有时候我们会碰到类似的需求:要求从代码中获取某个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函数已经执行完毕了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容