View.Post()保证UI带你装逼带你飞

333 阅读5分钟

前言

日常开发中我们可能会遇到如下问题:

1、在onCreate\onStrart()\onResume()中获取View的宽高为0; 2、在onCreate\onStrart()\onResume()中直接调用Scroview.scrollTo(x,y)没有效果;

那么接下来一探究竟:

原因分析:

因为当onCreate()方法被调用的时候会通过LayoutInflater将xml文件填充到ContentView。
填充过程中只包括创建视图,不包括设置视图大小。而设置视图的大小和具体的位置则是通过布局层层遍历获得的。
如下图:

测量过程由measure(int , int)方法完成,该方法从上到下遍历视图树。在递归的过程中,每个视图都会向下层传递尺寸和规格,当measure方法遍历结束时,每个视图都保存了各自的尺寸信息。第二个过程由layout(int, int, int, int)方法完成,该方法也是由上而下遍历视图树。遍历过程中,每个父视图通过测量过程的结果定位所有姿势图的位置信息。

也就是说我们在onCreate\onStrart()\onResume()的时候我们并不知道什么时候布局测量完成,所以接下来我们去寻找一些方法。

解决方案:

方案一:View.Post()/View.PostDelay() [重点]

我们先来看下源码:

/**
 * <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)可以正常使用了。

view.post(new Runnable() {
        @Override
        public void run() {
        //view的相关操作
    }
});

所以个人推荐使用View.post()既方便又可以保证指定的任务在视图操作中顺序执行。

方案二:onWindowFocusChanged

使用如下:

@Override
   public void onWindowFocusChanged(boolean hasFocus) {
    //view的相关操作
}

我们看下官方注释:

/**
 * 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会被调用多次。

view.getViewTreeObserver().addOnGlobalFocusChangeListener(new ViewTreeObserver.OnGlobalFocusChangeListener() {
    @Override
    public void onGlobalFocusChanged(View oldFocus, View newFocus) {
      //view的相关操作
    }
});

我们看一下View官方注释:

/**
 * 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() 。

扫码关注公众号“伟大程序猿的诞生“,更多干货等着你~
扫码关注公众号“伟大程序猿的诞生“,更多干货等着你~
扫码关注公众号“伟大程序猿的诞生“,更多干货等着你~