平滑滚动到指定位置的smoothScrollToPositionFromTop()方法并不总是按照预期工作

24

我已经试了一段时间来让smoothScrollToPositionFromTop()正常工作,但它并不能总是滚动到正确的位置。

我有一个带有10个按钮的布局中的ListView(具有10个项目),因此我可以滚动到列表中的每个项目。通常,当我向前或向后滚动一个位置时,它可以正常工作,但通常当我尝试向前或向后滚动超过3个位置时,ListView无法准确地停在所选的位置上。当它失败时,它通常会停留在0.5至1.5个项目处,而且滚动失败是不可预测的。

我还查看了 smoothScrollToPosition after notifyDataSetChanged not working in android,但这种解决方法对我没有用,而且我也没有改变任何数据。

我真的想自动滚动到所选的列表项,但我现在无法弄清楚如何实现。是否有人以前遇到过这个问题,并知道如何解决?


setSelection()可以使用,但不够流畅。该函数有一些Bug。如果该项在布局中,则不会发生任何事情。我也受到了影响。 - tasomaniac
是的,setSelection() 函数可以正常工作,但不幸的是它没有动画效果。 - mennovogel
我在这里找到了相同的问题:http://code.google.com/p/android/issues/detail?id=36062。目前我使用了他们的解决方案:“_现在的解决方法是,在开始滚动时监听SCROLL_STATE_IDLE,并再次平稳地滚动到相同的位置smoothScrollToPositionFromTop._” - mennovogel
有趣。我会做到的。 - tasomaniac
5个回答

54

这是一个已知的 bug,参见 https://code.google.com/p/android/issues/detail?id=36062

然而,我实现了这个解决方法来处理可能出现的所有边缘情况:

首先调用smothScrollToPositionFromTop(position),然后在滚动完成后调用setSelection(position)。后一个调用通过直接跳转到所需位置来纠正不完整的滚动。这样做,用户仍然有一种被动画式地滚动到此位置的印象。

我在两个帮助方法中实现了这个解决方法:

smoothScrollToPositionFromTop()

public static void smoothScrollToPositionFromTop(final AbsListView view, final int position) {
    View child = getChildAtPosition(view, position);
    // There's 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;
    }
}

1
感谢这个解决方法。View.canScrollVertically(int) 需要 API Level 14。你可以使用 ViewCompat.canScrollVertically(View, int) 来代替。 - jenzz
不确定AdapterView是否已更改,但我在您的getChildAtPosition()方法中使用AbsListview代替它。 - nommer
为了获得更准确的结果,请使用smoothScrollToPosition方法而不是smoothScrollToPositionFromTop。 - Ayman Mahgoub
是的,对我也起作用了,但我需要在postDelay中设置1000毫秒,并且为位置加1。只为某人的参考。[链接]http://stackoverflow.com/questions/30802348/get-listview-item-position-after-soft-keyboard-shown,这是我的问题,我也通过使用这种方法解决了它!谢谢! - LiuWenbin_NO.
1
在SCROLL_STATE_IDLE时使用API级别21中添加的AbsListView的setSelectionFromTop方法,以避免不正确的定位。 - dira
显示剩余3条评论

3
这里是解决方案的实现。

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

smoothScrollToPositionFromTop() 的第二次调用并不总是会滚动。虽然有时它能正常工作。请参考我的答案,了解另一种解决方案。 - Lars Blumberg
第二次调用smoothScrollToPositionFromTop存在起始问题 :( - StErMi

0
在谷歌问题追踪器页面的第四楼中提到:https://issuetracker.google.com/issues/36952786 “之前给出的解决方法是,在开始滚动时监听SCROLL_STATE_IDLE,然后再次smoothScrollToPositionFromTop到同一个位置。”但这种方法不总是有效。
实际上,调用带有SCROLL_STATE_IDLE的onScrollStateChanged并不一定意味着滚动已经完成。因此,它仍然无法保证ListView每次都能滚动到正确的位置,特别是当列表项视图的高度不完全相同时。
经过研究,我发现了另一种完美、正确和合理的方法。众所周知,Listview提供了一个scrollListBy(int y)方法,它可以让我们立即将Listview向上滚动y像素。然后,借助计时器,我们可以自己平滑地正确地滚动列表。

我们需要做的第一件事是计算每个列表项视图的高度,包括屏幕外的视图。由于列表数据和子视图类型已知,因此可以计算每个列表项视图的高度。因此,给定一个平滑滚动到的目标位置,我们可以计算其在y方向上的滚动距离。此外,计算应在完成初始化ListView之后进行。

第二件事是将计时器和scrollListBy(int)方法结合起来。实际上,我们可以使用android.os.Handler的sendEmptyMessageDelayed()方法。因此,解决方案可以是:

/**
 * 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.
    }

}

在初始化我们的ListView之后,我们调用doMeasureOnLayoutChange()方法。之后,我们可以通过smoothScrollToPosition(int pos)方法滚动ListView。我们可以像这样调用doMeasureOnLayoutChange()方法:

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

最后,我们的ListView可以平滑地滚动到目标位置,更重要的是,正确无误。

0

尝试将高度从“wrap_content”更改为“match_parent”

<RecyclerView   
android: layout_height="match_parent" 
... >

smothScrollToPosition(0)     // works ok 

0

这是Lars Blumberg在Kotlin中的答案,包括dira的评论,对我有效。

private fun smoothScrollToPositionFromTop(listView: AbsListView, position: Int, offset: Int) {

    listView.setOnScrollListener(object : AbsListView.OnScrollListener {

        override fun onScroll(
            view: AbsListView?,
            firstVisibleItem: Int,
            visibleItemCount: Int,
            totalItemCount: Int
        ) { }

        override fun onScrollStateChanged(view: AbsListView?, scrollState: Int) {
            view?.setOnScrollListener(null)

            // Fix for scrolling bug.
            if (scrollState == SCROLL_STATE_IDLE) {
                Handler(Looper.getMainLooper()).post {
                    listView.setSelectionFromTop(position, offset)
                }
            }
        }
    })

    // Perform scrolling to position
    Handler(Looper.getMainLooper()).post {
        listView.smoothScrollToPositionFromTop(position, offset)
    }
}

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