Implementation of onScrollListener to detect the end of scrolling in a ListView
I've a ListView
displaying some items. I'd like to perform some operation on the items that are currently displayed in the visible portion of the ListView
, depending on how the ListView
has been scrolled; thus I thought to implements the OnScrollListener
of the ListView
.
Accordingly to the Android api reference, the onScroll method "will be called after the scroll has completed". This seems to me right what I needed, as once the scroll has completed, I perform my actions on the ListView
(the onScroll method returns the index of the first item displayed a开发者_JAVA百科nd the number of items displayed).
But once implemented, I see from the LogCat
that the onScroll method is not just fired once the scroll has completed, but is fired for every new item that enters the displaying view, from the beginning to the end of the scrolling. This is not the behavior I expect nor I need. The other method of the listener (onScrollStateChanged), instead, does not provide information about the items currently displayed in the ListView
.
So, does anyone know how to use this couple of methods to detect the ending of the scroll and get the information about the displayed items? The misalignment between the api reference and the actual behavior of the method confused me a bit. Thanks in advance.
P.S.: I've seen some similar topics around, but nothing helps me understanding how the whole thing works..!
In the end I've reached a solution not so much elegant but that worked for me; having figured out that onScroll method is called for every step of the scrolling instead of just being called at the scroll end, and that onScrollStateChanged is actually being called only when scrolling is completed, I do something like this:
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
this.currentFirstVisibleItem = firstVisibleItem;
this.currentVisibleItemCount = visibleItemCount;
}
public void onScrollStateChanged(AbsListView view, int scrollState) {
this.currentScrollState = scrollState;
this.isScrollCompleted();
}
private void isScrollCompleted() {
if (this.currentVisibleItemCount > 0 && this.currentScrollState == SCROLL_STATE_IDLE) {
/*** In this way I detect if there's been a scroll which has completed ***/
/*** do the work! ***/
}
}
Practically, everytime the ListView is being scrolled I save the data about the first visible item and the visible item count (onScroll method); when the state of scrolling changes (onScrollStateChanged) I save the state and I call another method which actually understand if there's been a scroll and if it's completed. In this way I also have the data about the visible items that I need. Maybe not clean but works!
Regards
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if (list.getLastVisiblePosition() == list.getAdapter().getCount() - 1
&& list.getChildAt(list.getChildCount() - 1).getBottom() <= list.getHeight()) {
// Scroll end reached
// Write your code here
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if((firstVisibleItem + visibleItemCount) == totalItemCount)
{
Log.i("Info", "Scroll Bottom" );
}
}});
To get this behavior down is tricky, and took me quite a while to perfect. The meat of the problem is that the scroll listeners by themselves are not really quite sufficient to detect a "scroll stop" (including the directional buttons/trackball), as far as I could tell. I ended up doing a combination of things that works right as I expect it.
The best way I figured I could do it was to extend ListView and override a few methods:
....
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || keyCode == KeyEvent.KEYCODE_DPAD_UP) {
startWait();
}
return super.onKeyUp(keyCode, event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
stopWait();
}
if (event.getAction() == MotionEvent.ACTION_UP ||
event.getAction() == MotionEvent.ACTION_CANCEL) {
startWait();
}
return super.onTouchEvent(event);
}
private OnScrollListener customScrollListener = new OnScrollListener() {
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
int totalItemCount) {
stopWait();
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {
startWait();
} else {
stopWait();
}
}
};
//All this waiting could be improved, but that's the idea
private Thread waitThread = null;
private int waitCount = Integer.MIN_VALUE;
public void stopWait() {
waitCount = Integer.MIN_VALUE;
}
public synchronized void startWait() {
waitCount = 0;
if (waitThread != null) {
return;
}
waitThread = new Thread(new Runnable() {
@Override
public void run() {
try {
for (; waitCount.get() < 10; waitCount++) {
Thread.sleep(50);
}
//Kick it back to the UI thread.
view.post(theRunnableWithYourOnScrollStopCode); // HERE IS WHERE YOU DO WHATEVER YOU WANTED TO DO ON STOP
} catch (InterruptedException e) {
} finally {
waitThread = null;
}
}
});
waitThread.start();
}
Note that you also have to bind the customScrollListener
in your constructors. This implementation is nice, I think, because it won't immediately fire the "event", it will wait a bit until it has actually fully stopped scrolling.
Analyzing previous answers, found its own:
In onViewCreated, onCreate or wherever you grab your listview:
ListView list = (ListView) view.findViewById(R.id.[your id]);
list.setOnScrollListener(this);
Then fill in listener methods:
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
// yes, just leave it empty
}
public void onScrollStateChanged(AbsListView view, int scrollState) {
this.isScrollCompleted(view, scrollState);
}
private void isScrollCompleted(AbsListView view, int scrollState) {
if (!view.canScrollVertically(1) && this.currentScrollState == SCROLL_STATE_IDLE) {
// do work
}
}
Seems does what it should: detect end of list without tons of code and modifications of adapter.
Try this one...
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
int totalItemCount) {
int position = firstVisibleItem+visibleItemCount;
int limit = totalItemCount;
// Check if bottom has been reached
if (position >= limit && totalItemCount > 0) {
//scroll end reached
//Write your code here
}
}
Just more useful representation of e-cal's answer
Listener implementation:
public abstract class OnScrollFinishListener implements AbsListView.OnScrollListener {
int mCurrentScrollState;
int mCurrentVisibleItemCount;
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
mCurrentScrollState = scrollState;
if (isScrollCompleted()) {
onScrollFinished();
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
mCurrentVisibleItemCount = visibleItemCount;
}
private boolean isScrollCompleted() {
return mCurrentVisibleItemCount > 0 && mCurrentScrollState == SCROLL_STATE_IDLE;
}
protected abstract void onScrollFinished();
}
Usage:
listView.setOnScrollListener(new OnScrollFinishListener() {
@Override
protected void onScrollFinished() {
// do whatever you need here!
}
});
There is yet one more simple solution. Put a listener to the Adapter of listview and in the getview check if(position== totalItemCount of items in array to be displayed), if yes i call up my listener from adapter and in the Listener Run your operation whatever it be
I experienced a lot of issues when trying to figure out if the List is at the bottom from the OnScrollListener, the best solution I found is to check from the getView method of the adapter if I'm currntly showing the last row:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ActivityView view;
if (convertView==null)
view = (ActivityView)inflater.inflate(R.layout.activity_item, null);
else
view = (ActivityView)convertView;
view.showRow(activitiesList.get(position),position,controller,this);
if(position == activitiesList.size() - 1)
{
Log.i("BOTTOM","reached");
}
return view;
}
there I can easily create an Interface to notify the Fragment/Activity that I reached the bottom.
cheers.
精彩评论