ScrollView如何检测滚动到底部?

95
我有一个应用程序,其中有一个使用 ScrollView 的Activity。我需要检测用户何时到达 ScrollView 的底部。我进行了一些谷歌搜索,并找到了这个页面,其中解释了如何实现。但是,在示例中,那个人扩展了 ScrollView 。正如我所说,我需要扩展Activity。
所以,我想:“好吧,让我们尝试创建一个自定义类,扩展 ScrollView ,覆盖 onScrollChanged() 方法,检测滚动结束并相应地处理。”
我做到了,但在这行代码中:
scroll = (ScrollViewExt) findViewById(R.id.scrollView1);

我的代码抛出了 java.lang.ClassCastException异常。我在XML中更改了<ScrollView>标签,但显然它并没有起作用。我的问题是:如果ScrollViewExt 扩展自 ScrollView,为什么会向我抛出一个ClassCastException异常呢?还有没有一种方法可以检测到滚动结束而不需要太多干扰?

谢谢大家。

编辑: 如承诺的那样,这里是我XML文件中相关的代码:

<ScrollView
        android:id="@+id/scrollView1"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >


        <WebView
            android:id="@+id/textterms"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center_horizontal"
            android:textColor="@android:color/black" />

    </ScrollView>

我将其从 TextView 更改为 WebView 以便能够调整文本的对齐方式。我的目标是实现“除非完全阅读合同条款,否则不会激活接受按钮”的效果。我的扩展类称为 ScrollViewEx。如果我将标签 ScrollView 更改为 ScrollViewExt,它将抛出

android.view.InflateException: Binary XML file line #44: Error inflating class ScrollViewExt

因为它不理解标签 ScrollViewEx,所以我不认为有解决方案...

感谢您的回答!


不需要自定义类或复杂扩展。只需使用canScrollVertically(int)即可。 - yuval
13个回答

114

完成了!

除了Alexandre提供的修复之外,我还不得不创建一个接口:

public interface ScrollViewListener {
    void onScrollChanged(ScrollViewExt scrollView, 
                         int x, int y, int oldx, int oldy);
}

然后,我不得不在我的ScrollViewExt中覆盖ScrollView的OnScrollChanged方法:

public class ScrollViewExt extends ScrollView {
    private ScrollViewListener scrollViewListener = null;
    public ScrollViewExt(Context context) {
        super(context);
    }

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

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

    public void setScrollViewListener(ScrollViewListener scrollViewListener) {
        this.scrollViewListener = scrollViewListener;
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        if (scrollViewListener != null) {
            scrollViewListener.onScrollChanged(this, l, t, oldl, oldt);
        }
    }
}

现在,就像Alexandre所说的那样,在XML标签中放入包名(这是我的错),让我的Activity类实现之前创建的接口,然后将它们全部组合起来:
scroll = (ScrollViewExt) findViewById(R.id.scrollView1);
scroll.setScrollViewListener(this);

在方法OnScrollChanged中,从接口...
@Override
public void onScrollChanged(ScrollViewExt scrollView, int x, int y, int oldx, int oldy) {
    // We take the last son in the scrollview
    View view = (View) scrollView.getChildAt(scrollView.getChildCount() - 1);
    int diff = (view.getBottom() - (scrollView.getHeight() + scrollView.getScrollY()));

    // if diff is zero, then the bottom has been reached
    if (diff == 0) {
        // do stuff
    }
}

它起作用了!

非常感谢您的帮助,Alexandre!


@MarkoCakic 请查看此答案以获取滚动视图内容的高度。https://dev59.com/9HA65IYBdhLWcg3w1SRC - Uselessinfo
我有一个问题,我正在动态更新我的滚动视图,所以我不想在scrollChanged上进行此操作,我想检测用户是否向上滚动(关闭自动滚动),如果用户向下滚动到底部(打开自动滚动)... 有人可以帮忙吗? - AL̲̳I
看看这个阿里:http://stackoverflow.com/questions/4414929/android-scrollview-how-to-detect-begin-and-end-of-scrollview?rq=1 - Fustigador
@Fustigator,ScrollViewListener的接口声明放在哪里? - Brabbeldas
你是指这段代码吗?scroll = (ScrollViewExt) findViewById(R.id.scrollView1); scroll.setScrollViewListener(this); 那已经过了一段时间,如果我记得正确的话,它在具有你想要知道何时到达底部的滚动视图的活动中。 - Fustigador
显示剩余8条评论

103

我发现一种简单的方法来检测这个:

   scrollView.getViewTreeObserver()
       .addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
            @Override
            public void onScrollChanged() {
                if (scrollView.getChildAt(0).getBottom()
                     <= (scrollView.getHeight() + scrollView.getScrollY())) {
                    //scroll view is at bottom
                } else {
                    //scroll view is not at bottom
                }
            }
        });
  • 不需要自定义ScrollView。
  • ScrollView只能承载一个直接子View,所以scrollView.getChildAt(0)是可以的。
  • 即使ScrollView的直接子View的高度为match_parentwrap_content,这个解决方案也是正确的。

14
为避免多次调用底部,请将“<=”替换为“==”。 - Harshil Pansare
1
谢谢,我们不应该扩展ScrollView来做这么常见的事情。 - Andrew Koster
当子元素使用边距时,这个方法不起作用。一个解决方法是 scrollview(framelayout(childWithMargin))) - m.reiter
1
肯定应该被接受为答案。 - frogstair
当在RecyclerView中存在ScrollView时,这种方法无法正常工作。 - Aayush Singh
它甚至可以在嵌套滚动视图中使用RecyclerView!谢谢。 - NimaAzhd

88
有一个简单的内置方法可以实现这个功能:canScrollVertically(int) 例如:
@Override
public void onScrollChanged() {
    if (!scrollView.canScrollVertically(1)) {
        // bottom of scroll view
    }
    if (!scrollView.canScrollVertically(-1)) {
        // top of scroll view
    }
}

这也适用于RecyclerView、ListView,实际上任何其他视图,因为该方法已在View上实现。
如果您有一个水平的ScrollView,可以使用canScrollHorizontally(int)来实现相同的效果。

3
为了完整起见,这里是检查是否已经到达右端的代码 !scrollView.canScrollHorizontally(1),如果在最左边则使用 !scrollView.canScrollHorizontally(-1) - keithmaxx

20

Fustigador的回答非常好,但我发现一些设备(如三星Galaxy Note V)在计算后不能达到0,还剩下2个点。我建议可以增加一些缓冲区,例如:

@Override
public void onScrollChanged(ScrollViewExt scrollView, int x, int y, int oldx, int oldy) {
    // We take the last son in the scrollview
    View view = (View) scrollView.getChildAt(scrollView.getChildCount() - 1);
    int diff = (view.getBottom() - (scrollView.getHeight() + scrollView.getScrollY()));

    // if diff is zero, then the bottom has been reached
    if (diff <= 10) {
        // do stuff
    }
}

17
scrollView = (ScrollView) findViewById(R.id.scrollView);

    scrollView.getViewTreeObserver()
            .addOnScrollChangedListener(new 
            ViewTreeObserver.OnScrollChangedListener() {
                @Override
                public void onScrollChanged() {

                    if (!scrollView.canScrollVertically(1)) {
                        // bottom of scroll view
                    }
                    if (!scrollView.canScrollVertically(-1)) {
                        // top of scroll view


                    }
                }
            });

只需添加到这里。如果你希望在滚动到屏幕底部时显示某个视图,但一旦你向上滚动一点,你希望某个视图隐藏,那就使用这个代码。if (!scrollView.canScrollVertically(1)) { // 滚动视图底部 binding.stickyViewShadow.visibility = View.VISIBLE } if (scrollY < oldScrollY) { // 向上滚动 stickyView.visibility = View.GONE } - undefined

10

编辑

根据您提供的XML内容,我可以看到您使用了一个ScrollView。如果您想要使用自定义的视图,您需要编写com.your.packagename.ScrollViewExt,并且您将能够在您的代码中使用它。

<com.your.packagename.ScrollViewExt
    android:id="@+id/scrollView1"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <WebView
        android:id="@+id/textterms"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_horizontal"
        android:textColor="@android:color/black" />

</com.your.packagename.ScrollViewExt>

编辑结束

你能否发布XML内容?

我认为你可以添加一个滚动监听器,并检查最后显示的项是否是ListView中的最新项,例如:

mListView.setOnScrollListener(new OnScrollListener() {      
    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        // TODO Auto-generated method stub      
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem,
            int visibleItemCount, int totalItemCount) {
        // TODO Auto-generated method stub
        if(view.getLastVisiblePosition()==(totalItemCount-1)){
            //dosomething
        }
    }
});

我明天会处理它,现在不在工作状态。但是我看到你建议使用ListView。我没有使用ListView,只是在ScrollView中使用了一个TextView。 - Fustigador
哦,抱歉我想到了ListView。你能发布一下使用setContentViewl的XML文件和你的自定义视图吗? - Alexandre B.

7

我查阅了互联网上的解决方案。在我正在从事的项目中,大多数方案都不起作用。以下这些解决方案对我很管用。

使用onScrollChangeListener(适用于API 23):

   if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        scrollView.setOnScrollChangeListener(new View.OnScrollChangeListener() {
            @Override
            public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {

                int bottom =   (scrollView.getChildAt(scrollView.getChildCount() - 1)).getHeight()-scrollView.getHeight()-scrollY;

                if(scrollY==0){
                    //top detected
                }
                if(bottom==0){
                    //bottom detected
                }
            }
        });
    }

使用TreeObserver的scrollChangeListener
 scrollView.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
        @Override
        public void onScrollChanged() {
            int bottom =   (scrollView.getChildAt(scrollView.getChildCount() - 1)).getHeight()-scrollView.getHeight()-scrollView.getScrollY();

            if(scrollView.getScrollY()==0){
                //top detected
            }
            if(bottom==0) {
                //bottom detected
            }
        }
    });

希望这个解决方案能帮到你 :)

5
你可以利用支持库的 NestedScrollView 和它的 NestedScrollView.OnScrollChangeListener 接口。

https://developer.android.com/reference/android/support/v4/widget/NestedScrollView.html

如果您的应用程序针对API 23或更高版本,请使用以下方法在ScrollView上进行操作:

View.setOnScrollChangeListener(OnScrollChangeListener listener) 

请按照@Fustigador在其回答中描述的示例进行操作。但是,请注意,正如@Will所述,您应考虑添加一个小缓冲区,以防用户或系统由于任何原因无法完全到达列表底部。

还值得注意的是,滚动更改侦听器有时会使用负值或大于视图高度的值调用。据推测,这些值代表滚动操作的“动量”。但是,除非适当处理(floor / abs),否则它们可能会在将视图滚动到范围的顶部或底部时导致问题检测滚动方向。

https://developer.android.com/reference/android/view/View.html#setOnScrollChangeListener(android.view.View.OnScrollChangeListener)


4

我们应该始终添加 scrollView.getPaddingBottom() 来匹配完整的滚动视图高度,因为有时滚动视图在xml文件中具有填充,这种情况下它将不起作用。

scrollView.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
        @Override
        public void onScrollChanged() {
            if (scrollView != null) {
               View view = scrollView.getChildAt(scrollView.getChildCount()-1);
               int diff = (view.getBottom()+scrollView.getPaddingBottom()-(scrollView.getHeight()+scrollView.getScrollY()));

          // if diff is zero, then the bottom has been reached
               if (diff == 0) {
               // do stuff
                }
            }
        }
    });

2

大部分答案都有效,但当您滚动到底部时,监听器会被触发多次,这在我的情况下是不希望的。为了避免这种行为,我添加了一个标志scrollPositionChanged,检查是否有滚动位置的变化再调用方法。

public class EndDetectingScrollView extends ScrollView {
    private boolean scrollPositionChanged = true;

    private ScrollEndingListener scrollEndingListener;

    public interface ScrollEndingListener {
        void onScrolledToEnd();
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);

        View view = this.getChildAt(this.getChildCount() - 1);
        int diff = (view.getBottom() - (this.getHeight() + this.getScrollY()));
        if (diff <= 0) {
            if (scrollPositionChanged) {
                scrollPositionChanged = false;
                if (scrollEndingListener != null) {
                    scrollEndingListener.onScrolledToEnd();
                }
            }
        } else {
            scrollPositionChanged = true;
        }
    }

    public void setScrollEndingListener(ScrollEndingListener scrollEndingListener) {
        this.scrollEndingListener = scrollEndingListener;
    }
}

然后只需设置监听器

scrollView.setScrollEndingListener(new EndDetectingScrollView.ScrollEndingListener() {
    @Override
    public void onScrolledToEnd() {
        //do your stuff here    
    }
});

您如果喜欢的话,可以采用与前文相同的方式来操作。
scrollView.getViewTreeObserver().addOnScrollChangedListener(...)

但是您需要从添加此侦听器的类中提供标志。

不错!在 API 级别 23 之前运行良好! - Abhiroop Nandi Ray

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