检测 ScrollView 的结束

我有一个应用程序,它有一个活动,使用 ScrollView。我需要检测当用户到底部的 ScrollView。我做了一些谷歌和我发现 这一页解释。但是,在这个例子中,这些家伙扩展了 ScrollView。正如我所说,我需要扩展活动。

因此,我说: “好的,让我们尝试创建一个扩展 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,以便能够证明里面的文本。我想实现的是“接受按钮不激活,直到合同条款完全阅读”的事情。我的扩展类称为 ScrollViewExt。如果我将标记 ScrollView改为 ScrollViewExt,它会抛出一个

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

因为它不明白标签 ScrollViewEx。我不认为它有一个解决方案..。

谢谢你的回答!

123285 次浏览

EDIT

With the content of your XML, I can see that you use a ScrollView. If you want to use your custom view, you must write com.your.packagename.ScrollViewExt and you will be able to use it in your code.

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

EDIT END

Could you post the xml content ?

I think that you could simply add a scroll listener and check if the last item showed is the lastest one from the listview like :

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

Did it!

Aside of the fix Alexandre kindly provide me, I had to create an Interface:

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

Then, i had to override the OnScrollChanged method from ScrollView in my ScrollViewExt:

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

Now, as Alexandre said, put the package name in the XML tag (my fault), make my Activity class implement the interface created before, and then, put it all together:

scroll = (ScrollViewExt) findViewById(R.id.scrollView1);
scroll.setScrollViewListener(this);

And in the method OnScrollChanged, from the interface...

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

And it worked!

Thank you very much for your help, Alexandre!

To determine if you are at the end of your custom ScrollView you could also use a member variable and store the last y-position. Then you can compare the last y-position with the current scroll position.

private int scrollViewPos;


...


@Override
public void onScrollChanged(ScrollViewExt scrollView, int x, int y, int oldx, int oldy) {


//reached end of scrollview
if (y > 0 && scrollViewPos == y){
//do something
}


scrollViewPos = y;
}

Fustigador answer was great, but I found some device (Like Samsung Galaxy Note V) cannot reach 0, have 2 point left, after the calculation. I suggest to add a little buffer like below:

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

You can make use of the Support Library's NestedScrollView and it's NestedScrollView.OnScrollChangeListener interface.

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

Alternatively if your app is targeting API 23 or above, you can make use of the following method on the ScrollView:

View.setOnScrollChangeListener(OnScrollChangeListener listener)

Then follow the example that @Fustigador described in his answer. Note however that as @Will described, you should consider adding a small buffer in case the user or system isn't able to reach the complete bottom of the list for any reason.

Also worth noting is that the scroll change listener will sometimes be called with negative values or values greater than the view height. Presumably these values represent the 'momentum' of the scroll action. However unless handled appropriately (floor / abs) they can cause problems detecting the scroll direction when the view is scrolled to the top or bottom of the range.

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

I found a simple way to detect this :

   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
}
}
});
  • Doesn't need to custom ScrollView.
  • Scrollview can host only one direct child, so scrollView.getChildAt(0) is okay.
  • This solution is right even the height of direct child of scroll view is match_parent or wrap_content.

We should always add scrollView.getPaddingBottom() to match full scrollview height because some time scroll view has padding in xml file so that case its not going to work.

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

Most of answers works beside a fact, that when u scroll to the bottom, listener is triggered several times, which in my case is undesirable. To avoid this behavior I've added flag scrollPositionChanged that checks if scroll position even changed before calling method once again.

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

Then just set listener

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

You may do the same thing if u do in like

scrollView.getViewTreeObserver().addOnScrollChangedListener(...)

but you have to provide flag from class your adding this listener.

All of these answers are so complicated, but there is a simple built-in method that accomplishes this: canScrollVertically(int)

For example:

@Override
public void onScrollChanged() {
if (!scrollView.canScrollVertically(1)) {
// bottom of scroll view
}
if (!scrollView.canScrollVertically(-1)) {
// top of scroll view
}
}

This also works with RecyclerView, ListView, and actually any other view since the method is implemented on View.

If you have a horizontal ScrollView, the same can be achieved with canScrollHorizontally(int)

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




}
}
});

I wanted to show/hide a FAB with an offset before the very bottom of the scrollview. This is the solution I came up with (Kotlin):

scrollview.viewTreeObserver.addOnScrollChangedListener {
if (scrollview.scrollY < scrollview.getChildAt(0).bottom - scrollview.height - offset) {
// fab.hide()
} else {
// fab.show()
}
}

I went through the solutions on the internet. Mostly solutions didn't work in the project I'm working on. Following solutions work fine for me.

Using onScrollChangeListener (works on 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
}
}
});
}

using scrollChangeListener on TreeObserver

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

Hope this solution helps :)

The answer is very simple:

Just add a OnScrollChangedListener to your scrollView (RecyclerView or NestedScrollBar, etc) and implement the onScrollChanged method. For instance, if you use a NestedScrollView:

val nestedScrollView = view.findViewById<NestedScrollView>(R.id.nested_scroll_view_id)
nestedScrollView.viewTreeObserver?.addOnScrollChangedListener {
if (!nestedScrollView.canScrollVertically(1)) {
button.isEnabled = true
}
}

Let me now if you have any problem with this.