SmoothScrollToPositionFromTop () does not always work as it should

I try to work smoothScrollToPositionFromTop () for a while, but it does not always iterate over the correct position.

I have a ListView (with 10 items) in a layout with 10 buttons on the side, so I can scroll through each item in the list. Usually, when I scroll one position backward or forward, it works fine, but often when I try to scroll more than three positions backward or forward, the ListView does not exactly end at the selected position. When it fails, it usually ends with 0.5 to 1.5 elements, and in fact it is not predictable when the scroll fails.

I also checked smoothScrollToPosition after notifyDataSetChanged does not work in android , but this fix does not work for me and I am not modifying any data.

I would really like to automatically scroll through the selected list items, but I can’t figure out how to do this. Has anyone had this problem before and knows how to fix it?

+20
android
Jan 23 '13 at 11:52
source share
3 answers

This is a known bug. See https://code.google.com/p/android/issues/detail?id=36062

However, I applied this workaround, which applies to all edge cases that may occur:

Call smothScrollToPositionFromTop(position) , and then when scrolling is complete, call setSelection(position) . The last call corrects incomplete scrolling, jumping directly to the desired position. At the same time, the user still has the impression that he is scrolling to this position.

I implemented this workaround in two helper methods:

smoothScrollToPositionFromTop ()

 public static void smoothScrollToPositionFromTop(final AbsListView view, final int position) { View child = getChildAtPosition(view, position); // There no need to scroll if child is already at top or view is already scrolled to its end if ((child != null) && ((child.getTop() == 0) || ((child.getTop() > 0) && !view.canScrollVertically(1)))) { return; } view.setOnScrollListener(new AbsListView.OnScrollListener() { @Override public void onScrollStateChanged(final AbsListView view, final int scrollState) { if (scrollState == SCROLL_STATE_IDLE) { view.setOnScrollListener(null); // Fix for scrolling bug new Handler().post(new Runnable() { @Override public void run() { view.setSelection(position); } }); } } @Override public void onScroll(final AbsListView view, final int firstVisibleItem, final int visibleItemCount, final int totalItemCount) { } }); // Perform scrolling to position new Handler().post(new Runnable() { @Override public void run() { view.smoothScrollToPositionFromTop(position, 0); } }); } 

getChildAtPosition ()

 public static View getChildAtPosition(final AdapterView view, final int position) { final int index = position - view.getFirstVisiblePosition(); if ((index >= 0) && (index < view.getChildCount())) { return view.getChildAt(index); } else { return null; } } 
+44
Jan 08 '14 at 2:02 on
source share

Here is the implementation of the solution.

  void smoothScrollToPositionFromTopWithBugWorkAround(final AbsListView listView, final int position, final int offset, final int duration){ //the bug workaround involves listening to when it has finished scrolling, and then //firing a new scroll to the same position. //the bug is the case that sometimes smooth Scroll To Position sort of misses its intended position. //more info here : https://code.google.com/p/android/issues/detail?id=36062 listView.smoothScrollToPositionFromTop(position, offset, duration); listView.setOnScrollListener(new OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { if(scrollState==OnScrollListener.SCROLL_STATE_IDLE){ listView.setOnScrollListener(null); listView.smoothScrollToPositionFromTop(position, offset, duration); } } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { } }); } 
+3
Apr 05 '13 at 8:14
source share

As mentioned on the google issuetracker page on the fourth floor: https://issuetracker.google.com/issues/36952786

The workaround described earlier: "The workaround at the moment is listening to SCROLL_STATE_IDLE when the scroll starts and smoothScrollToPositionFromTop back to the same position." will not always work.

In fact, calling onScrollStateChanged using SCROLL_STATE_IDLE does not necessarily mean that the scrolling is complete. As a result, he still cannot guarantee that the Listview scrolls to the correct position each time, especially if the views of the list items are not the same.

After researching, I found a different approach that works perfectly correctly and reasonably. As you know, Listview provides the scrollListBy (int y) method, which allows us to instantly scroll through the List with y pixels. Then, using the timer, we can scroll the list smoothly and correctly ourselves.

The first thing we need to do is calculate the height of each view of the list item, including views off the screen. Since the list data and types of child views are already known earlier, you can calculate the height of each view of the list item. Thus, for a given position for scrolling to smooth, we can calculate its scroll distance in the y direction. In addition, the calculation should be performed after the initialization of the ListView.

Secondly, it is a combination of a timer and the scrollListBy (int) method. In fact, we can use the sendEmptyMessageDelayed () method android.os.Handler. So the solution could be:

 /** * Created by CaiHaozhong on 2017/9/29. */ public class ListViewSmoothScroller { private final static int MSG_ACTION_SCROLL = 1; private final static int MSG_ACTION_ADJUST = 2; private ListView mListView = null; /* The accumulated height of each list item view */ protected int[] mItemAccumulateHeight = null; protected int mTimeStep = 20; protected int mHeaderViewHeight; private int mPos; private Method mTrackMotionScrollMethod = null; protected int mScrollUnit = 0; protected int mTotalMove = 0; protected int mTargetScrollDis = 0; private Handler mMainHandler = new Handler(Looper.getMainLooper()){ public void handleMessage(Message msg) { int what = msg.what; switch (what){ case MSG_ACTION_SCROLL: { int scrollDis = mScrollUnit; if(mTotalMove + mScrollUnit > mTargetScrollDis){ scrollDis = mTargetScrollDis - mTotalMove; } if(Build.VERSION.SDK_INT >= 19) { mListView.scrollListBy(scrollDis); } else{ if(mTrackMotionScrollMethod != null){ try { mTrackMotionScrollMethod.invoke(mListView, -scrollDis, -scrollDis); }catch(Exception ex){ ex.printStackTrace(); } } } mTotalMove += scrollDis; if(mTotalMove < mTargetScrollDis){ mMainHandler.sendEmptyMessageDelayed(MSG_ACTION_SCROLL, mTimeStep); }else { mMainHandler.sendEmptyMessageDelayed(MSG_ACTION_ADJUST, mTimeStep); } break; } case MSG_ACTION_ADJUST: { mListView.setSelection(mPos); break; } } } }; public ListViewSmoothScroller(Context context, ListView listView){ mListView = listView; mScrollUnit = Tools.dip2px(context, 60); mPos = -1; try { mTrackMotionScrollMethod = AbsListView.class.getDeclaredMethod("trackMotionScroll", int.class, int.class); }catch (NoSuchMethodException ex){ ex.printStackTrace(); mTrackMotionScrollMethod = null; } if(mTrackMotionScrollMethod != null){ mTrackMotionScrollMethod.setAccessible(true); } } /* scroll to a target position smoothly */ public void smoothScrollToPosition(int pos){ if(mListView == null) return; if(mItemAccumulateHeight == null || pos >= mItemAccumulateHeight.length){ return ; } mPos = pos; mTargetScrollDis = mItemAccumulateHeight[pos]; mMainHandler.sendEmptyMessage(MSG_ACTION_SCROLL); } /* call after initializing ListView */ public void doMeasureOnLayoutChange(){ if(mListView == null){ return; } int headerCount = mListView.getHeaderViewsCount(); /* if no list item */ if(mListView.getChildCount() < headerCount + 1){ return ; } mHeaderViewHeight = 0; for(int i = 0; i < headerCount; i++){ mHeaderViewHeight += mListView.getChildAt(i).getHeight(); } View firstListItemView = mListView.getChildAt(headerCount); computeAccumulateHeight(firstListItemView); } /* calculate the accumulated height of each list item */ protected void computeAccumulateHeight(View firstListItemView){ int len = listdata.size();// count of list item mItemAccumulateHeight = new int[len + 2]; mItemAccumulateHeight[0] = 0; mItemAccumulateHeight[1] = mHeaderViewHeight; int currentHeight = mHeaderViewHeight; for(int i = 2; i < len + 2; i++){ currentHeight += getItemHeight(firstListItemView); mItemAccumulateHeight[i] = currentHeight; } } /* get height of a list item. You may need to pass the listdata of the list item as parameter*/ protected int getItemHeight(View firstListItemView){ // Considering the structure of listitem View and the list data in order to calculate the height. } } 

After initializing our ListView, we call the doMeasureOnLayoutChange () method. After that, we can scroll through the ListView using the smoothScrollToPosition (int pos) method. We can call the doMeasureOnLayoutChange () method as follows:

 mListAdapter.notifyDataSetChanged(); mListView.post(new Runnable() { @Override public void run() { mListViewSmoothScroller.doMeasureOnLayoutChange(); } }); 

Finally, our ListView can scroll to the target position smoothly and, more importantly, correctly.

0
Oct 27 '17 at 15:16
source share



All Articles