在安卓開發的過程中,可能有時候我們會碰到類似的需求:要求從代碼中獲取某個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函數已經執行完畢了。