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閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容