Подтвердить что ты не робот

SmoothScrollToPositionFromTop() не всегда работает так, как должно

Я пытаюсь какое-то время работать smoothScrollToPositionFromTop(), но он не всегда перебирает правильную позицию.

У меня есть ListView (с 10 элементами) в макете с 10 кнопками сбоку, поэтому я могу прокручивать каждый элемент в списке. Обычно, когда я прокручиваю одну позицию назад или вперед, она работает нормально, но часто, когда я пытаюсь прокручивать более трех позиций назад или вперед, ListView точно не заканчивается в выбранной позиции. Когда он терпит неудачу, он обычно заканчивается от 0,5 до 1,5 элементов, и на самом деле он не предсказуем, когда прокрутка проваливается.

Я также проверил smoothScrollToPosition после того, как notifyDataSetChanged не работает в android, но это исправление не работает для меня, и я не изменяю никаких данных.

Мне бы очень хотелось автоматически прокручивать выбранные элементы списка, но не могу понять, как это сделать. У кого-нибудь была эта проблема раньше и знает, как ее исправить?

4b9b3361

Ответ 1

Это известная ошибка. См. 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 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;
    }
}

Ответ 2

Вот реализация решения.

    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

Как упоминается на странице google issuetracker на четвертом этаже: https://issuetracker.google.com/issues/36952786

Обходной путь, описанный ранее: "Обходной путь на данный момент - это прослушивание SCROLL_STATE_IDLE при запуске свитка и smoothScrollToPositionFromTop снова в ту же позицию". не всегда будет работать.

Фактически, вызов onScrollStateChanged с помощью SCROLL_STATE_IDLE не обязательно означает, что прокрутка завершена. В результате он все еще не может гарантировать, что Listview каждый раз прокручивается до правильной позиции, особенно если представления элементов списка не одинаковы.

После исследования я нашел другой подход, который работает совершенно правильно и разумно. Как известно, Listview предоставляет метод scrollListBy (int y), который позволяет нам мгновенно прокручивать список List с y пикселями. Затем, с помощью таймера, мы можем прокручивать список плавно и правильно сами.

Первое, что нам нужно сделать, это вычислить высоту каждого представления элемента списка, включая представления за пределами экрана. Поскольку данные списка и типы дочерних представлений уже известны ранее, можно вычислить высоту каждого представления элемента списка. Таким образом, при заданной позиции для прокрутки до плавного, мы можем рассчитать его расстояние прокрутки в направлении y. Кроме того, расчет должен быть выполнен после завершения инициализации ListView.

Во-вторых, это сочетание таймера и метода scrollListBy (int). На самом деле мы можем использовать метод sendEmptyMessageDelayed() android.os.Handler. Таким образом, решение может быть:

/**
 * 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(). После этого мы можем прокрутить ListView с помощью метода smoothScrollToPosition (int pos). Мы можем вызвать метод doMeasureOnLayoutChange() следующим образом:

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

Наконец, наш ListView можно прокручивать в целевую позицию плавно и, что более важно, правильно.