Android深入研究之View的可见性检测

一、背景

  在Android开发中,我们常常会对View的可见性visiblity进行操作或者检查。常见的场景有:

  • 在消息流中,根据ImageView是否在屏幕中出现了再决定是否加载;
  • 在视频流页面,当视频滑入屏幕被用户可见时播放,滑出屏幕就自动停止播放等等;
  • 网络请求数据,根据返回的数据结果控制相应View可见或不可见;
  • 需要根据view是否可见或第一次可见,做特殊的处理,如埋点上报等操作。

  在ListView、RecyclerView、ScrollView里可能会遇到比较多的对View的可视性进行操作或检查的场景。
  比如在下面的ScrollView中,根据view自身属性判断view可见性可根据下面四个方法获取:

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

image

二、检查View是否可见的基本方法

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方法。

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);

PS:由源码分析可以看出getGlobalVisibleRect()和getLocalVisibleRect()对View的可见性visibility判断结果相同,只是获取出的rect值有所区别。

5. 判断手机屏幕是否熄灭or是否解锁

PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
boolean isScreenOn = pm.isScreenOn();
boolean isInteractive = pm.isInteractive();
// 可能有些版本上面isScreenOn方法隐藏了或者是deprecated了,可以尝试反射调用它,但是要记得用的时候catch异常
Method isScreenOnMethod = pm.getClass().getMethod("isScreenOn");
boolean isScreenOn = (Boolean) isScreenOnMethod.invoke(pm);

6.结论

(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没有任何关系。
(3)可以使用getGlobalVisibleRect()和getLocalVisibleRect()方法判断ScrollView中View的可见性。

三、ListView、RecyclerView、ScrollView中如何检查View的可见性

  说实话感觉App开发中用得最多的就是各种列表啊、滚动滑动的View。在Android里面这几个可以滚动的View,都有着各自的特点。在用到上面的检测方法时,可以好好结合这几个View的特点,在它们各自的滚动过程中,更加有效的去检查View的可见性。

  可以先根据自己的业务需要,把上面提到的方法封装成一个VisibilityCheckUtil工具类,例如可以提供一个check方法,当View的物理面积有50%可见时,就返回true。

1. ScrollView

  假设我们有一个mView在mScrollView中,我们可以监听mScrollView的滚动,在onScrollChanged中检查mView的可见性。

mScrollView.getViewTreeObserver().addOnScrollChangedListener(
        new ViewTreeObserver.OnScrollChangedListener() {
 
          @Override
          public void onScrollChanged() {
            // 可以先判断ScrollView中的mView是不是在屏幕中可见
            Rect scrollBounds = new Rect();
            if (!mView.getLocalVisibleRect(scrollBounds)) {
                return;
            }
            
            // 再用封装好的工具类检查可见性是否大于50%
            if (VisibilityCheckUtil.check(mView)) {
                // do something
            }
          }
        });

2.ListView

  假设我们在mListView的第8个位置有一个需要检查可见性的mView。

  首先要监听mListView的滚动,接着在onScroll回调中,调用mListView.getFirstVisiblePosition和mListView.getLastVisiblePosition查看第10个位置是否处于可见范围,然后在调用封装好的VisibilityCheckUtil去检查mView是否可见。

mListView.setOnScrollListener(new OnScrollListener() {
      @Override
      public void onScrollStateChanged(AbsListView view, int scrollState) {
        mScrollState = scrollState;
      }
 
      @Override
      public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
          int totalItemCount) {
        if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
          return;
        }
 
        int first = mListView.getFirstVisiblePosition();
        int last = mListView.getLastVisiblePosition();
        // 满足3个条件:先判断ListView中的mView是不是在可见范围中,再判断是不是大于50%面积可见
        if (8 >= first && 8 <= last && VisibilityCheckUtil.check(mView)) {
            // do something
        }
      }
    });

3. RecyclerView

  和上面类似,还是把mView摆放在第8个位置,检查原理和ListView类似。

mLinearLayoutManager = new LinearLayoutManager(this);
mRecyclerView.setLayoutManager(mLinearLayoutManager);
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
 
      @Override
      public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);
        if (mLinearLayoutManager == null) {
          return;
        }
 
        int firstVisiblePosition = mLinearLayoutManager.findFirstVisibleItemPosition();
        int lastVisiblePosition = mLinearLayoutManager.findLastVisibleItemPosition();
        // 同样是满足3个条件
        if (8 >= firstVisiblePosition && 8 <= lastVisiblePosition && VisibilityCheckUtil.check(mView)) {
          // do something
        }
      }
    });

  注意事项:实际的开发中肯定会遇到更多的场景,我们都要先分析界面的特点,再结合前面提到的几个方法,更有效地检查View的可见性。

  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值