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

Posted

技术标签:

【中文标题】Android:如何检查 ScrollView 内的 View 是不是可见?【英文标题】:Android: how to check if a View inside of ScrollView is visible?Android:如何检查 ScrollView 内的 View 是否可见? 【发布时间】:2011-06-05 10:54:48 【问题描述】:

我有一个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 内的另一个布局中?这就是我的布局方式,但是当我这样做时,这里给出的答案都对我不起作用。 是的,我的一系列视图位于 LinearLayout 中,它是 ScrollView 的第一个子项。 Qberticus 的回答对我有用。 【参考方案1】:

这行得通:

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

【讨论】:

完美运行。为了更清楚:如果视图完全或部分可见,则返回 true; false 表示视图完全不可见。 [+1] 我使用此代码让GridView/ListView/GridViewWithHeaderSwipeRefreshLayout 一起工作。 有人能解释一下为什么会这样吗? getHitRect 在父坐标中返回一个矩形,但是getLocalVisibleRect 在滚动视图的本地坐标中返回一个矩形,不是吗? 这里不覆盖重叠,如果Child View被另一个子元素重叠,它仍然会返回true 是的,我们需要一个 Rect 的实例。但是是否需要 getHitRect。如果我使用 Rect (0,0-0,0) 有什么不同。我们可以看到 getLocalVisibleRect 调用 getGlobalVisibleRect。并且 Rect 在这里设置 r.set(0, 0, width, height);.@BillMote【参考方案2】:

在您正在测试的视图上使用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);

【讨论】:

我应该在哪里调用这个方法? @Qberticus 如何调用方法?我正在使用它,它总是返回错误。请告诉我 究竟在哪里调用这些方法?【参考方案3】:

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

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;
    

【讨论】:

这是正确答案 =) 在我的情况下,我更改了 if 如下:scrollBounds.top bottom +1 Helton 如果您的视图被推到滚动视图的顶部或底部,您将分别需要 = 你真的测试过这个吗?它总是在最简单的布局 ScrollView 和 TextView 中返回 false。 getHitRect() 和 getDrawingRect() 有什么区别?请指导 此代码仅在视图直接添加到 ScrollView 容器的根目录时才有效。如果您想在子视图等中处理子视图,请查看 Phan Van Linh 的答案。【参考方案4】:

此扩展程序有助于检测完全可见的视图。 如果您的 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 值返回给 FIRST PARENT

2) 下面是关于getDrawingRect 将如何返回的示例 Link

【讨论】:

我想要一个解决方案,如果视图隐藏在键盘下,方法应该返回 false,这可以完成工作。谢谢。【参考方案5】:

我的解决方案是使用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+ @SolidSnake ,不需要导入不同的类,它工作正常【参考方案6】:

要使用 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

【讨论】:

这不起作用..即使部分可见的视图也被归类为完全可见【参考方案7】:
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;
        
    

【讨论】:

这与 ab11 要求的不同。 isShown() 仅检查可见性标志,而不检查视图是否在屏幕的可见区域中。 @Romain Guy 当视图完全滚离屏幕时,代码不包括在内。它应该是` public static int getVisiblePercent(View v) if (v.isShown()) Rect r = 新矩形(); boolean isVisible = v.getGlobalVisibleRect(r); if (isVisible) double sVisible = r.width() * r.height();双 sTotal = v.getWidth() * v.getHeight(); return (int) (100 * sVisible / sTotal); 其他 返回 -1; 其他 返回 -1; `【参考方案8】:

我今天遇到了同样的问题。在谷歌搜索和阅读 android 参考资料时,我发现了这篇文章和我最终使用的方法;

public final boolean getLocalVisibleRect (Rect r)

它们不仅提供 Rect 还提供布尔值来指示 View 是否可见。不利的一面是,这种方法是无证的:(

【讨论】:

这只会告诉您该项目是否设置为可见性(true)。它不会告诉您“可见”项目是否在视口中实际可见。 getLocalVisibleRect 的代码不支持您的说法:` public final boolean getLocalVisibleRect(Rect r) final Point offset = mAttachInfo != null ? mAttachInfo.mPoint:新点(); if (getGlobalVisibleRect(r, offset)) r.offset(-offset.x, -offset.y); // 使 r 本地返回 true; 返回假; `【参考方案9】:

我想检测你的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

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

VISIBLE 这个视图是可见的。与 setVisibility(int) 和 android:visibility 一起使用。

INVISIBLE 这个视图是不可见的,但它仍然占用空间用于布局。与 setVisibility(int) 和 android:visibility 一起使用。

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

【讨论】:

slow clap. OP 想知道的是,假设视图的可见性是 View#VISIBLE,如何知道视图本身是否在滚动视图中可见。 我刚刚检查了一个简单的项目。布局有 ScrollView 和 TextView 作为子级;即使 TextView 完全可见,也总是返回 false。 它总是返回 false。【参考方案10】:

科特林方式;

用于列出滚动视图的滚动并在子视图在屏幕上可见时获取操作的扩展。

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

【讨论】:

【参考方案11】:

您可以使用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);
    

【讨论】:

【参考方案12】:

我最终在我的项目中实现了两个 Java 答案的组合(@bill-mote https://***.com/a/12428154/3686125 和 @denys-vasylenko https://***.com/a/25528434/3686125)作为一组 Kotlin 扩展,它们支持标准的垂直 ScrollView 或 Horizo​​ntalScrollView控制。

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

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

fun View.isPartiallyOrFullyVisible(horizontalScrollView: HorizontalScrollView) : Boolean 
    val scrollBounds = Rect()
    horizontalScrollView.getHitRect(scrollBounds)
    return getLocalVisibleRect(scrollBounds)


fun View.isPartiallyOrFullyVisible(scrollView: ScrollView) : Boolean 
    val scrollBounds = Rect()
    scrollView.getHitRect(scrollBounds)
    return getLocalVisibleRect(scrollBounds)


fun View.isFullyVisible(horizontalScrollView: HorizontalScrollView) : Boolean 
    val 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 
    val 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)

使用示例,遍历滚动视图的 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)")
    

【讨论】:

为什么你使用var 并禁止ide提示? 糟糕,我让 IDE 在转换后更正了这些错误,但没有对其进行审查。谢谢!已编辑。【参考方案13】:

我知道已经很晚了。但我有一个很好的解决方案。下面是获取滚动视图中视图可见度百分比的代码 sn-p。

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

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

在上面的代码 sn-p 中,我们得到了滚动视图触摸事件的回调,并在滚动停止回调后 150 毫秒(非强制性)后发布了一个可运行对象。在该可运行文件中,我们将获得滚动视图在屏幕上的位置和滚动视图高度。然后获取滚动视图的直接子视图组实例并获取子计数。在我的例子中,滚动视图的直接子级是名为 scrollViewRootChild 的 LinearLayout。然后迭代 scrollViewRootChild 的所有子视图。在上面的代码 sn-p 中,你可以看到我在一个名为 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.
    

如果您觉得此代码有任何改进,请贡献。

【讨论】:

【参考方案14】:

我的方式:

scrollView.viewTreeObserver?.addOnScrollChangedListener 
     scrollView.getDrawingRect(Rect())
     myViewInsideScrollView.getLocalVisibleRect(Rect()) 

【讨论】:

【参考方案15】:

使用@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
                        

                        


                        
                    
                    
                );

【讨论】:

以上是关于Android:如何检查 ScrollView 内的 View 是不是可见?的主要内容,如果未能解决你的问题,请参考以下文章

LinearLayout不在ScrollView内滚动[重复]

在 ScrollView 中的 EditText 内滚动 [重复]

在 Scrollview 中滚动时 Android v2 MapFragment 抖动

EditText 在 ScrollView 内不可滚动

带有 TabLayout 的片段内的 ScrollView 不滚动,为啥?

android 如何禁止scrollview 滚动