产品需求:视频view感知到页面生命周期的自动播放或暂停,同时自动数据上报曝光等。
技术需求:感知到view的展示,移除,覆盖等场景,而且要在view内部实现。
技术原理:
检测 View 可见性
主要从下面几个方面来考虑了:
View 的加载过程监控
View 是否在屏幕中的检测
View 是否被覆盖的检测
View 的加载过程监控
view 的加载过程,就是 view 被添加到 window 的过程,很容易的就想到了两个方法 onAttachedToWindow和onDetachedFromWindow,同时还有另外的方法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 的区域,其坐标是相对于整个窗口而言的;
public boolean getGlobalVisibleRect(Rect r, Point globalOffset) {
int width = mRight - mLeft;
int height = mBottom - mTop;
if (width > 0 && height > 0) {
r.set(0, 0, width, height);
if (globalOffset != null) {
globalOffset.set(-mScrollX, -mScrollY);
}
return mParent == null || mParent.getChildVisibleRect(this, r, globalOffset);
}
return false;
}
getLocalVisibleRect 得到的 Rect 是当前在屏幕内的 View 区域的,其坐标是相对于当前 view 本身的;
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;
}
另外如果 View 在当前屏幕内,则两者都返回 true,如果不在则返回 false,而且如果在屏幕外两者得到的 Rect 的坐标内容是一样的。可以通过这两个方法的任意一种来获取是否在屏幕中,并且计算当前显示出来的面积。
//检测是否被遮挡,如果被遮挡或者不在屏幕中则返回true
public boolean isViewCovered(final View view) {
if(view == null || !isViewValid) {
LogTool.d(TAG,"isViewCovered view == null");
return true;
}
View currentView = view;
Rect currentViewRect = new Rect();
boolean partVisible = currentView.getGlobalVisibleRect(currentViewRect);
// 获取可见区域为false,说明被覆盖了
if(!partVisible){
LogTool.d(TAG,"isViewCovered partVisible is false");
return true;
}
//可见区域小于view区域的50%
if(currentViewRect.width()*currentViewRect.height() < view.getMeasuredWidth()*view.getMeasuredHeight()/2){
LogTool.d(TAG,"isViewCovered Visible rect is smaller than 50% area!");
return true;
} else {
return false;
}
}
基于以上的代码可以有效的判断出当前 View 是否或者是否在屏幕中,但是有些时候即使是在屏幕中,当前的 View 也可能被其他的 View 遮挡,这个时候就要做第三步了。
View是否被覆盖的检测
关于 View 是否被其他 View 遮挡的问题,貌似只有一种解决方案——循环查找父级 View 以及兄弟 View 然后判断两者的 Rect 区域是否有交际。
最终代码如下:
public boolean isViewCovered(final View view) {
if(view == null || !isViewValid) {
LogTool.d(TAG,"isViewCovered view == null");
return true;
}
View currentView = view;
Rect currentViewRect = new Rect();
boolean partVisible = currentView.getGlobalVisibleRect(currentViewRect);
// 获取可见区域为false,说明被覆盖了
if(!partVisible){
LogTool.d(TAG,"isViewCovered partVisible is false");
return true;
}
//可见区域小于view区域的50%
if(currentViewRect.width()*currentViewRect.height() < view.getMeasuredWidth()*view.getMeasuredHeight()/2){
LogTool.d(TAG,"isViewCovered Visible rect is smaller than 50% area!");
return true;
}
ViewGroup currentParent;
Rect viewRect, otherViewRect;
View otherView;
while (currentView.getParent() instanceof ViewGroup) {
currentParent = (ViewGroup) currentView.getParent();
// if the parent of view is not visible,return true
if (currentParent.getVisibility() != View.VISIBLE){
LogTool.d(TAG,"isViewCovered Visible currentParent is not visible!");
return true;
}
int start = indexOfViewInParent(currentView, currentParent);
for (int i = start + 1; i < currentParent.getChildCount(); i++) {
viewRect = new Rect();
view.getGlobalVisibleRect(viewRect);
otherView = currentParent.getChildAt(i);
otherViewRect = new Rect();
otherView.getGlobalVisibleRect(otherViewRect);
// if view intersects its older brother(covered),return true
if (Rect.intersects(viewRect, otherViewRect)){
LogTool.d(TAG, "isViewCovered view intersects its older brother(covered)");
return true;
}
}
currentView = currentParent;
}
return false;
}
上面的方法就能很好的检测当前 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 的可见性检测的所有内容,关于是最后的监听方面,如果您有好的方案烦请不吝指教。
参考
判断view 是否被遮挡
View 源码
————————————————
版权声明:本文为CSDN博主「耳东_」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u011494285/article/details/81350719
java版本资料及详细讲解: