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没有任何关系。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容