Android:如何检查ScrollView中的视图是否可见?

196

我有一个ScrollView,其中包含一系列Views。 我想确定一个视图当前是否可见(如果ScrollView当前显示了它的任何一部分)。 我希望下面的代码能够实现这一点,但令人惊讶的是它并没有:

Rect bounds = new Rect();
view.getDrawingRect(bounds);

Rect scrollBounds = new Rect(scroll.getScrollX(), scroll.getScrollY(), 
        scroll.getScrollX() + scroll.getWidth(), scroll.getScrollY() + scroll.getHeight());

if(Rect.intersects(scrollBounds, bounds))
{
    //is  visible
}

我很好奇你是如何让它工作的。我也在尝试做同样的事情,但 ScrollView 只能容纳一个直接子元素。你的“一系列视图”是否被包裹在 ScrollView 内的另一个布局中?这就是我的布局方式,但当我这样做时,这里给出的任何答案都对我不起作用。 - Rooster242
1
是的,我的一系列视图都在一个LinearLayout中,它是ScrollView的第1个子项。Qberticus的答案对我有用。 - ab11
15个回答

211

这个有效:

Rect scrollBounds = new Rect();
scrollView.getHitRect(scrollBounds);
if (imageView.getLocalVisibleRect(scrollBounds)) {
    // Any portion of the imageView, even a single pixel, is within the visible window
} else {
    // NONE of the imageView is within the visible window
}

1
完美运作。为了更清晰,如果视图完全或部分可见,则返回true;false表示视图完全不可见。 - qwertzguy
1
我使用了以下代码,使得 GridView/ListView/GridViewWithHeader 能够与 SwipeRefreshLayout 兼容。 - Kartik
有人能解释一下为什么这个代码可以工作吗?getHitRect返回的是父视图坐标系中的矩形,但是getLocalVisibleRect返回的是滚动视图本地坐标系中的矩形,不是吗? - Pin
3
如果子视图被另一个子元素覆盖,这个不会涵盖重叠部分,它仍将返回true。 - Pradeep
1
是的,我们需要一个 Rect 的实例。但是获取 getHitRect 是否必要?如果我使用一个 Rect(0,0-0,0) 会有什么不同吗?我们可以看到 getLocalVisibleRect 调用了 getGlobalVisibleRect。而且在这里设置了 Rect:r.set(0, 0, width, height)。@BillMote - chefish
显示剩余4条评论

67

在测试视图时,请使用View#getHitRect而不是View#getDrawingRect。您可以在ScrollView上使用View#getDrawingRect而无需进行明确计算。

View#getDrawingRect的代码:

 public void getDrawingRect(Rect outRect) {
        outRect.left = mScrollX;
        outRect.top = mScrollY;
        outRect.right = mScrollX + (mRight - mLeft);
        outRect.bottom = mScrollY + (mBottom - mTop);
 }

View#getHitRect的代码:

public void getHitRect(Rect outRect) {
        outRect.set(mLeft, mTop, mRight, mBottom);
}

40
我应该在哪里调用这些方法? - Tooto
4
如何调用这些方法?我正在使用它,但它总是返回false。请告诉我。 - KK_07k11A0585
3
这些方法应该在哪里调用? - zemaitis

66

如果您想要检测视图是否完全可见:

private boolean isViewVisible(View view) {
    Rect scrollBounds = new Rect();
    mScrollView.getDrawingRect(scrollBounds);

    float top = view.getY();
    float bottom = top + view.getHeight();

    if (scrollBounds.top < top && scrollBounds.bottom > bottom) {
        return true;
    } else {
        return false;
    }
}

7
这是正确的答案 =)在我的情况下,我把 if 语句修改成了这样:scrollBounds.top <= top && scrollBounds.bottom >= bottom - Helton Isac
2
如果你的视图被推到滚动视图的顶部或底部,你需要使用<=或>=。+1 Helton - Joe Maher
你真的测试过吗?在最简单的布局ScrollView和TextView作为子项时,它总是返回false。 - Farid
1
getHitRect()和getDrawingRect()有什么区别?请指教。 - VVB
4
如果视图直接添加到ScrollView容器的根目录下,此代码才能正常工作。如果您想处理子视图中的子视图等情况,请查看Phan Van Linh的回答。 - thijsonline

27

这个扩展可以帮助检测视图完全可见。
如果你的ViewScrollView的子级(例如:ScrollView -> LinearLayout -> ContraintLayout -> ... -> YourView),它也可以工作。

fun ScrollView.isViewVisible(view: View): Boolean {
    val scrollBounds = Rect()
    this.getDrawingRect(scrollBounds)
    var top = 0f
    var temp = view
    while (temp !is ScrollView){
        top += (temp).y
        temp = temp.parent as View
    }
    val bottom = top + view.height
    return scrollBounds.top < top && scrollBounds.bottom > bottom
}

注意

1) view.getY()view.getX() 返回的是相对于第一个父视图的 x、y 值。

2) 下面是一个有关 getDrawingRect 如何返回值的示例: enter image description here 链接


我想要一个解决方案,如果视图被键盘隐藏,方法应该返回false,这个做到了。谢谢。 - Rahul

22

我的解决方案是使用 NestedScrollView 滚动元素:

    final Rect scrollBounds = new Rect();
    scroller.getHitRect(scrollBounds);

    scroller.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
        @Override
        public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {

            if (myBtn1 != null) {

                if (myBtn1.getLocalVisibleRect(scrollBounds)) {
                    if (!myBtn1.getLocalVisibleRect(scrollBounds)
                            || scrollBounds.height() < myBtn1.getHeight()) {
                        Log.i(TAG, "BTN APPEAR PARCIALY");
                    } else {
                        Log.i(TAG, "BTN APPEAR FULLY!!!");
                    }
                } else {
                    Log.i(TAG, "No");
                }
            }

        }
    });
}

需要API 23或更高版本。 - Desolator
@SolidSnake,不,你需要导入不同的类,它可以正常工作。 - Parth Anjaria

11
为了补充Bill Mote的回答,使用getLocalVisibleRect,您可能想要检查视图是否只是部分可见:
Rect scrollBounds = new Rect();
scrollView.getHitRect(scrollBounds);
if (!imageView.getLocalVisibleRect(scrollBounds)
    || scrollBounds.height() < imageView.getHeight()) {
    // imageView is not within or only partially within the visible window
} else {
    // imageView is completely visible
}

7
不起作用,即使部分可见的视图也被归类为完全可见。 - azfar

9
public static int getVisiblePercent(View v) {
        if (v.isShown()) {
            Rect r = new Rect();
            v.getGlobalVisibleRect(r);
            double sVisible = r.width() * r.height();
            double sTotal = v.getWidth() * v.getHeight();
            return (int) (100 * sVisible / sTotal);
        } else {
            return -1;
        }
    }

2
这与ab11所要求的不同。isShown()仅检查可见性标志,而不检查视图是否在屏幕的可见区域内。 - Romain Guy
5
@Romain Guy 这段代码不能处理视图完全滚出屏幕的情况。应该使用以下代码:public static int getVisiblePercent(View v) { if (v.isShown()) { Rect r = new Rect(); boolean isVisible = v.getGlobalVisibleRect(r); if (isVisible) { double sVisible = r.width() * r.height(); double sTotal = v.getWidth() * v.getHeight(); return (int) (100 * sVisible / sTotal); } else { return -1; } } else { return -1; } }该函数用于计算给定视图在屏幕上可见的百分比,将矩形区域覆盖到视图上以计算可见面积占总面积的百分比。如果视图不可见,则返回-1。 - chefish

6

如果你想检测你的View是否完全可见,请尝试使用以下方法:

private boolean isViewVisible(View view) {
    Rect scrollBounds = new Rect();
    mScrollView.getDrawingRect(scrollBounds);
    float top = view.getY();
    float bottom = top + view.getHeight();
    if (scrollBounds.top < top && scrollBounds.bottom > bottom) {
        return true; //View is visible.
    } else {
        return false; //View is NOT visible.
    }
}

严格来说,您可以通过以下方法获取视图的可见性:

if (myView.getVisibility() == View.VISIBLE) {
    //VISIBLE
} else {
    //INVISIBLE
}

视图中可见性的可能常量值如下: VISIBLE 该视图是可见的。可与 setVisibility(int) 和 android:visibility 一起使用。 INVISIBLE 该视图是不可见的,但它仍然为布局目的占用空间。可与 setVisibility(int) 和 android:visibility 一起使用。 GONE 该视图是不可见的,并且它在布局目的上不占用任何空间。可与 setVisibility(int) 和 android:visibility 一起使用。

3
缓慢鼓掌声 OP 想知道的是,在视图可见性为 View#VISIBLE 的情况下,如何确定滚动视图中的视图本身是否可见。 - Joao Sousa
1
我刚刚检查了一个简单的项目。布局中有ScrollView和TextView作为子项;即使TextView完全可见,它始终返回false。 - Farid
它总是返回false。 - Rahul

6
今天我遇到了同样的问题。在谷歌搜索和阅读安卓参考资料时,我找到了这篇文章,并最终采用了另一种方法。
public final boolean getLocalVisibleRect (Rect r)

很好,他们不仅提供了Rect,还提供了一个布尔值来指示视图是否可见。但是,负面的是这个方法没有文档说明 :(

1
这只告诉你该项是否设置为可见(true)。它并不告诉你“可见”项是否实际在视口内可见。 - Bill Mote
getLocalVisibleRect的代码不支持你的要求: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; } - mbafford

6

Kotlin方式;

一个用于列出滚动视图的滚动并在子视图可见时执行操作的扩展。

@SuppressLint("ClickableViewAccessibility")
fun View.setChildViewOnScreenListener(view: View, action: () -> Unit) {
    val visibleScreen = Rect()

    this.setOnTouchListener { _, motionEvent ->
        if (motionEvent.action == MotionEvent.ACTION_MOVE) {
            this.getDrawingRect(visibleScreen)

            if (view.getLocalVisibleRect(visibleScreen)) {
                action()
            }
        }

        false
    }
}

使用这个扩展函数适用于任何可滚动的视图

nestedScrollView.setChildViewOnScreenListener(childView) {
               action()
            }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接