在安卓开发的过程中,可能有时候我们会碰到类似的需求:要求从代码中获取某个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方法的源码
/**
* Causes the Runnable to be added to the message queue.
* The runnable will be run on the user interface thread.
*
* @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 sRunQueues = new ThreadLocal();
原来它是个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 mActions = new ArrayList();
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函数已经执行完毕了。