View 的可见性检查还可以这样~

背景&问题

在Android开发中,我们常常会对View的可视性visiblity进行操作或者检查。如网络请求数据,根据返回的数据结果控制相应View可见或不可见,或者判断某个View是否在屏幕中可见,不可见时给予用户相应提示信息等。在ListView、RecyclerView、ScrollView里我们可能会比较经常做这些事。比如在下面的ScrollView中:



四种方法获取的结果如下:

View5.getVisibility() = View.VISIBLE;
View5.isShown() = true; 
View5.getGlobalVisibleRect() = false;
View5.getLocalVisibleRect() =  false;

为什么有这样的结果呢?四种方法的具体的区别是什么?getGlobalVisibleRect和getLocalVisibleRect具体怎么用呢?先说下几种方法的具体区别。

基本方法

1.View.getVisibility()

这是常用的也是最基本的检查View可见性的方法,这个方法的返回值有View.VISIBLE(可见)、View.INVISIBLE(不可见但占着原来的空间)和View.GONE( 不可见且不占原来的空间)。如果这个方法返回的是View.INVISIBLE或者View.GONE,那么这个View肯定是对用户不可见的。

2.View.isShown()

这个方法和View.getVisibility()作用类似,重要的区别就是:

  • getVisibility()返回的是int值,isShown()返回的是boolean值
  • View.isShown()会对View的所有父类调用getVisibility方法
/**
 * Returns the visibility of this view and all of its ancestors
 *
 * @return True if this view and all of its ancestors are {@link #VISIBLE}
 */
public boolean isShown() {
    View current = this;
    //noinspection ConstantConditions
    do {
        if ((current.mViewFlags & VISIBILITY_MASK) != VISIBLE) {
            return false;
        }
        ViewParent parent = current.mParent;
        if (parent == null) {
            return false; // We are not attached to the view root
        }
        if (!(parent instanceof View)) {
            return true;
        }
        current = (View) parent;
    } while (current != null);

    return false;
}

由源码中注释可以知道,这个方法递归地去检查这个View以及它的parentView的Visibility属性是不是等于View.VISIBLE,这样就对这个View的所有parentView做了一个检查。另外这个方法还在递归的检查过程中,检查了parentView == null,也就是说所有的parentView都不能为null。否则就说明这个View根本没有被addView过(比如创建界面UI时,可能会先new一个View,然后根据条件动态地把它add带一个ViewGroup中),那肯定是不可能对用户可见的。

3.View.getGlobalVisibleRect()

顾名思义,这个方法会返回一个View是否可见的boolean值,同时还会将该View的可见区域left,top,right,bottom值保存在一个rect对象中,具体使用方法如下:

Rect globalRect = new Rect();
boolean visibile = view5.getGlobalVisibleRect(globalRect);

getGlobalVisibleRect(Rect r)最后调用的是getGlobalVisibleRect(Rect r, Point globalOffset)方法,看下该方法的注释:

/**
 * If some part of this view is not clipped by any of its parents, then
 * return that area in r in global (root) coordinates. To convert r to local
 * coordinates (without taking possible View rotations into account), offset
 * it by -globalOffset (e.g. r.offset(-globalOffset.x, -globalOffset.y)).
 * If the view is completely clipped or translated out, return false.
 *
 * @param r If true is returned, r holds the global coordinates of the
 *        visible portion of this view.
 * @param globalOffset If true is returned, globalOffset holds the dx,dy
 *        between this view and its root. globalOffet may be null.
 * @return true if r is non-empty (i.e. part of the view is visible at the
 *         root level.
 */

由以上注释可以知道,当这个View只要有一部分仍然在屏幕中(没有被父View遮挡,即not clipped by any of its parents),那么将把没有被遮挡的那部分区域保存在rect对象中返回,且返回visibility为true。此时的rect是以手机屏幕作为坐标系(即global coordinates),也就是原点是屏幕左上角;如果它全部被父View遮挡住了或者本身就是不可见的,返回的visibility就为false,rect中的值为0。

4.View.getLocalVisibleRect()

这个方法和getGlobalVisibleRect有些类似,也可以拿到这个View在屏幕的可见区域的坐标,唯一的区别getLocalVisibleRect(rect)获得的rect坐标系的原点是View自己的左上角,而不是屏幕左上角。其也会调用getGlobalVisibleRect()方法:

public final boolean getLocalVisibleRect(Rect r) {
    final Point offset = mAttachInfo != null ? mAttachInfo.mPoint : new Point();
    if (getGlobalVisibleRect(r, offset)) {
        r.offset(-offset.x, -offset.y); // make r local
        return true;
    }
    return false;
}

由以上源码可以看到,getLocalVisibleRect()会先获取View的offset point(相对屏幕或者ParentView的偏移坐标),然后再去调用getGlobalVisibleRect(Rect r, Point globalOffset)方法来获取可见区域,最后再把得到的GlobalVisibleRect和Offset坐标做一个加减法,转换坐标系原点。使用方法如下:

Rect localRect = new Rect();
boolean visibile = view5.getLocalVisibleRect(localRect);
* 5.getGlobalVisibleRect() VS getLocalVisibleRect()*

回到最开始的问题,四种方法获取view5的visibility结果应该很好理解了,那getGlobalVisibleRect()和getLocalVisibleRect()中获取出的rect值具体区别在哪儿?如下图,假设屏幕大小为1080x1920,以ScrollView为Parent View,在ScrollView的onScrollChanged()中对view1,view3和view5的可见性进行判断:



代码比较简单,直接就看debug结果吧,如下:



由以上结果可以看出getGlobalVisibleRect()和getLocalVisibleRect()对View的可见性visibility判断结果相同,只是获取出的rect值有所区别:
  • 当View在屏幕中全部可见时(图中view3),根据上面的介绍知,getLocalVisibleRect()的原点是自己的左上角,所以当View的左上角在屏幕中时,获取的rect左上角坐标一定为(0,0),右下角为(View.getWidth, View.getHeight),而getGlobalVisibleRect()的原点是屏幕左上角,获取出的rect值是与getLocalVisibleRect()左上角不为(0,0);
  • 当View在屏幕中部分可见时(图中view1),getLocalVisibleRect()获取的rect值左上角不为(0,0),但此时也与getGlobalVisibleRect()获取值不同;
  • View在屏幕中全部不可见时(图中view5),两者的visibility都为false,且两者获取的rect值相同。这是为什么呢?由源码可以知道,getLocalVisibleRect()最终调用的是getGlobalVisibleRect()方法,并会减去View自身的便偏移坐标offset point,但只有当View可见时才会减去这个偏移坐标,要是不可见就直接返回了,所以此时两者获取出的rect值是相同的。
6.注意&tips

(1)使用getGlobalVisibleRect() getLocalVisibleRect()判断View的可见性时,一定要等View绘制完成后,再去调用这两个方法,否则无法得到对的结果,返回值的rect值都是0,visibility为false。这和获取View的宽高原理是一样的,如果View没有被绘制完成,那么View.getWidth和View.getHeight一定是等于0的。例如,测试时发现,仅仅在代码中findViewById()把View初始化出来,而对View没有其他操作,并不能保证View绘制完成,就像以下代码:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    visibleButton = (Button) findViewById(R.id.visible_test);
    boolean localVisibility = visibleButton.getLocalVisibleRect(rectLocal);    //localVisibility始终为false,rectLocal值为0
    boolean globalVisibility = visibleButton.getGlobalVisibleRect(rectGlobal);  //globalVisibility始终为false,rectGlobal值为0          
}

(2)关于getGlobalVisibleRect()方法的特别说明,这个方法只能检查出这个View在手机屏幕(或者说是相对它的父View)的位置,而不能检查出与其他兄弟View的相对位置:
比如有一个ViewGroup,下面有View1、View2这两个子View,View1和View2是平级关系。此时如果View2盖住了View1,那么用getGlobalVisibleRect方法检查View1的可见性,得到的返回值依然是true,得到的可见矩形区域rect也是没有任何变化的。也就是说View1.getGlobalVisibleRect(rect)得到的结果与View2没有任何关系。

推荐阅读更多精彩内容