As is mention in the google issuetracker page on the fourth floor:https://issuetracker.google.com/issues/36952786
The workaround given earlier, "A workaround for now is listening for SCROLL_STATE_IDLE when starting the scroll, and smoothScrollToPositionFromTop again to the same position." won't always work either.
Actually, the call to onScrollStateChanged with SCROLL_STATE_IDLE doesn't necessarily mean the scrolling has finished. As a result, it still can’t guarantee that the Listview scrolls to a correct position every time, especially when list item views are not all in same height.
After researching, I found another approach that work perfectly correctly and reasonably. As is known, Listview provides a method scrollListBy(int y), which enables us to scroll the Listview up with y pixels instantly. Then, with the help of a timer, we can scroll the list smoothly and correctly by ourselves.
The first thing we need to do is computing the height of each list item view, including the views outside the screen. As the list data and the types of child views are already known before, it is feasible to compute the height of each list item view. So, given a target position to scroll to smoothly, we can calculate its scroll distance in y direction. In addition, the calculation should be done after finishing initializing the ListView.
The second thing is combining a timer and the scrollListBy(int) method. Actually we can use the sendEmptyMessageDelayed() method of android.os.Handler. Thus, the solution can 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 finishing initializing our ListView, we invoke the doMeasureOnLayoutChange() method. After that, we can scroll the ListView by the method smoothScrollToPosition(int pos). We can invoke doMeasureOnLayoutChange() method like this:
mListAdapter.notifyDataSetChanged();
mListView.post(new Runnable() {
@Override
public void run() {
mListViewSmoothScroller.doMeasureOnLayoutChange();
}
});
Finally, our ListView can be scrolled to a target position smoothly, and more important, correctly.