Швидке прокручування в RecyclerView


74

Я намагаюся використовувати новий RecyclerViewклас для сценарію, коли я хочу, щоб компонент прив'язувався до певного елементу під час прокрутки (старий Android Galleryприходить на розум як приклад такого списку з елементом із центральним замком).

Я досі використовую такий підхід:

У мене є інтерфейс, ISnappyLayoutManagerякий містить метод, getPositionForVelocityякий обчислює, в якій позиції подання повинно закінчувати прокрутку, враховуючи початкову швидкість переміщення.

public interface ISnappyLayoutManager {
    int getPositionForVelocity(int velocityX, int velocityY);  
}

Тоді у мене є клас, SnappyRecyclerViewякий підкласи RecyclerViewі замінює свій метод fling () таким чином, щоб перекидати подання точно в потрібній кількості:

public final class SnappyRecyclerView extends RecyclerView {

    /** other methods deleted **/

    @Override
    public boolean fling(int velocityX, int velocityY) {
        LayoutManager lm = getLayoutManager();

        if (lm instanceof ISnappyLayoutManager) {
            super.smoothScrollToPosition(((ISnappyLayoutManager) getLayoutManager())
                    .getPositionForVelocity(velocityX, velocityY));
        }
        return true;
    }
}

Я не дуже задоволений таким підходом з кількох причин. Перш за все, це суперечить філософії "RecyclerView", коли потрібно здійснити підклас його для реалізації певного типу прокрутки. По-друге, якщо я хочу просто використовувати за замовчуванням LinearLayoutManager, це стає дещо складним, оскільки мені доводиться возитися з його внутрішніми елементами, щоб зрозуміти його поточний стан прокрутки і точно розрахувати, куди це прокручується. Нарешті, це навіть не бере до уваги всі можливі сценарії прокрутки, так як якщо ви переміщуєте список, а потім робите паузу, а потім піднімаєте палець, ніякої події кидка не відбувається (швидкість занадто низька), тому список залишається на півдорозі положення. Можливо, про це можна подбати, додавши до прослуховувача стану прокрутки RecyclerView, але це також здається дуже хакі.

Я відчуваю, що мені, мабуть, чогось не вистачає. Чи є кращий спосіб це зробити?

Відповіді:


79

З LinearSnapHelperцим зараз це дуже просто.

Все, що вам потрібно зробити, це:

SnapHelper helper = new LinearSnapHelper();
helper.attachToRecyclerView(recyclerView);

Це так просто! Зверніть увагу, що він LinearSnapHelperбув доданий до бібліотеки підтримки, починаючи з версії 24.2.0.

Це означає, що ви повинні додати це до модуля вашого додатка build.gradle

compile "com.android.support:recyclerview-v7:24.2.0"

Редагувати: AndroidX LinearSnapHelper


4
на жаль, він відривається до середини елемента списку
sativa

10
Варто зазначити, у випадку, якщо хтось має однакові проблеми з цим рішенням, тоді я це зробив: якщо ви отримуєте "IllegalStateException: Екземпляр OnFlingListener вже встановлений" під час налаштування reciclerview, вам слід зателефонувати reciclerView.setOnFlingListener (null); перед snapHelper.attachToRecyclerView (reciclerView);
Аналізатор

Як я можу контролювати швидкість прив'язки за допомогою SnapHelper?
Тайлер Пфафф,

3
@sativa "Реалізація прив'яже центр цільового дочірнього подання до центру приєднаного RecyclerView. Якщо ви маєте намір змінити цю поведінку, тоді перевизначте calcuDistanceToFinalSnap (RecyclerView.LayoutManager, View)."
Джейк

Як програмувати програмно, тому що це не робиться, поки ми не натискаємо або не прокручуємо трохи, будь-які обхідні рішення?
NotABот

62

У підсумку я придумав щось дещо інше, ніж вище. Це не ідеально, але для мене це прийнятно працює, і може бути корисно комусь іншому. Я не прийму цю відповідь, сподіваючись, що хтось інший приходить разом із чимось кращим і менш удачним (і можливо, я не розумію RecyclerViewреалізації та пропускаю якийсь простий спосіб зробити це, але тим часом це досить добре для державної роботи!)

Основи реалізації полягають у наступному: прокрутка в a RecyclerViewє свого роду розподіленою між RecyclerViewі LinearLayoutManager. Мені потрібно розглянути два випадки:

  1. Користувач переглядає подання. Типовою поведінкою є те, що RecyclerViewпередає перекидання до внутрішнього, Scrollerякий потім виконує магію прокрутки. Це проблематично, оскільки тоді, як RecyclerViewправило, осідає в нерозблокованому положенні. Я вирішую це, перекриваючи RecyclerView fling()реалізацію, і замість того, щоб кидати, плавно прокручую LinearLayoutManagerпозицію.
  2. Користувач піднімає палець з недостатньою швидкістю для ініціювання прокрутки. У цьому випадку не відбувається кидання. Я хочу виявити цей випадок у тому випадку, якщо подання не знаходиться у зафіксованому положенні. Я роблю це, замінюючи onTouchEventметод.

The SnappyRecyclerView:

public final class SnappyRecyclerView extends RecyclerView {

    public SnappyRecyclerView(Context context) {
        super(context);
    }

    public SnappyRecyclerView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public SnappyRecyclerView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    public boolean fling(int velocityX, int velocityY) {
        final LayoutManager lm = getLayoutManager();        

      if (lm instanceof ISnappyLayoutManager) {
            super.smoothScrollToPosition(((ISnappyLayoutManager) getLayoutManager())
                    .getPositionForVelocity(velocityX, velocityY));
            return true;
        }
        return super.fling(velocityX, velocityY);
    }        

    @Override
    public boolean onTouchEvent(MotionEvent e) {
        // We want the parent to handle all touch events--there's a lot going on there, 
        // and there is no reason to overwrite that functionality--bad things will happen.
        final boolean ret = super.onTouchEvent(e);
        final LayoutManager lm = getLayoutManager();        

      if (lm instanceof ISnappyLayoutManager
                && (e.getAction() == MotionEvent.ACTION_UP || 
                    e.getAction() == MotionEvent.ACTION_CANCEL)
                && getScrollState() == SCROLL_STATE_IDLE) {
            // The layout manager is a SnappyLayoutManager, which means that the 
            // children should be snapped to a grid at the end of a drag or 
            // fling. The motion event is either a user lifting their finger or 
            // the cancellation of a motion events, so this is the time to take 
            // over the scrolling to perform our own functionality.
            // Finally, the scroll state is idle--meaning that the resultant 
            // velocity after the user's gesture was below the threshold, and 
            // no fling was performed, so the view may be in an unaligned state 
            // and will not be flung to a proper state.
            smoothScrollToPosition(((ISnappyLayoutManager) lm).getFixScrollPos());
        }        

      return ret;
    }
}

Інтерфейс для швидких менеджерів макетів:

/**
 * An interface that LayoutManagers that should snap to grid should implement.
 */
public interface ISnappyLayoutManager {        

    /**
     * @param velocityX
     * @param velocityY
     * @return the resultant position from a fling of the given velocity.
     */
    int getPositionForVelocity(int velocityX, int velocityY);        

    /**
     * @return the position this list must scroll to to fix a state where the 
     * views are not snapped to grid.
     */
    int getFixScrollPos();        

}

І ось приклад того, LayoutManagerщо підкласи the LinearLayoutManagerпризводять LayoutManagerдо плавного прокручування:

public class SnappyLinearLayoutManager extends LinearLayoutManager implements ISnappyLayoutManager {
    // These variables are from android.widget.Scroller, which is used, via ScrollerCompat, by
    // Recycler View. The scrolling distance calculation logic originates from the same place. Want
    // to use their variables so as to approximate the look of normal Android scrolling.
    // Find the Scroller fling implementation in android.widget.Scroller.fling().
    private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)
    private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
    private static double FRICTION = 0.84;

    private double deceleration;

    public SnappyLinearLayoutManager(Context context) {
        super(context);
        calculateDeceleration(context);
    }

    public SnappyLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
        super(context, orientation, reverseLayout);
        calculateDeceleration(context);
    }

    private void calculateDeceleration(Context context) {
        deceleration = SensorManager.GRAVITY_EARTH // g (m/s^2)
                * 39.3700787 // inches per meter
                // pixels per inch. 160 is the "default" dpi, i.e. one dip is one pixel on a 160 dpi
                // screen
                * context.getResources().getDisplayMetrics().density * 160.0f * FRICTION;
    }

    @Override
    public int getPositionForVelocity(int velocityX, int velocityY) {
        if (getChildCount() == 0) {
            return 0;
        }
        if (getOrientation() == HORIZONTAL) {
            return calcPosForVelocity(velocityX, getChildAt(0).getLeft(), getChildAt(0).getWidth(),
                    getPosition(getChildAt(0)));
        } else {
            return calcPosForVelocity(velocityY, getChildAt(0).getTop(), getChildAt(0).getHeight(),
                    getPosition(getChildAt(0)));
        }
    }

    private int calcPosForVelocity(int velocity, int scrollPos, int childSize, int currPos) {
        final double dist = getSplineFlingDistance(velocity);

        final double tempScroll = scrollPos + (velocity > 0 ? dist : -dist);

        if (velocity < 0) {
            // Not sure if I need to lower bound this here.
            return (int) Math.max(currPos + tempScroll / childSize, 0);
        } else {
            return (int) (currPos + (tempScroll / childSize) + 1);
        }
    }

    @Override
    public void smoothScrollToPosition(RecyclerView recyclerView, State state, int position) {
        final LinearSmoothScroller linearSmoothScroller =
                new LinearSmoothScroller(recyclerView.getContext()) {

                    // I want a behavior where the scrolling always snaps to the beginning of 
                    // the list. Snapping to end is also trivial given the default implementation. 
                    // If you need a different behavior, you may need to override more
                    // of the LinearSmoothScrolling methods.
                    protected int getHorizontalSnapPreference() {
                        return SNAP_TO_START;
                    }

                    protected int getVerticalSnapPreference() {
                        return SNAP_TO_START;
                    }

                    @Override
                    public PointF computeScrollVectorForPosition(int targetPosition) {
                        return SnappyLinearLayoutManager.this
                                .computeScrollVectorForPosition(targetPosition);
                    }
                };
        linearSmoothScroller.setTargetPosition(position);
        startSmoothScroll(linearSmoothScroller);
    }

    private double getSplineFlingDistance(double velocity) {
        final double l = getSplineDeceleration(velocity);
        final double decelMinusOne = DECELERATION_RATE - 1.0;
        return ViewConfiguration.getScrollFriction() * deceleration
                * Math.exp(DECELERATION_RATE / decelMinusOne * l);
    }

    private double getSplineDeceleration(double velocity) {
        return Math.log(INFLEXION * Math.abs(velocity)
                / (ViewConfiguration.getScrollFriction() * deceleration));
    }

    /**
     * This implementation obviously doesn't take into account the direction of the 
     * that preceded it, but there is no easy way to get that information without more
     * hacking than I was willing to put into it.
     */
    @Override
    public int getFixScrollPos() {
        if (this.getChildCount() == 0) {
            return 0;
        }

        final View child = getChildAt(0);
        final int childPos = getPosition(child);

        if (getOrientation() == HORIZONTAL
                && Math.abs(child.getLeft()) > child.getMeasuredWidth() / 2) {
            // Scrolled first view more than halfway offscreen
            return childPos + 1;
        } else if (getOrientation() == VERTICAL
                && Math.abs(child.getTop()) > child.getMeasuredWidth() / 2) {
            // Scrolled first view more than halfway offscreen
            return childPos + 1;
        }
        return childPos;
    }

}

1
Чудова відповідь! Також хороша відправна точка для інших особливостей поведінки користувацького прокручування, де ви також хочете налаштувати кінцеву позицію перемикання / прокрутки користувача. Єдине, що Constant.INCHES_PER_METERне існує, тож я собі його встановив 39.3700787.
Mac_Cain13

+1 Насправді це досить добре. Його можна легко розширити для підтримки статичних GridLayouts, замінивши + 1s на + getSpanCount(). Це вимагає трохи більше роботи, щоб обчислити це для динамічних макетів сітки з різними розмірами комірок. Дякую @Catherine!
Себастьян Рот,

Дякую за дійсно корисні заняття. Я виявив, що при великих елементах recyclerView (висотою 1 екран) прокрутка вгору надмірна. Це можна виправити, змінивши рядок методом calcPosForVelocity (): return (int) Math.max(currPos + tempScroll / childSize, 0);наreturn (int) Math.max(currPos + tempScroll / childSize + 2 , 0);
Uwais A

Рішення @UwaisA, згадане для майже повноекранних предметів, також добре працювало для мене.
wblaschko

1
@ ShahrozKhan91 жодної ідеї вибачте, чесно кажучи - я не розумію, як працює клас, просто зрозумів, що це був параметр, який мені потрібно було змінювати, і зробив деякі спроби та помилки.
Uwais A,

14

Мені вдалося знайти більш чистий спосіб зробити це. @Catherine (OP) дайте мені знати, чи можна це покращити, або ви вважаєте, що це покращення у порівнянні з вашим :)

Ось прокручувач, який я використовую.

https://github.com/humblerookie/centerlockrecyclerview/

Тут я пропустив деякі незначні припущення, наприклад, наприклад.

1) Початкове та кінцеве заповнення : Перший та останній елементи в горизонтальній прокрутці повинні мати початкові та остаточні прокладки відповідно встановлені таким чином, щоб початковий та остаточний види були в центрі при прокрутці відповідно до першого та останнього. Наприклад, у onBindViewHolder ви могли б це зробити щось на зразок цього.

@Override
public void onBindViewHolder(ReviewHolder holder, int position) {
holder.container.setPadding(0,0,0,0);//Resetpadding
     if(position==0){
//Only one element
            if(mData.size()==1){
                holder.container.setPadding(totalpaddinginit/2,0,totalpaddinginit/2,0);
            }
            else{
//>1 elements assign only initpadding
                holder.container.setPadding(totalpaddinginit,0,0,0);
            }
        }
        else
        if(position==mData.size()-1){
            holder.container.setPadding(0,0,totalpaddingfinal,0);
        } 
}

 public class ReviewHolder extends RecyclerView.ViewHolder {

    protected TextView tvName;
    View container;

    public ReviewHolder(View itemView) {
        super(itemView);
        container=itemView;
        tvName= (TextView) itemView.findViewById(R.id.text);
    }
}

Логіка досить загальна, і її можна використовувати для багатьох інших випадків. У моєму випадку вигляд утилізатора горизонтальний і розтягується на всю горизонтальну ширину без полів (в основному центр X координат reciclerview є центром екрану) або нерівних відступів.

Якщо хтось стикається з проблемою, будь ласка, прокоментуйте.


13

Мені також потрібен був швидкий вигляд утилізатора. Я хочу дозволити утилізатору переглядати елемент, який прив'язується ліворуч від стовпця. Це закінчилося реалізацією SnapScrollListener, який я встановив у поданні утилізатора. Це мій код:

SnapScrollListener:

class SnapScrollListener extends RecyclerView.OnScrollListener {

    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        if (RecyclerView.SCROLL_STATE_IDLE == newState) {
            final int scrollDistance = getScrollDistanceOfColumnClosestToLeft(mRecyclerView);
            if (scrollDistance != 0) {
                mRecyclerView.smoothScrollBy(scrollDistance, 0);
            }
        }
    }

}

Розрахунок оснащення:

private int getScrollDistanceOfColumnClosestToLeft(final RecyclerView recyclerView) {
    final LinearLayoutManager manager = (LinearLayoutManager) recyclerView.getLayoutManager();
    final RecyclerView.ViewHolder firstVisibleColumnViewHolder = recyclerView.findViewHolderForAdapterPosition(manager.findFirstVisibleItemPosition());
    if (firstVisibleColumnViewHolder == null) {
        return 0;
    }
    final int columnWidth = firstVisibleColumnViewHolder.itemView.getMeasuredWidth();
    final int left = firstVisibleColumnViewHolder.itemView.getLeft();
    final int absoluteLeft = Math.abs(left);
    return absoluteLeft <= (columnWidth / 2) ? left : columnWidth - absoluteLeft;
}

Якщо перший видимий вигляд прокручується на екрані більше половини ширини, наступний видимий стовпець прив'язується ліворуч.

Налаштування слухача:

mRecyclerView.addOnScrollListener(new SnapScrollListener());

1
Чудовий метод, але перед викликом smoothScrollBy () слід перевірити, чи не повертає getScrollDistanceOfColumnClosestToLeft () ненульове значення, або ви отримаєте нескінченні дзвінки onScrollStateChanged (SCROLL_STATE_IDLE).
Михайло Ігнатьєв

8

Ось простіший хак для плавного прокручування до певної позиції у випадку кидка:

@Override
public boolean fling(int velocityX, int velocityY) {

    smoothScrollToPosition(position);
    return super.fling(0, 0);
}

Перевизначте метод перекидання викликом smoothScrollToPosition (int position), де "int position" - це положення вигляду, яке потрібно в адаптері. Вам потрібно буде якось отримати значення позиції, але це залежить від ваших потреб та реалізації.


6

Трохи посварившись із RecyclerView, це те, що я придумав дотепер, і те, що я використовую зараз. У нього є один незначний недолік, але я не буду розливати боби (поки), оскільки ви, мабуть, цього не помітите.

https://gist.github.com/lauw/fc84f7d04f8c54e56d56

Він підтримує лише горизонтальні перегляди та прив'язки до центру, а також може зменшувати подання на основі віддаленості від центру. Використовуйте як заміну RecyclerView.

Редагувати: 08/2016 Зробив це сховище:
https://github.com/lauw/Android-SnappingRecyclerView
Я просто продовжуватиму це, працюючи над кращою реалізацією.


Це рішення ще найкраще, дякую! : D Також, я знайшов вашу квасолю.
Pkmmte

Дякую! Існує невелика затримка при початковому показі SnappingRecyclerView. Перший елемент починається зліва, а потім він показує перший елемент у центрі. Будь-яке рішення для цього?
wouter88

Працює як оберіг! Сподіваючись, реалізація буде змінена на використання сучасних методів, наданихRecyclerView
Akshay Chordiya

@ wouter88 ви можете почати з іншої позиції, такою як: "mSnappingRecyclerView.getLayoutManager (). scrollToPosition (YOUR_POSITION);"
Ali_dev

6

5

Дуже простий підхід для досягнення прискореної поведінки -

    recyclerView.setOnScrollListener(new OnScrollListener() {
        private boolean scrollingUp;

        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            // Or use dx for horizontal scrolling
            scrollingUp = dy < 0;
        }

        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            // Make sure scrolling has stopped before snapping
            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                // layoutManager is the recyclerview's layout manager which you need to have reference in advance
                int visiblePosition = scrollingUp ? layoutManager.findFirstVisibleItemPosition()
                        : layoutManager.findLastVisibleItemPosition();
                int completelyVisiblePosition = scrollingUp ? layoutManager
                        .findFirstCompletelyVisibleItemPosition() : layoutManager
                        .findLastCompletelyVisibleItemPosition();
                // Check if we need to snap
                if (visiblePosition != completelyVisiblePosition) {
                    recyclerView.smoothScrollToPosition(visiblePosition);
                    return;
                }

        }
    });

Єдиним невеликим недоліком є ​​те, що він не защелкнеться назад, коли ви прокрутите менше половини частково видимої комірки - але якщо це вас не турбує, це чисте і просте рішення.


Я не думаю, що це прокрутить до центру. Він (smoothScrollToPosition) просто переносить вид на видиму область.
humblerookie

4

Якщо вам потрібна підтримка прив'язки для початку, зверху, кінця або знизу, використовуйте GravitySnapHelper ( https://github.com/rubensousa/RecyclerViewSnap/blob/master/app/src/main/java/com/github/rubensousa/recyclerviewsnap/GravitySnapHelper .java ).

Прив'язувальний центр:

SnapHelper snapHelper = new LinearSnapHelper();
snapHelper.attachToRecyclerView(recyclerView);

Початок прив'язки з GravitySnapHelper:

startRecyclerView.setLayoutManager(new LinearLayoutManager(this,
                LinearLayoutManager.HORIZONTAL, false));

SnapHelper snapHelperStart = new GravitySnapHelper(Gravity.START);
snapHelperStart.attachToRecyclerView(startRecyclerView);

Закріплення верху за допомогою GravitySnapHelper:

topRecyclerView.setLayoutManager(new LinearLayoutManager(this));

SnapHelper snapHelperTop = new GravitySnapHelper(Gravity.TOP);
snapHelperTop.attachToRecyclerView(topRecyclerView);

3

Я реалізував робоче рішення для горизонтальної орієнтації RecyclerView, яке просто зчитує координати onTouchEvent, на першому MOVE та UP. На UP розрахуйте позицію, до якої нам потрібно перейти.

public final class SnappyRecyclerView extends RecyclerView {

private Point   mStartMovePoint = new Point( 0, 0 );
private int     mStartMovePositionFirst = 0;
private int     mStartMovePositionSecond = 0;

public SnappyRecyclerView( Context context ) {
    super( context );
}

public SnappyRecyclerView( Context context, AttributeSet attrs ) {
    super( context, attrs );
}

public SnappyRecyclerView( Context context, AttributeSet attrs, int defStyle ) {
    super( context, attrs, defStyle );
}


@Override
public boolean onTouchEvent( MotionEvent e ) {

    final boolean ret = super.onTouchEvent( e );
    final LayoutManager lm = getLayoutManager();
    View childView = lm.getChildAt( 0 );
    View childViewSecond = lm.getChildAt( 1 );

    if( ( e.getAction() & MotionEvent.ACTION_MASK ) == MotionEvent.ACTION_MOVE
            && mStartMovePoint.x == 0) {

        mStartMovePoint.x = (int)e.getX();
        mStartMovePoint.y = (int)e.getY();
        mStartMovePositionFirst = lm.getPosition( childView );
        if( childViewSecond != null )
            mStartMovePositionSecond = lm.getPosition( childViewSecond );

    }// if MotionEvent.ACTION_MOVE

    if( ( e.getAction() & MotionEvent.ACTION_MASK ) == MotionEvent.ACTION_UP ){

        int currentX = (int)e.getX();
        int width = childView.getWidth();

        int xMovement = currentX - mStartMovePoint.x;
        // move back will be positive value
        final boolean moveBack = xMovement > 0;

        int calculatedPosition = mStartMovePositionFirst;
        if( moveBack && mStartMovePositionSecond > 0 )
            calculatedPosition = mStartMovePositionSecond;

        if( Math.abs( xMovement ) > ( width / 3 )  )
            calculatedPosition += moveBack ? -1 : 1;

        if( calculatedPosition >= getAdapter().getItemCount() )
            calculatedPosition = getAdapter().getItemCount() -1;

        if( calculatedPosition < 0 || getAdapter().getItemCount() == 0 )
            calculatedPosition = 0;

        mStartMovePoint.x           = 0;
        mStartMovePoint.y           = 0;
        mStartMovePositionFirst     = 0;
        mStartMovePositionSecond    = 0;

        smoothScrollToPosition( calculatedPosition );
    }// if MotionEvent.ACTION_UP

    return ret;
}}

Мені чудово підходить, дайте мені знати, якщо щось не так.


2

Щоб оновити відповідь humblerookie:

Цей прослуховувач прокрутки справді ефективний для центрального блокування https://github.com/humblerookie/centerlockrecyclerview/

Але ось простіший спосіб додати відступ на початку та в кінці перегляду утилізатора, щоб відцентрувати його елементи:

mRecycler.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            int childWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, CHILD_WIDTH_IN_DP, getResources().getDisplayMetrics());
            int offset = (mRecycler.getWidth() - childWidth) / 2;

            mRecycler.setPadding(offset, mRecycler.getPaddingTop(), offset, mRecycler.getPaddingBottom());

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                mRecycler.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            } else {
                mRecycler.getViewTreeObserver().removeGlobalOnLayoutListener(this);
            }
        }
    });

2
Привіт @gomino, радий, що ти знайшов це зручним, але запропоноване вами заповнення зменшить область прокрутки перегляду утилізатора. Однак додавання підбивки до дітей (першої та останньої), однак, могло б дати плід.
humblerookie

Легко @humblerookie просто додати android:clipToPadding="false"і android:clipChildren="false"до recyclerview в файлі макета XML
Gomino

1

І ще одним варіантом очищення є використання користувацьких LayoutManager, ви можете перевірити https://github.com/apptik/multiview/tree/master/layoutmanagers

Він знаходиться в стадії розробки, але працює досить добре. Знімок доступний: https://oss.sonatype.org/content/repositories/snapshots/io/apptik/multiview/layoutmanagers/

Приклад:

recyclerView.setLayoutManager(new SnapperLinearLayoutManager(getActivity()));

Проблема вашого підходу полягає в тому, що все це поширює LinearLayoutManager. Що робити, якщо вам потрібен будь-який інший LayoutManager. У цьому полягає велика перевага приєднання прослуховувача прокрутки та спеціального скролера. Ви можете приєднати його до будь-якого LayoutManager, який хочете. В іншому випадку це було б непоганим рішенням, ваші LayoutManagers хороші та чисті.
Michał K

Дякую! Ви маєте рацію, що наявність прослуховувача прокрутки може бути більш гнучким у багатьох випадках. Я вирішив зробити це для LinearLayoutManager лише тому, що я не знайшов практичної потреби використовувати його для сітки або в похилому стані, і я природно вважаю, що ця поведінка обробляється LayoutManager.
kalin

-1

Мені потрібно було щось трохи інше, ніж усі відповіді вище.

Основними вимогами були такі:

  1. Це працює однаково, коли користувач кидає пальцем або просто відпускає палець.
  2. Використовує власний механізм прокрутки, щоб мати те саме відчуття, що і звичайний RecyclerView.
  3. Коли він зупиняється, він починає плавне прокручування до найближчої точки прив'язки.
  4. Не потрібно використовувати спеціальні LayoutManagerабо RecyclerView. Просто a RecyclerView.OnScrollListener, який потім додається recyclerView.addOnScrollListener(snapScrollListener). Таким чином код стає набагато чистішим.

І дві дуже конкретні вимоги, які слід легко змінити у наведеному нижче прикладі, щоб відповідати вашому випадку:

  1. Працює горизонтально.
  2. Прив'язує лівий край елемента до певної точки в RecyclerView.

Це рішення використовує власний LinearSmoothScroller. Різниця полягає в тому, що на завершальному кроці, коли виявляється "цільовий вигляд", він змінює розрахунок зміщення так, що він прив'язується до певної позиції.

public class SnapScrollListener extends RecyclerView.OnScrollListener {

private static final float MILLIS_PER_PIXEL = 200f;

/** The x coordinate of recycler view to which the items should be scrolled */
private final int snapX;

int prevState = RecyclerView.SCROLL_STATE_IDLE;
int currentState = RecyclerView.SCROLL_STATE_IDLE;

public SnapScrollListener(int snapX) {
    this.snapX = snapX;
}

@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
    super.onScrollStateChanged(recyclerView, newState);
    currentState = newState;
    if(prevState != RecyclerView.SCROLL_STATE_IDLE && currentState == RecyclerView.SCROLL_STATE_IDLE ){
        performSnap(recyclerView);
    }
    prevState = currentState;

}

private void performSnap(RecyclerView recyclerView) {
    for( int i = 0 ;i < recyclerView.getChildCount() ; i ++ ){
        View child = recyclerView.getChildAt(i);
        final int left = child.getLeft();
        int right = child.getRight();
        int halfWidth = (right - left) / 2;
        if (left == snapX) return;
        if (left - halfWidth <= snapX && left + halfWidth >= snapX) { //check if child is over the snapX position
            int adapterPosition = recyclerView.getChildAdapterPosition(child);
            int dx = snapX - left;
            smoothScrollToPositionWithOffset(recyclerView, adapterPosition, dx);
            return;
        }
    }
}

private void smoothScrollToPositionWithOffset(RecyclerView recyclerView, int adapterPosition, final int dx) {
    final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
    if( layoutManager instanceof LinearLayoutManager) {

        LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()) {
            @Override
            public PointF computeScrollVectorForPosition(int targetPosition) {
                return ((LinearLayoutManager) layoutManager).computeScrollVectorForPosition(targetPosition);
            }

            @Override
            protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
                final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference());
                final int distance = (int) Math.sqrt(dx * dx + dy * dy);
                final int time = calculateTimeForDeceleration(distance);
                if (time > 0) {
                    action.update(-dx, -dy, time, mDecelerateInterpolator);
                }
            }

            @Override
            protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
                return MILLIS_PER_PIXEL / displayMetrics.densityDpi;
            }
        };

        scroller.setTargetPosition(adapterPosition);
        layoutManager.startSmoothScroll(scroller);

    }
}

цей код має багато хитрих моментів, де сценарії прокрутки не будуть оброблятися належним чином, а також матимуть різну поведінку на різних екранах. це також суперечить філософії "RecyclerView", де ви фактично обмежуєте можливість користувацького прокрутки.
kalin

1. Які хитрі сценарії? Я не міг придумати жодного, про який би не згадував на самому початку (і з яким легко впоратися, якщо вам потрібно з ним). 2.У нього немає різної поведінки на різних екранах, що змушує вас думати, що він має? 3. Чи можете ви надати мені посилання на філософію RecyclerView і там, де сказано, що нам слід використовувати LayoutManager замість прокрутки? Приєднання скролера є загальнодоступним API, тому я абсолютно не уявляю, про що ви говорите. Чи читали ви код, бо я не думаю;)
Міхал К

0) загалом мені подобається ваша ідея прослуховування прокрутки, оскільки вона в багатьох випадках є більш гнучкою. 1) я не буду детально розглядати, але onScrollStateChanged, який викликає perforSnap, буде викликаний знову за допомогою плавної дії прокрутки, яка може призвести до StackOverflow, з іншого боку, ніколи не перевіряйте орієнтацію, що призводить до серйозних проблем, і враховуючи вищезазначений рекурсивний виклик Переповнення стека. 2) u використовуйте пікселі, щоб отримати позицію, яку краще використовувати%, особливо коли користувач обертає екран, тоді, безсумнівно, не слід очікувати точки прив'язки.
kalin

3) RV супермодульний, і тому він такий хороший. приєднання SmoothScroller доступне, але ви щільно пов’язуєте своє із прив’язуванням, яке видалило цю гнучкість, воно також доступне лише для екземплярів LinearLayoutManager, що позбавляє належної гнучкості вашої ідеї. я прочитав код :) {cheers}
kalin
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.