Android:如何检查ScrollView内部的View是否可见?


168

我有一个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只能容纳1个直接子代。您的“一系列视图”是否包装在ScrollView中的另一个布局中?那是我的布局方式,但是当我这样做时,这里给出的答案对我都不起作用。
Rooster242

1
是的,我的一系列视图都在LinearLayout中,它是ScrollView的1个子级。Qberticus的回答对我有用。
2012年

Answers:


65

使用View#getHitRect而不是View#getDrawingRect在要测试的视图上使用。您可以使用View#getDrawingRectScrollView,而不是明确地计算。

来自的代码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);
}

35
我应该在哪里调用这种方法?
Tooto 2014年

3
@Qberticus如何调用方法?我正在使用它,并且总是返回false。请让我知道
KK_07k11A0585 2015年

2
究竟在哪里调用这些方法?
zemaitis

193

这有效:

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;否则,返回true。false表示视图完全不可见。
qwertzguy

1
[+1]我用这个代码来获取GridView/ ListView/ GridViewWithHeader与合作SwipeRefreshLayout
Kartik

有人可以解释为什么这行得通吗?getHitRect在父坐标中getLocalVisibleRect返回rect ,但是在scrollview的局部坐标中返回rect,不是吗?
2015年

3
这不包括重叠,如果Child View被另一个子元素重叠,它将仍然返回true
Pradeep

1
是的,我们需要一个Rect实例。但是是否需要getHitRect。有没有什么不同的,如果我用一个矩形(0,0-0,0)。我们可以看到getLocalVisibleRect通话getGlobalVisibleRect.And矩形在这里设置r.set(0,0,宽度,高度); @ BillMote。
chefish

56

如果要检测视图是完全可见的:

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

6
这是正确的答案=)就我而言,我更改了if的样子:scrollBounds.top <= top && scrollBounds.bottom => bottom
Helton Isac

2
+1 Helton如果将视图按滚动视图的顶部或底部,则分别需要<=或> =
Joe Maher

你真的测试过了吗?在最简单的ScrollView和TextView布局中,它始终作为子项返回false。
Farid

1
getHitRect()和getDrawingRect()之间有什么区别?请引导
-VVB

2
仅当视图直接添加到ScrollView容器的根目录时,此代码才有效。如果您想在子视图等中处理子视图,请查看Phan Van Linh的答案
。– thijsonline

12

我的解决方案是使用NestedScrollViewScroll元素:

    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+
SolidSnake

@SolidSnake,不,您不需要导入其他类,它可以正常工作
Parth Anjaria

10

为了使用getLocalVisibleRect扩展Bill Mote的答案,您可能需要检查视图是否仅部分可见:

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
}

6
这不起作用..即使部分可见的视图也被归类为完全可见
Azfar 2014年

10

此扩展有助于检测完全可见的视图。
如果您View是...的孩子ScrollView(例如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值返回给FIRST PARENT

2)这是有关如何getDrawingRect返回 链接的示例在此处输入图片说明


我想要一个解决方案,如果视图隐藏在键盘下,则方法应该返回false,并且可以完成此工作。谢谢。
拉胡尔

8
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()仅检查可见性标志,而不检查视图是否在屏幕的可见区域中。
罗曼·盖伊

4
@Romain Guy当视图完全滚动到屏幕之外时,代码不会覆盖。应该是`public static int getVisiblePercent(View v){if(v.isShown()){Rect r = new Rect(); boolean isVisible = v.getGlobalVisibleRect(r); 如果(isVisible){double sVisible = r.width()* r.height(); double sTotal = v.getWidth()* v.getHeight(); return(int)(100 * sVisible / sTotal); } else {return -1; }} else {return -1; }`
Chefish

6

我今天也遇到了同样的问题。在谷歌搜索和阅读Android参考时,我发现了这篇文章以及最终使用的一种方法。

public final boolean getLocalVisibleRect (Rect r)

它们的优点是不仅提供Rect,而且还提供布尔值以指示View是否可见。不利的一面是,该方法未公开:(


1
这仅告诉您该项目是否设置为visible(true)。它不会告诉您“可见”项在视口中是否实际可见。
Bill Mote 2013年

getLocalVisibleRect的代码不支持您的要求:`public final boolean getLocalVisibleRect(Rect r){final Point offset = mAttachInfo!= null吗?mAttachInfo.mPoint:新Point(); 如果(getGlobalVisibleRect(r,offset)){r.offset(-offset.x,-offset.y); //使r local return true; 返回false;}`
mbafford 2014年

6

我想检测您View是否已完全使用visible,请尝试以下方法:

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
}

视图中可见性的可能常量值为:

可见 这种看法是可见的。与setVisibility(int)和android:visibility一起使用。

隐形 这种观点是无形的,但它仍然占用空间布局的目的。与setVisibility(int)和android:visibility一起使用。

消失 该视图是不可见的,并且它不占用任何空间用于布局目的。与setVisibility(int)和android:visibility一起使用。


3
拍手慢。OP想要知道的是,假设视图的可见性为View#VISIBLE,如何知道视图本身在滚动视图中是否可见。
Joao Sousa 2015年

1
我只是检查了一个简单的项目。布局具有ScrollView和TextView作为子级;即使TextView完全可见,也始终返回false。
Farid

它始终返回false。
拉胡尔

3

您可以使用FocusAwareScrollView当视图变为可见时通知的:

FocusAwareScrollView focusAwareScrollView = (FocusAwareScrollView) findViewById(R.id.focusAwareScrollView);
    if (focusAwareScrollView != null) {

        ArrayList<View> viewList = new ArrayList<>();
        viewList.add(yourView1);
        viewList.add(yourView2);

        focusAwareScrollView.registerViewSeenCallBack(viewList, new FocusAwareScrollView.OnViewSeenListener() {

            @Override
            public void onViewSeen(View v, int percentageScrolled) {

                if (v == yourView1) {

                    // user have seen view1

                } else if (v == yourView2) {

                    // user have seen view2
                }
            }
        });

    }

这是课程:

import android.content.Context;
import android.graphics.Rect;
import android.support.v4.widget.NestedScrollView;
import android.util.AttributeSet;
import android.view.View;

import java.util.ArrayList;
import java.util.List;

public class FocusAwareScrollView extends NestedScrollView {

    private List<OnScrollViewListener> onScrollViewListeners = new ArrayList<>();

    public FocusAwareScrollView(Context context) {
        super(context);
    }

    public FocusAwareScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public FocusAwareScrollView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    public interface OnScrollViewListener {
        void onScrollChanged(FocusAwareScrollView v, int l, int t, int oldl, int oldt);
    }

    public interface OnViewSeenListener {
        void onViewSeen(View v, int percentageScrolled);
    }

    public void addOnScrollListener(OnScrollViewListener l) {
        onScrollViewListeners.add(l);
    }

    public void removeOnScrollListener(OnScrollViewListener l) {
        onScrollViewListeners.remove(l);
    }

    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        for (int i = onScrollViewListeners.size() - 1; i >= 0; i--) {
            onScrollViewListeners.get(i).onScrollChanged(this, l, t, oldl, oldt);
        }
        super.onScrollChanged(l, t, oldl, oldt);
    }

    @Override
    public void requestChildFocus(View child, View focused) {
        super.requestChildFocus(child, focused);
    }

    private boolean handleViewSeenEvent(View view, int scrollBoundsBottom, int scrollYOffset,
                                        float minSeenPercentage, OnViewSeenListener onViewSeenListener) {
        int loc[] = new int[2];
        view.getLocationOnScreen(loc);
        int viewBottomPos = loc[1] - scrollYOffset + (int) (minSeenPercentage / 100 * view.getMeasuredHeight());
        if (viewBottomPos <= scrollBoundsBottom) {
            int scrollViewHeight = this.getChildAt(0).getHeight();
            int viewPosition = this.getScrollY() + view.getScrollY() + view.getHeight();
            int percentageSeen = (int) ((double) viewPosition / scrollViewHeight * 100);
            onViewSeenListener.onViewSeen(view, percentageSeen);
            return true;
        }
        return false;
    }

    public void registerViewSeenCallBack(final ArrayList<View> views, final OnViewSeenListener onViewSeenListener) {

        final boolean[] viewSeen = new boolean[views.size()];

        FocusAwareScrollView.this.postDelayed(new Runnable() {
            @Override
            public void run() {

                final Rect scrollBounds = new Rect();
                FocusAwareScrollView.this.getHitRect(scrollBounds);
                final int loc[] = new int[2];
                FocusAwareScrollView.this.getLocationOnScreen(loc);

                FocusAwareScrollView.this.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {

                    boolean allViewsSeen = true;

                    @Override
                    public void onScrollChange(NestedScrollView v, int x, int y, int oldx, int oldy) {

                        for (int index = 0; index < views.size(); index++) {

                            //Change this to adjust criteria
                            float viewSeenPercent = 1;

                            if (!viewSeen[index])
                                viewSeen[index] = handleViewSeenEvent(views.get(index), scrollBounds.bottom, loc[1], viewSeenPercent, onViewSeenListener);

                            if (!viewSeen[index])
                                allViewsSeen = false;
                        }

                        //Remove this if you want continuous callbacks
                        if (allViewsSeen)
                            FocusAwareScrollView.this.setOnScrollChangeListener((NestedScrollView.OnScrollChangeListener) null);
                    }
                });
            }
        }, 500);
    }
}

1

科特林方式;

用于列出滚动视图的滚动并在屏幕上显示子视图时执行操作的扩展。

@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()
            }

0

我知道很晚了。但是我有一个很好的解决方案。以下是用于获取滚动视图中的视图可见性百分比的代码段。

首先,在滚动视图上设置触摸侦听器,以获取滚动停止的回调。

@Override
public boolean onTouch(View v, MotionEvent event) {
    switch ( event.getAction( ) ) {
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            new Handler().postDelayed(new Runnable() {
                @Override
                public void run() {
                    if(mScrollView == null){
                        mScrollView = (ScrollView) findViewById(R.id.mScrollView);
                    }
                    int childCount = scrollViewRootChild.getChildCount();

                    //Scroll view location on screen
                    int[] scrollViewLocation = {0,0};
                    mScrollView.getLocationOnScreen(scrollViewLocation);

                    //Scroll view height
                    int scrollViewHeight = mScrollView.getHeight();
                    for (int i = 0; i < childCount; i++){
                        View child = scrollViewRootChild.getChildAt(i);
                        if(child != null && child.getVisibility() == View.VISIBLE){
                            int[] viewLocation = new int[2];
                            child.getLocationOnScreen(viewLocation);
                            int viewHeight = child.getHeight();
                            getViewVisibilityOnScrollStopped(scrollViewLocation, scrollViewHeight,
                                    viewLocation, viewHeight, (String) child.getTag(), (childCount - (i+1)));
                        }
                    }
                }
            }, 150);
            break;
    }
    return false;
}

在上面的代码片段中,我们获得了针对滚动视图触摸事件的回调,并在停止滚动的回调后的150毫秒后(非必需)发布了一个runnable。在该可运行对象中,我们将获得滚动视图在屏幕上的位置以及滚动视图的高度。然后获取滚动视图的直接子视图组实例并获取子计数。在我的情况下,滚动视图的直接子级是LinearLayout,名为scrollViewRootChild。然后,迭代scrollViewRootChild的所有子视图。在上面的代码片段中,您可以看到我正在以名为viewLocation的整数数组获取孩子在屏幕上的位置,并以变​​量名viewHeight获取视图的高度。然后我调用了一个私有方法getViewVisibilityOnScrollStopped。通过阅读文档,您可以了解此方法的内部工作原理。

/**
 * getViewVisibilityOnScrollStopped
 * @param scrollViewLocation location of scroll view on screen
 * @param scrollViewHeight height of scroll view
 * @param viewLocation location of view on screen, you can use the method of view claas's getLocationOnScreen method.
 * @param viewHeight height of view
 * @param tag tag on view
 * @param childPending number of views pending for iteration.
 */
void getViewVisibilityOnScrollStopped(int[] scrollViewLocation, int scrollViewHeight, int[] viewLocation, int viewHeight, String tag, int childPending) {
    float visiblePercent = 0f;
    int viewBottom = viewHeight + viewLocation[1]; //Get the bottom of view.
    if(viewLocation[1] >= scrollViewLocation[1]) {  //if view's top is inside the scroll view.
        visiblePercent = 100;
        int scrollBottom = scrollViewHeight + scrollViewLocation[1];    //Get the bottom of scroll view 
        if (viewBottom >= scrollBottom) {   //If view's bottom is outside from scroll view
            int visiblePart = scrollBottom - viewLocation[1];  //Find the visible part of view by subtracting view's top from scrollview's bottom  
            visiblePercent = (float) visiblePart / viewHeight * 100;
        }
    }else{      //if view's top is outside the scroll view.
        if(viewBottom > scrollViewLocation[1]){ //if view's bottom is outside the scroll view
            int visiblePart = viewBottom - scrollViewLocation[1]; //Find the visible part of view by subtracting scroll view's top from view's bottom
            visiblePercent = (float) visiblePart / viewHeight * 100;
        }
    }
    if(visiblePercent > 0f){
        visibleWidgets.add(tag);        //List of visible view.
    }
    if(childPending == 0){
        //Do after iterating all children.
    }
}

如果您对此代码有任何改进,请贡献力量。


0

我最终实现了两个Java答案的组合(@ bill-mote https://stackoverflow.com/a/12428154/3686125和@ denys-vasylenko https://stackoverflow.com/a/25528434/3686125)我的项目是一组Kotlin扩展,支持标准的Vertical ScrollView或Horizo​​ntalScrollView控件。

我只是将它们扔到了一个名为Extensions.kt的Kotlin文件中,没有类,只有方法。

我用这些来确定用户停止在项目中的各种滚动视图中滚动时要捕捉到的项目:

fun View.isPartiallyOrFullyVisible(horizontalScrollView: HorizontalScrollView) : Boolean {
    @Suppress("CanBeVal") var scrollBounds = Rect()
    horizontalScrollView.getHitRect(scrollBounds)
    return getLocalVisibleRect(scrollBounds)
}

fun View.isPartiallyOrFullyVisible(scrollView: ScrollView) : Boolean {
    @Suppress("CanBeVal") var scrollBounds = Rect()
    scrollView.getHitRect(scrollBounds)
    return getLocalVisibleRect(scrollBounds)
}

fun View.isFullyVisible(horizontalScrollView: HorizontalScrollView) : Boolean {
    @Suppress("CanBeVal") var scrollBounds = Rect()
    horizontalScrollView.getDrawingRect(scrollBounds)
    val left = x
    val right = left + width
    return scrollBounds.left < left && scrollBounds.right > right
}

fun View.isFullyVisible(scrollView: ScrollView) : Boolean {
    @Suppress("CanBeVal") var scrollBounds = Rect()
    scrollView.getDrawingRect(scrollBounds)
    val top = y
    val bottom = top + height
    return scrollBounds.top < top && scrollBounds.bottom > bottom
}

fun View.isPartiallyVisible(horizontalScrollView: HorizontalScrollView) : Boolean = isPartiallyOrFullyVisible(horizontalScrollView) && !isFullyVisible(horizontalScrollView)
fun View.isPartiallyVisible(scrollView: ScrollView) : Boolean = isPartiallyOrFullyVisible(scrollView) && !isFullyVisible(scrollView)

用法示例,遍历scrollview的LinearLayout子级并记录输出:

val linearLayoutChild: LinearLayout = getChildAt(0) as LinearLayout
val scrollView = findViewById(R.id.scroll_view) //Replace with your scrollview control or synthetic accessor
for (i in 0 until linearLayoutChild.childCount) {
    with (linearLayoutChild.getChildAt(i)) {
        Log.d("ScrollView", "child$i left=$left width=$width isPartiallyOrFullyVisible=${isPartiallyOrFullyVisible(scrollView)} isFullyVisible=${isFullyVisible(scrollView)} isPartiallyVisible=${isPartiallyVisible(scrollView)}")
    }
}

1
为什么要使用var和抑制ide提示?
Filipkowicz

-1

我使用@Qberticus的答案,但要顺便说一句,我编写了一堆代码来检查是否每当滚动视图被调用并滚动时是否触发@Qberticus答案,您可以做任何您想做的事情,在我的情况下,包含视频的社交网络,因此当在屏幕上绘制视图时,我会像Facebook和Instagram一样播放视频。这是代码:

mainscrollview.getViewTreeObserver().addOnScrollChangedListener(new OnScrollChangedListener() {

                    @Override
                    public void onScrollChanged() {
                        //mainscrollview is my scrollview that have inside it a linearlayout containing many child views.
                        Rect bounds = new Rect();
                         for(int xx=1;xx<=postslayoutindex;xx++)
                         {

                          //postslayoutindex is the index of how many posts are read.
                          //postslayoutchild is the main layout for the posts.
                        if(postslayoutchild[xx]!=null){

                            postslayoutchild[xx].getHitRect(bounds);

                        Rect scrollBounds = new Rect();
                        mainscrollview.getDrawingRect(scrollBounds);

                        if(Rect.intersects(scrollBounds, bounds))
                        {
                            vidPreview[xx].startPlaywithoutstoppping();
                         //I made my own custom video player using textureview and initialized it globally in the class as an array so I can access it from anywhere.
                        }
                        else
                        {

                        }


                        }
                    }
                    }
                });
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.