Android 检测 View 的可见性

目前遇到一个需求,为了增加应用中广告投放的精确度与有效程度,现在需要对 app 中广告位的展示情况做一个统计并上报。

实现思路

思路很简单,因为需要对多个广告位做统计,那么就封装出来一个广告的控件,然后在这一个控件里面统一的检测广告的出现次数,即曝光的有效程度。

根据产品需求,要广告出现在屏幕中2秒以上且出现的面积要大于50%才算是有效曝光,所以就要动态的来检测展示广告的 view 的可见性。而这个也是这篇博客的内容。

检测 View 可见性

主要从下面几个方面来考虑了:

  • View 的加载过程监控
  • View 是否在屏幕中的检测
  • View 是否被覆盖的检测

View 的加载过程监控

view 的加载过程,就是 view 被添加到 window 的过程,很容易的就想到了两个方法 onAttachedToWindowonDetachedFromWindow,同时还有另外的方法onWindowVisibilityChanged

  • onAttachedToWindow和 onDetachedFromWindow

这两个方法分别是 view 被添加到 window 时,以及从 window 删除时的回调。监控这两者能很好的知道 view 被添加以及从 window 删除的时间,但是这两个方法在 view 的整个生命周期中只有一次调用,仅仅是这两者来监控是肯定不行的。

  • onWindowVisibilityChanged

onWindowVisibilityChanged 其实和上面两个方法有所关联,查看源码可知:

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
	...
	onAttachedToWindow();
	int vis = info.mWindowVisibility;
    if (vis != GONE) {
         onWindowVisibilityChanged(vis);
    }
}

void dispatchDetachedFromWindow() {
	...
	if (info != null) {
		int vis = info.mWindowVisibility;
		if (vis != GONE) {
			onWindowVisibilityChanged(GONE);
		}
	}
	onDetachedFromWindow();
}

所以上面的两个选择其一来监控就行了。

然后就是 View 的是否在屏幕中的检测以及是否被遮挡的检测了。

View 的是否在屏幕中的检测

关于是否在屏幕中的检测主要是通过 getGlobalVisibleRect()方法来实现的。同时 view 还提供了另外一个方法getLocalVisibleRect()。这两个方法都能用来检测 view 是否在屏幕中,他们的区别在于:

  • getGlobalVisibleRect 得到的 Rect 是当前在屏幕内的 View 的区域,其坐标是相对于整个窗口而言的;
  • getLocalVisibleRect 得到的 Rect 是当前在屏幕内的 View 区域的,其坐标是相对于当前 view 本身的;

另外如果 View 在当前屏幕内,则两者都返回 true,如果不在则返回 false,而且如果在屏幕外两者得到的 Rect 的坐标内容是一样的。可以通过这两个方法的任意一种来获取是否在屏幕中,并且计算当前显示出来的面积。

//检测是否被遮挡,如果被遮挡或者不在屏幕中则返回true
private fun isShade(): Boolean {
	if (this.visibility != View.VISIBLE) {
            return true
    }
    var currentView = this as View
    val currentViewRect = Rect()
    val isCoverd = currentView.getGlobalVisibleRect(currentViewRect)
    if(!isCoverd){
	    return true
    }
    if (currentViewRect.width() * currentViewRect.height() 
		    < this.measuredHeight * this.measuredWidth / 2) {
            // 如果移出屏幕的面积大于 50% 则认为被遮罩了
            return true
     }
}

基于以上的代码可以有效的判断出当前 View 是否或者是否在屏幕中,但是有些时候即使是在屏幕中,当前的 View 也可能被其他的 View 遮挡,这个时候就要做第三步了。

View是否被覆盖的检测

关于 View 是否被其他 View 遮挡的问题,貌似只有一种解决方案——循环查找父级 View 以及兄弟 View 然后判断两者的 Rect 区域是否有交际。

最终代码如下:

private fun isShade(): Boolean {

        if (this.visibility != View.VISIBLE) {
            return true
        }

        var currentView = this as View

        val currentViewRect = Rect()
	    val isCoverd = currentView.getGlobalVisibleRect(currentViewRect)
	    // 如果在屏幕外肯定是不可见的
	    if(!isCoverd){
		    return true
	    }
	    
        if (currentViewRect.width() * currentViewRect.height() <= this.measuredHeight * this.measuredWidth / 2) {
            // 如果移出屏幕的面积大于 50% 则认为被遮罩了
            return true
        }
		// 记录下被移出屏幕外的面积
        val outScreenArea = this.measuredHeight * this.measuredWidth - currentViewRect.width() * currentViewRect.height()
		// 循环查找父布局及兄弟布局
        while (currentView.parent is ViewGroup) {
            val currentParent = currentView.parent as ViewGroup
            
            if (currentParent.visibility != View.VISIBLE)
                return true

            val start = indexOfViewInParent(currentView, currentParent)
            for (i in start + 1 until currentParent.childCount) {
                val otherView = currentParent.getChildAt(i)
                // 这里主要是为了排除 invisible 属性标记的 view 
                if (otherView.visibility != View.VISIBLE) {
                    break
                }
                val viewRect = Rect()
                this.getGlobalVisibleRect(viewRect)
                val otherViewRect = Rect()
                otherView.getGlobalVisibleRect(otherViewRect)
                // 这个方法用来检测两个区域是否重叠,并且如果重叠的话
                // 就将当前 Rect 修改为重叠的区域
                if (otherViewRect.intersect(viewRect)) {
                    if ((outScreenArea + otherViewRect.width() * otherViewRect.height()) 
					                    >= viewRect.width() * viewRect.height() / 2)
                    // 表示相交区域 + 屏幕外的区域 大于 50% 则也认为被遮罩了
                        return true
                }
            }
            currentView = currentParent
        }
        return false
    }

    private fun indexOfViewInParent(view: View, parent: ViewGroup): Int {
        var index = 0
        // 查找出应该从第几个子 view 开始 参考事件分发机制从用户可以见到的最上层开始分发,
        // 最上层的子 view,index 值总是更大
        while (index < parent.childCount) {
            if (parent.getChildAt(index) == view)
                break
            index++
        }
        return index
    }

上面的方法就能很好的检测当前 View 是否在屏幕中,以及是否被其他 view 所遮挡。当然这个是根据我的需求写出来的代码,如果有不同的需求,可以稍作修改,因为基本原理就在这儿。

接下来就是检测的时机问题了。

检测 View 可见性的时机

因为需求的原因,项目中需要在适当的时机来检测当前 view 是否可见,可能引起 view 可见性的因素有:

  • listVie、ScrollView 的滑动
  • viewPager 切换
  • fragment 切换
  • Activity 切换

目前能想到的就这些,如果还有其他的欢迎大家补充。其中第四个好说,因为它会导致 view.onDetachedFromWindow 被调用。主要是前面三个,在 fragment show\hint 切换过程中不会触发任何 view 的变化,包括滑动等。

至少目前我没有找到一个能够在 view 中被动监听由系统通知的 view 状态切换监听。没办法只能用笨方法——由上层的这些控件在状态变化时主动通知广告View 来进行可见性检测。一般在 listView、OnScrollView 的滑动监听,以及 Fragment 的 onHintChange 中等。只是调用一下方法,成本不算太高还算能接受。

好了,以上就是关于 Android 中 View 的可见性检测的所有内容,关于是最后的监听方面,如果您有好的方案烦请不吝指教。

参考

版权声明:本文为博主原创文章,转载请声明出处,请尊重别人的劳动成果,谢谢!

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值