1. 简介
翻看之前的博客,深度解析源码 onCreate() 和 onResume() 中获取不到View的宽高值?在文章中通过分析源码解析了获取不到 View 宽高值的原因,在文章结尾处留了一个问题,是打算后面继续分析解读的,但是却给忘了,欠下的总归是要弥补的,因此这里来补上,本文就来深度详解 View.post() 为何能够获取到 View 的宽高值?
1.1 问题描述
首先,承接之前文章提出的问题,下面三处打印输出的结果是什么呢?带着问题思考一下,然后猜测一下输出结果,之后我们再带着问题去探寻源码。
class TestViewPostActivity : Activity() {
private val TAG: String = TestViewPostActivity::class.java.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_view_post)
btnViewPost.apply {
Log.e(TAG, "打印输出日志 1---> onCreate() 获取View的测量宽度:" + this.width)
}
btnViewPost.post(Runnable {
Log.e(TAG, "打印输出日志 2---> onCreate() 中 通过 post 方法获取View的测量宽度:" + btnViewPost.width)
})
}
override fun onResume() {
super.onResume()
btnViewPost.apply {
Log.e(TAG, "打印输出日志 3---> onResume() 获取View的测量宽度:" + this.width)
}
}
}
1.2 结果展示
来看一下输出的日志结果,看看给你的答案一致不? 从图上的打印输出可以看到,onCreate() 和 onResume() 方法中是获取不到 View 的测量值的,在 onCreate() 方法中通过 View # post() 方法可以获取到 View 的测量值。
和你的答案一致不?如果一致且能说出所以然,说明你对 Activity 的生命周期流程、View 绘制流程、以及 View 是如何关联到 Window 的流程都有了一定的理解。不一致且心中疑惑不解的,没关系,下面就来一起学习吧!
2. 源码分析
关于 onCreate() 和 onResume() 方法中获取不到 View 的测量值,可参考深度解析源码 onCreate() 和 onResume() 中获取不到View的宽高值?
这里简述一下,从 Activity 的启动到界面显示出来的过程中,View 绘制流程的开始时机是在 ActivityThread 的 handleResumeActivity() 方法,在该方法首先完成 Activity 生命周期 ** Activity # onResume()** 方法回调,然后开始 View 绘制任务。也就是说 View 绘制流程要在 Activity # onResume() 方法之后,但绝大部分业务是在 Activity # onCreate() 方法,比如要获取某个 View 的实际宽高值,由于 View 的绘制任务还未开始,所以就无法正确获取到。
关于 View # post() 方法为何能够获取到 View 的宽高值,带着问题探寻源码吧…
2.1 View # post() 方法添加任务
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
......
AttachInfo mAttachInfo;
......
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
getRunQueue().post(action);
return true;
}
......
}
执行流程如下:
- 获取 mAttachInfo 信息,并判断其值是否为空,如不为空,则获取其内部的 mHandler 对象,并通过获取到的 mHandler 对象的 post() 方法将待执行的 Runnable 添加到其内部的 MessageQueen 中等待执行。
- 如果为空,则将待执行的 Runnable 加入当前 View 的待执行队列,保存待执行 Runnable 对象,直到找到它需要在哪个线程上执行。注意注释:注释很有用哈,假定待执行对象将放置在 attach 之后执行,实际上也确实是,后面会分析。
通过上述分析可知,mAttachInfo 实例是否有值对流程走向起到决定性作用,那么问题来了哦,此时 mAttachInfo 实例到底有没有值呢?
先给出结论,下文具体分析,mAttachInfo 是 View 中的一个类型为 AttachInfo 的成员变量,每一个被添加到 Window 中的 View 都会持有一个 AttachInfo 实例。该 AttachInfo 实例是在 ViewRootImpl 的构造方法中构建的,在 ViewRootImpl # performTraversals() 方法中经过一些判断赋值后,由 View # dispatchAttachedToWindow() 方法将 AttachInfo 对象传递给 View 并赋值给 mAttachInfo。由于在深度解析源码 onCreate() 和 onResume() 中获取不到View的宽高值文章中分析过,在 Activity # onCreate() 方法中,View 还未被添加到 Window 中,也没有开始执行 View 的测量、布局及绘制流程等,所以此时的 mAttachInfo 还未被赋值。
2.2 HandlerActionQueue.post() 方法添加任务
由于 mAttachInfo 此时还是空值,跟踪查看 getRunQueue().post(action) 方法,首先来看一下 getRunQueue() 方法,代码如下:
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
......
private HandlerActionQueue mRunQueue;
......
private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}
......
}
该方法返回的是 HandlerActionQueue 类型的队列,当 View 还没有关联到 Handler 时,将添加到 View 的待执行对象 Runnable 加入到该等待队列,待合适的时机交给 Handler 来处理。
继续来看 HandlerActionQueue # post() 方法,代码如下:
public class HandlerActionQueue {
......
private HandlerAction[] mActions;
......
public void post(Runnable action) {
postDelayed(action, 0);
}
public void postDelayed(Runnable action, long delayMillis) {
final HandlerAction handlerAction = new HandlerAction(action, delayMillis);
synchronized (this) {
if (mActions == null) {
mActions = new HandlerAction[4];
}
mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
mCount++;
}
}
......
}
将传入的待执行对象 Runnable 封装成 HandlerAction 待执行的任务,HandlerAction 内部持有待执行的 Runnable 对象和延迟执行的时间。并将封装的 HandlerAction 对象保存在长度为 4 的 HandlerAction 数组 mActions 中,该数组用于保存添加的待执行任务。
分析完流程的第2部分,只是将 View # post() 方法添加的待执行对象 Runnable,封装成 HandlerAction 待执行任务,保存在 HandlerAction 数组 mActions 中,没有执行任务的入口。
2.3 探究 AttachInfo 的由来
现在回头来分析流程的第1部分,mAttachInfo 已被赋值的情况,在 2.1 View # post() 方法添加任务 中有分析,mAttachInfo 是 View 被添加到 Window 后,在创建 ViewRootImpl 实例对象时,在 ViewRootImpl 构造函数中创建的 mAttachInfo 实例对象,然后通过 ViewRootImpl # performTraversals() 方法中经过一些判断赋值后,由 View # dispatchAttachedToWindow() 方法将 AttachInfo 对象传递给 View 并赋值给 mAttachInfo。
时序图如下所示:
2.3.1 AttachInfo 类
在详细分析流程前,先来查看一下 AttachInfo 这个内部类,代码如下:
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
......
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
AttachInfo mAttachInfo;
......
final static class AttachInfo {
......
AttachInfo(IWindowSession session, IWindow window, Display display,
ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer,
Context context) {
mSession = session;
mWindow = window;
mWindowToken = window.asBinder();
mDisplay = display;
mViewRootImpl = viewRootImpl;
mHandler = handler;
mRootCallbacks = effectPlayer;
mTreeObserver = new ViewTreeObserver(context);
}
}
......
}
AttachInfo 是 View 中的一个 final 修饰的静态内部类,通过其构造函数可以看到,其内部存储了当前 View Hierachy 控件树所绑定的 Window 的各种有用的信息,并且会派发给 View Hierachy 控件树中的每一个 View,保存在每个 View 自己的 mAttachInfo 变量中。
注意: 其持有的 mHandler 是 ViewRootHandler 对象,用来处理需要在 UI 主线程执行的操作,如:来自于系统进程 WMS 的 View 刷新、隐藏等消息事件。
2.3.2 ViewRootImpl 类
通过前面的分析可知,mAttachInfo 实例对象是在 ViewRootImpl 构造函数中创建的,继续看一下 ViewRootImpl 类的构造方法,代码如下:
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks,
AttachedSurfaceControl {
......
boolean mFirst;
final ViewRootHandler mHandler = new ViewRootHandler();
......
public ViewRootImpl(Context context, Display display) {
this(context, display, WindowManagerGlobal.getWindowSession(),
false );
}
public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session,
boolean useSfChoreographer) {
mContext = context;
mWindowSession = session;
mDisplay = display;
......
mThread = Thread.currentThread();
......
mWindow = new W(this);
......
mFirst = true;
mPerformContentCapture = true;
mAdded = false;
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
context);
......
}
......
}
调用 ViewRootImpl 的构造方法来构建实例时,保存 Context、IWindowSession 等信息,并使用这些信息创建 View.AttachInfo 实例对象。
ViewRootImpl 实现了 ViewParent 接口,作为整个 View Hierachy 控件树的根部,View 测量、布局及绘制等都由 ViewRootImpl 触发。另一方面,它是 WindowManagerGlobal 工作的实际实现者,还需要负责与 WMS 交互通信以调整 Window 的位置大小,以及对来自 WMS 的事件(如 Window 尺寸改变等)作出相应的处理,所以说这个类很重要。
2.3.3 ActivityThread # handleResumeActivity() 方法
结合上面的时序图来探索 View 是如何添加到当前页面 Activity 的 Window 中的,其添加时机在 ActivityThread 的 handleResumeActivity() 方法中,并且在 Activity # onResume() 方法回调之后,代码如下:
public final class ActivityThread extends ClientTransactionHandler {
......
@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
......
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
......
final Activity a = r.activity;
......
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l);
}
......
}
}
......
}
}
获取当前 Activity 的 Window 实现类 PhoneWindow,然后获取 PhoneWindow 的顶层 View,这里指的是 DecorView。通过当前 Activity 的 WindowManager 实现类 WindowManagerImpl 添加获取到的顶层 DecorView。
一般情况下,每个 Activity 都有一个关联的 Window 对象,由 WindowManager 负责管理。Window 本身更倾向于一个抽象的概念,它具体的实体以 View 的形式存在。PhoneWindow 是 Window 抽象类的唯一实现类,其内部包含一个 DecorView 对象,用来承载添加到 Window 中的 View。通过 Activity # setContentView() 方法设置的布局将添加到 DecorView 的布局 ID 为 content 的容器中,以此构建一个包含多个 View 的 View Hierachy 控件树。
2.3.4 WindowManagerImpl # addView() 方法
public final class WindowManagerImpl implements WindowManager {
@UnsupportedAppUsage
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
private final Context mContext;
private final Window mParentWindow;
private IBinder mDefaultToken;
......
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
}
WindowManagerImpl 是 WindowManager 接口的实现类,虽是接口的实现类,但功能实现都委托给 WindowManagerGlobal 来完成。所以继续通过 WindowManagerGlobal 的 addView() 方法添加 DecorView,继续查看代码流程。
2.3.5 WindowManagerGlobal # addView() 方法
public final class WindowManagerGlobal {
......
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
......
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
......
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
......
}
创建 ViewRootImpl 实例对象,并调用该对象的 setView() 方法把 DecorView 传给 ViewRootImpl,继续查看代码流程。
WindowManagerGlobal 是一个 final 修饰的类,没有继承实现任何一个类、接口,是 WindowManager 功能的最终实现者。维护了当前进程中所有已经添加到系统中的 Window 的信息。注意:在一个进程中仅有一个 WindowManagerGlobal 的实例。
2.3.6 ViewRootImpl # setView() 方法
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks,
AttachedSurfaceControl {
......
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
synchronized (this) {
if (mView == null) {
mView = view;
......
mSoftInputMode = attrs.softInputMode;
mWindowAttributesChanged = true;
mAttachInfo.mRootView = view;
mAttachInfo.mScalingRequired = mTranslator != null;
mAttachInfo.mApplicationScale =
mTranslator == null ? 1.0f : mTranslator.applicationScale;
......
mAdded = true;
requestLayout();
......
try {
......
res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId,
mInsetsController.getRequestedVisibility(), inputChannel, mTempInsets,
mTempControls);
......
}
......
view.assignParent(this);
}
}
}
......
}
将传入的 DecorView 赋值给 mView 保存,调用 ViewRootImpl # requestLayout() 方法,在向 Window 中添加顶层 View 之前,先通过 ViewRootImpl # requestLayout() 方法在 UI 主线程中安排一次“遍历”,所谓“遍历”是指 ViewRootImpl 中的核心方法 performTraversal(),这个方法实现对 View Hierachy 控件树进行测量、布局、向 WMS 申请修改 Window 属性以及重绘的所有工作,继续查看代码流程。
2.3.7 ViewRootImpl # requestLayout() 方法
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks,
AttachedSurfaceControl {
......
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
......
performTraversals();
......
}
}
......
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
}
代码执行流程如下:
- ViewRootImpl # requestLayout() 方法中,调用 ViewRootImpl # scheduleTraversals() 方法来调度安排 TraversalRunnable 的执行。
- ViewRootImpl # scheduleTraversals() 方法中,通过 MessageQueue # postSyncBarrier() 方法为 UI 主线程的 Handler 添加同步屏障消息,然后通过 Choreographer # postCallback() 方法提交一个任务,mTraversalRunnable 是待执行的任务回调,有了同步屏障消息 mTraversalRunnable 在下一次 VSync 信号到来时就会被优先执行,详情可参考文章 Handler 之同步屏障机制与 Android 的屏幕刷新机制的解析。
- 调用 ViewRootImpl # doTraversal() 方法,执行 mTraversalRunnable 任务,首先通过 MessageQueue # removeSyncBarrier() 方法移除同步屏障消息,传入 MessageQueue # postSyncBarrier() 方法的返回值作为参数标识需要移除哪个屏障,然后将该屏障消息会从 MessageQueue 中移除,以确保消息队列恢复正常操作,然后调用 ViewRootImpl # performTraversals() 方法执行 View 的绘制流程开始渲染页面。
友情提示: 参考 Android 屏幕刷新机制之 Choreographer 可深入 Native 层进一步探索 Android 的屏幕刷新机制,笔者将带你深入底层去探索。
2.3.8 ViewRootImpl # performTraversals() 方法
private void performTraversals() {
final View host = mView;
mIsInTraversal = true;
......
if (mFirst) {
......
mAttachInfo.mWindowVisibility = viewVisibility;
host.dispatchAttachedToWindow(mAttachInfo, 0);
......
}
......
getRunQueue().executeActions(mAttachInfo.mHandler);
......
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
......
final boolean didLayout = layoutRequested && (!mStopped || wasReportNextDraw);
if (didLayout) {
performLayout(lp, mWidth, mHeight);
......
}
......
mFirst = false;
......
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
if (!cancelDraw) {
......
performDraw();
}
......
mIsInTraversal = false;
}
执行流程如下:
- 获取 mView 中保存的 DecorView,由于创建 ViewRootImpl 实例对象时在其构造方法中将 mFirst 标志位置为 true,此时由于是第一次遍历,将调用 DecorView # dispatchAttachedToWindow() 方法并传入 mAttachInfo 实例,为 DecorView 中的每个 View 传递 mAttachInfo 关联信息。注意:mAttachInfo 实例也是在构建 ViewRootImpl 实例时创建的。
- 执行 View 的测量、布局操作后,将 mFirst 标志位置为 false,对于同一个 View Hierachy 控件树中的 View,后续再调用 ViewRootImpl # performTraversals() 方法时,则不再调用 DecorView # dispatchAttachedToWindow() 方法。
- 如果没有取消 View 的绘制,调用 ViewRootImpl # performDraw() 方法执行 View 的绘制,这不是本文的分析重点哈。
2.3.9 ViewGroup # dispatchAttachedToWindow() 方法
由上面的分析可知,host 的实际类型是继承自 FrameLayout 的 DecorView,即 DecorView 本质上是一个 ViewGroup,且由于 DecorView 以及 FrameLayout 都没有重写 dispatchAttachedToWindow() 方法,因此调用的是其父类 ViewGroup # dispatchAttachedToWindow() 方法,代码如下:
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
......
@Override
@UnsupportedAppUsage
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
super.dispatchAttachedToWindow(info, visibility);
mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
final View child = children[i];
child.dispatchAttachedToWindow(info,
combineVisibility(visibility, child.getVisibility()));
}
final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
for (int i = 0; i < transientCount; ++i) {
View view = mTransientViews.get(i);
view.dispatchAttachedToWindow(info,
combineVisibility(visibility, view.getVisibility()));
}
}
......
}
遍历并获取 ViewGroup 的每个子 View,调用 View # dispatchAttachedToWindow() 方法为每个子 View 传递 AttachInfo 关联信息。
2.3.10 View # dispatchAttachedToWindow() 方法
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
......
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
if (mOverlay != null) {
mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility);
}
mWindowAttachCount++;
......
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
performCollectViewAttributes(mAttachInfo, visibility);
onAttachedToWindow();
ListenerInfo li = mListenerInfo;
final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =
li != null ? li.mOnAttachStateChangeListeners : null;
if (listeners != null && listeners.size() > 0) {
for (OnAttachStateChangeListener listener : listeners) {
listener.onViewAttachedToWindow(this);
}
}
......
onVisibilityChanged(this, visibility);
......
}
.....
}
该方法中,首先为当前 View 的 mAttachInfo 赋值,然后调用 mRunQueue # executeActions() 方法执行队列中保存的待执行任务,注意: mRunQueue 类型是 HandlerActionQueue,其内部保存的是通过 View # post() 方法添加的待执行任务。
2.3.11 HandlerActionQueue # executeActions() 方法
public void executeActions(Handler handler) {
synchronized (this) {
final HandlerAction[] actions = mActions;
for (int i = 0, count = mCount; i < count; i++) {
final HandlerAction handlerAction = actions[i];
handler.postDelayed(handlerAction.action, handlerAction.delay);
}
mActions = null;
mCount = 0;
}
}
遍历 mActions 中保存的所有待执行任务,并通过 ViewRootHandler 的 postDelayed() 方法添加到其 MessageQueen 中排队执行,最后将保存任务的 mActions 置为 null,因为后续通过 View # post() 方法添加的待执行任务将直接添加到 AttachInfo 持有的 ViewRootHandler 中( 因为 mAttachInfo 已被赋值 )。
3. 总结
经过上面对源码的深入探索,结合时序图,来总结回答一下问题:为何 View # post() 方法能够获取到 View 的宽高值?
经过分析得出结果如下:
首先,通过 View # post() 方法添加待执行任务 Runnable,在 View 还未添加到 Window 之前,此时 mAttachInfo 还未被赋值,因此先通过 HandlerActionQueue # post() 方法将待执行的 Runnable 封装成 HandlerAction,然后将封装的 HandlerAction 对象保存在长度为 4 的 HandlerAction 数组 mActions 中,等待时机执行。
其次,在 Activity # onResume() 生命周期方法执行过后,调用 WindowManager # addView() 方法将 View 添加到 Window 中,通过前面结合时序图进行的源码分析可知,在 ViewRootImpl # performTraversals() 方法中,如果 mFirst 为 true,即当前 Window 窗口中第一次添加 View(注意:这里指的是 DecorView),将调用 DecorView # dispatchAttachedToWindow() 方法,DecorView 继承自 FrameLayout 是一个 ViewGroup,因此这里会继续调用父类 ViewGroup # dispatchAttachedToWindow() 方法,遍历获取每个子 View,并调用 View # dispatchAttachedToWindow() 方法为每个子 View 设置 AttachInfo 关联信息。
最后,在 View # dispatchAttachedToWindow() 方法中,为当前 View 的 mAttachInfo 赋值,然后调用 mRunQueue # executeActions() 方法执行 mActions 数组中通过 View # post() 方法添加的待执行任务,这里所谓的执行,其实是通过 mAttachInfo 内持有的 ViewRootHandler 的 postDelayed() 方法将待执行任务添加到 UI 主线程的 MessageQueen 中排队执行(排在 View 的测量、布局和绘制任务的后面)。
注意: 此时 UI 主线程中已经在执行 ViewRootImpl # performTraversals() 方法中 View 的测量、布局和绘制操作,并且移除了 MessageQueen 中的同步屏障消息以恢复消息队列正常的循环,所以通过 View # post() 方法添加的待执行任务是在 View 的测量、布局和绘制操作之后才会执行,因此能够获取到 View 的宽高值。
交流:如有分析错误或者别的理解,还望留言或者私信笔者讨论,共同学习。
|