前言
日常开发中我们可能会遇到如下问题:
1、在onCreateonStrart()onResume()中获取View的宽高为0; 2、在onCreateonStrart()onResume()中直接调用Scroview.scrollTo(x,y)没有效果;
那么接下来一探究竟:
原因分析:
因为当onCreate()方法被调用的时候会通过LayoutInflater将xml文件填充到ContentView。 填充过程中只包括创建视图,不包括设置视图大小。而设置视图的大小和具体的位置则是通过布局层层遍历获得的。 如下图:
测量过程由measure(int , int)方法完成,该方法从上到下遍历视图树。在递归的过程中,每个视图都会向下层传递尺寸和规格,当measure方法遍历结束时,每个视图都保存了各自的尺寸信息。第二个过程由layout(int, int, int, int)方法完成,该方法也是由上而下遍历视图树。遍历过程中,每个父视图通过测量过程的结果定位所有姿势图的位置信息。
也就是说我们在onCreateonStrart()onResume()的时候我们并不知道什么时候布局测量完成,所以接下来我们去寻找一些方法。
解决方案:
方案一:View.Post()/View.PostDelay() [重点]
我们先来看下源码:
代码语言:javascript复制/**
* <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);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}
重点是这句话:
The runnable will be run on the user interface thread
Runnable是一个接口,不是一个线程,一般线程会实现Runnable。所以如果我们使用匿名内部类是运行在UI主线程的,如果我们使用实现这个Runnable接口的线程类,则是运行在对应线程的。
也就是说我们用View.post(Runnable action)方法里,View获得当前线程(即UI线程)的Handler,然后将action对象post到Handler里。 在Handler里,它将传递过来的action对象包装成一个Message(Message的callback为action),然后将其投入UI线程的消息循环中。 当Handler再次处理该Message时,已经在UI线程里,直接调用runnable的run方法。因此,我们可以毫无顾虑的来更新UI。
也就是说我们通过View.Post()/View.PostDelay()方法就可以实现获取view的宽高,并且Scroview.scrollTo(x,y)可以正常使用了。
代码语言:javascript复制view.post(new Runnable() {
@Override
public void run() {
//view的相关操作
}
});
所以个人推荐使用View.post()既方便又可以保证指定的任务在视图操作中顺序执行。
方案二:onWindowFocusChanged
使用如下:
代码语言:javascript复制@Override
public void onWindowFocusChanged(boolean hasFocus) {
//view的相关操作
}
我们看下官方注释:
代码语言:javascript复制/**
* Called when the current {@link Window} of the activity gains or loses
* focus. This is the best indicator of whether this activity is visible
* to the user. The default implementation clears the key tracking
* state, so should always be called.
*
* <p>Note that this provides information about global focus state, which
* is managed independently of activity lifecycles. As such, while focus
* changes will generally have some relation to lifecycle changes (an
* activity that is stopped will not generally get window focus), you
* should not rely on any particular order between the callbacks here and
* those in the other lifecycle methods such as {@link #onResume}.
*
* <p>As a general rule, however, a resumed activity will have window
* focus... unless it has displayed other dialogs or popups that take
* input focus, in which case the activity itself will not have focus
* when the other windows have it. Likewise, the system may display
* system-level windows (such as the status bar notification panel or
* a system alert) which will temporarily take window input focus without
* pausing the foreground activity.
*
* @param hasFocus Whether the window of this activity has focus.
*
* @see #hasWindowFocus()
* @see #onResume
* @see View#onWindowFocusChanged(boolean)
*/
public void onWindowFocusChanged(boolean hasFocus) {
}
该方法会在view绘制完成之后调用,所以我们在这个时候去获取view宽高,或者Scroview.scrollTo(x,y)都可以正常运行了。 但是该方法如原注释所说,当Activity的窗口得到焦点和失去焦点时均会被调用一次,如果频繁地进行onResume和onPause,那么onWindowFocusChanged也会被频繁地调用。 所以也要结合具体业务场景。
方案三:ViewTreeObserver
使用ViewTreeObserver的众多回调可以完成这个功能,比如使用OnGlobalLayoutListener这个接口,当view树的状态发生改变或者view树内部的view的可见性发生改变时,onGlobalLayout方法将被回调,因此这是获取view的宽高一个很好的时机。需要注意的是,伴随着view树的状态改变等,onGlobalLayout会被调用多次。
代码语言:javascript复制view.getViewTreeObserver().addOnGlobalFocusChangeListener(new ViewTreeObserver.OnGlobalFocusChangeListener() {
@Override
public void onGlobalFocusChanged(View oldFocus, View newFocus) {
//view的相关操作
}
});
我们看一下View官方注释:
代码语言:javascript复制/**
* A view tree observer is used to register listeners that can be notified of global
* changes in the view tree. Such global events include, but are not limited to,
* layout of the whole tree, beginning of the drawing pass, touch mode change....
*
* A ViewTreeObserver should never be instantiated by applications as it is provided
* by the views hierarchy. Refer to {@link android.view.View#getViewTreeObserver()}
* for more information.
*/
public final class ViewTreeObserver {
//代码省略。。。
}
ViewTreeObserver这个类,这个类是用来注册当view tree全局状态改变时的回调监听器,这些全局事件包括很多,比如整个view tree视图的布局,视图绘制的开始,点击事件的改变等等。还有千万不要在应用程序中实例化ViewTreeObserver对象,因为该对象仅是由视图提供的。
综上,个人比较推荐方案一:View.Post()/View.PostDelay() 。