Schnelles Scrollen in RecyclerView

74

Ich versuche, die neue RecyclerViewKlasse für ein Szenario zu verwenden, in dem die Komponente beim Scrollen an einem bestimmten Element ausgerichtet werden soll (das alte Android ist Galleryein Beispiel für eine solche Liste mit einem zentrierten Element).

Dies ist der Ansatz, den ich bisher verfolge:

Ich habe eine Schnittstelle, ISnappyLayoutManagerdie eine Methode enthält getPositionForVelocity, die berechnet, an welcher Position die Ansicht das Scrollen beenden soll, wenn die anfängliche Schleudergeschwindigkeit gegeben ist.

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

Dann habe ich eine Klasse, SnappyRecyclerViewdie RecyclerViewihre fling () -Methode so unterordnet und überschreibt, dass die Ansicht genau um den richtigen Betrag verschoben wird:

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

Ich bin aus mehreren Gründen mit diesem Ansatz nicht sehr zufrieden. Zunächst scheint es der Philosophie von 'RecyclerView' zu widersprechen, es in Unterklassen unterteilen zu müssen, um eine bestimmte Art des Bildlaufs zu implementieren. Zweitens, wenn ich nur die Standardeinstellung verwenden möchte LinearLayoutManager, wird dies etwas komplex, da ich mit den Interna herumspielen muss, um den aktuellen Bildlaufstatus zu verstehen und genau zu berechnen, wohin dieser Bildlauf führt. Schließlich werden nicht einmal alle möglichen Bildlaufszenarien berücksichtigt. Wenn Sie die Liste verschieben und dann pausieren und dann einen Finger heben, tritt kein Schleuderereignis auf (die Geschwindigkeit ist zu niedrig), sodass die Liste auf halber Strecke bleibt Position. Dies kann möglicherweise durch Hinzufügen eines On-Scroll-Status-Listeners zum RecyclerViewbehoben werden, aber das fühlt sich auch sehr hackig an.

Ich habe das Gefühl, dass mir etwas fehlen muss. Gibt es einen besseren Weg, dies zu tun?

Catherine
quelle

Antworten:

79

Mit LinearSnapHelperist das jetzt sehr einfach.

Alles was Sie tun müssen ist Folgendes:

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

So einfach ist das! Beachten Sie, dass dies LinearSnapHelperab Version 24.2.0 in der Support-Bibliothek hinzugefügt wurde.

Das heißt, Sie müssen dies zu Ihrem App-Modul hinzufügen build.gradle

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

Bearbeiten: AndroidX LinearSnapHelper

razzledazzle
quelle
4
Leider rastet es in der Mitte des Listenelements ein
Sativa
8
Wenn jemand die gleichen Probleme mit dieser Lösung hat, habe ich Folgendes getan: Wenn beim Einrichten der recyclerview "IllegalStateException: Eine bereits gesetzte Instanz von OnFlingListener" angezeigt wird, sollten Sie recyclerView.setOnFlingListener (null) aufrufen. vor snapHelper.attachToRecyclerView (recyclerView);
Analizer
Wie kann ich die Geschwindigkeit des Schnappschusses mit dem SnapHelper steuern?
Tyler Pfaff
3
@sativa "Die Implementierung fängt die Mitte der untergeordneten Zielansicht in der Mitte der angehängten RecyclerView ein. Wenn Sie dieses Verhalten ändern möchten, überschreiben Sie berechneDistanceToFinalSnap (RecyclerView.LayoutManager, Ansicht)."
Jake
Wie man programmgesteuert schnappt, weil es nicht gerissen wird, bis wir ein bisschen tippen oder scrollen, irgendwelche Workaround-Leute?
NotABot
62

Am Ende habe ich mir etwas anderes ausgedacht als oben. Es ist nicht ideal, aber es funktioniert akzeptabel gut für mich und kann für jemand anderen hilfreich sein. Ich werde diese Antwort nicht akzeptieren, in der Hoffnung, dass jemand anderes etwas Besseres und weniger Hackiges mitbringt (und es ist möglich, dass ich die RecyclerViewImplementierung falsch verstehe und eine einfache Methode dazu vermisse, aber in der Zwischenzeit ist dies gut genug für die Regierungsarbeit!)

Die Grundlagen der Implementierung sind folgende: Das Scrollen in a RecyclerViewist eine Art Aufteilung zwischen dem RecyclerViewund dem LinearLayoutManager. Es gibt zwei Fälle, die ich behandeln muss:

  1. Der Benutzer wirft die Ansicht. Das Standardverhalten ist, dass der RecyclerViewFling an einen internen übergeben wird, Scrollerder dann die Bildlaufmagie ausführt. Dies ist problematisch, da sich die dann RecyclerViewnormalerweise in einer nicht eingerasteten Position niederlassen. Ich löse dieses RecyclerView fling()Problem, indem ich die Implementierung überschreibe und anstatt zu schleudern, die LinearLayoutManagerPosition glatt streiche .
  2. Der Benutzer hebt seinen Finger mit unzureichender Geschwindigkeit, um einen Bildlauf einzuleiten. In diesem Fall tritt kein Schleudern auf. Ich möchte diesen Fall für den Fall erkennen, dass sich die Ansicht nicht in einer eingerasteten Position befindet. Ich mache das, indem ich die onTouchEventMethode überschreibe .

Die 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;
    }
}

Eine Schnittstelle für bissige Layout-Manager:

/**
 * 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();        

}

Und hier ist ein Beispiel für eine LayoutManagerUnterklasse, die LinearLayoutManagerzu einem LayoutManagerreibungslosen Bildlauf führt:

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

}
Catherine
quelle
1
Gute Antwort! Auch ein guter Ausgangspunkt für andere benutzerdefinierte Bildlaufverhalten, bei denen Sie auch die Endposition des Benutzers anpassen möchten. Das einzige war, dass das Constant.INCHES_PER_METERnicht existiert, also habe ich es 39.3700787mir selbst gesetzt.
Mac_Cain13
+1 Eigentlich ist das ziemlich gut. Es kann leicht erweitert werden, um statische GridLayouts zu unterstützen, indem das + 1s durch ersetzt wird + getSpanCount(). Die Berechnung für dynamische Rasterlayouts mit unterschiedlichen Zellengrößen erfordert etwas mehr Arbeit. Danke @Catherine!
Sebastian Roth
Danke für die wirklich nützlichen Klassen. Ich habe festgestellt, dass das Scrollen nach oben bei großen recyclerView-Elementen (1 Bildschirmgröße) überempfindlich ist. Dies kann behoben werden, indem eine Zeile in der calcPosForVelocity () -Methode geändert wird: return (int) Math.max(currPos + tempScroll / childSize, 0);toreturn (int) Math.max(currPos + tempScroll / childSize + 2 , 0);
Uwais A
Die für @ Vollbild-Artikel erwähnte Lösung @UwaisA hat auch für mich gut funktioniert.
wblaschko
1
@ ShahrozKhan91 Keine Ahnung, sorry, um ehrlich zu sein - ich verstehe nicht, wie die Klasse funktioniert, habe nur herausgefunden, dass dies der Parameter war, den ich variieren musste, und habe einige Versuche und Irrtümer durchgeführt.
Uwais A
14

Ich habe es geschafft, einen saubereren Weg zu finden, dies zu tun. @Catherine (OP) lass es mich wissen, wenn dies verbessert werden kann oder du denkst, dass es eine Verbesserung gegenüber deiner ist :)

Hier ist der Scroll-Listener, den ich benutze.

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

Ich habe hier einige kleinere Annahmen wie z.

1) Anfangs- und Endauffüllung : Für das erste und letzte Element in der horizontalen Bildlaufleiste müssen die Anfangs- und Endauffüllung so eingestellt sein, dass die Anfangs- und Endansicht in der Mitte liegen, wenn zum ersten bzw. letzten Bildlauf gescrollt wird. Zum Beispiel im onBindViewHolder können Sie dies tun etwas wie das.

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

Die Logik ist ziemlich allgemein und man kann sie für viele andere Fälle verwenden. In meinem Fall ist die Recycler-Ansicht horizontal und erstreckt sich über die gesamte horizontale Breite ohne Ränder (im Grunde ist die mittlere X-Koordinate der Recyclerview die Mitte des Bildschirms) oder ungleichmäßige Polsterungen.

Falls jemand vor einem Problem steht, kommentieren Sie dies bitte.

humblerookie
quelle
13

Ich brauchte auch eine bissige Recycler-Ansicht. Ich möchte, dass das Element "Recycler-Ansicht" links von einer Spalte einrastet. Am Ende wurde ein SnapScrollListener implementiert, den ich in der Recycler-Ansicht festgelegt habe. Das ist mein Code:

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

}

Berechnung des Schnappschusses:

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

Wenn die erste sichtbare Ansicht mehr als die halbe Breite des Bildschirms entfernt ist, wird die nächste sichtbare Spalte nach links ausgerichtet.

Hörer einstellen:

mRecyclerView.addOnScrollListener(new SnapScrollListener());
Thomas R.
quelle
1
Tolle Methode, aber bevor Sie SmoothScrollBy () aufrufen, sollten Sie überprüfen, ob getScrollDistanceOfColumnClosestToLeft () einen Wert ungleich Null zurückgibt. Andernfalls erhalten Sie unendlich viele Aufrufe von onScrollStateChanged (SCROLL_STATE_IDLE).
Mihail Ignatiev
8

Hier ist ein einfacherer Hack für das reibungslose Scrollen zu einer bestimmten Position bei einem Fling-Ereignis:

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

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

Überschreiben Sie die Fling-Methode mit einem Aufruf vonoothScrollToPosition (int position), wobei "int position" die Position der gewünschten Ansicht im Adapter ist. Sie müssen den Wert der Position irgendwie ermitteln, dies hängt jedoch von Ihren Anforderungen und Ihrer Implementierung ab.

eDizzle
quelle
6

Nachdem ich ein bisschen mit RecyclerView herumgespielt habe, habe ich mir das bisher ausgedacht und was ich gerade benutze. Es hat einen kleinen Fehler, aber ich werde die Bohnen (noch) nicht verschütten, da Sie es wahrscheinlich nicht bemerken werden.

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

Es unterstützt nur horizontale Recycling-Ansichten und rastet in der Mitte ein. Außerdem können Ansichten verkleinert werden, je nachdem, wie weit sie vom Zentrum entfernt sind. Verwendung als Ersatz für RecyclerView.

Bearbeiten: 08/2016 In ein Repository aufgenommen:
https://github.com/lauw/Android-SnappingRecyclerView
Ich werde dies einfach fortsetzen, während ich an einer besseren Implementierung arbeite.

Lauw
quelle
Diese Lösung ist die bisher beste, danke! : D Außerdem habe ich deine Bohnen gefunden.
Pkmmte
Vielen Dank! Es gibt eine kleine Verzögerung beim ersten Anzeigen der SnappingRecyclerView. Das erste Element beginnt links und zeigt dann das erste Element in der Mitte. Gibt es eine Lösung dafür?
Wouter88
Klappt wunderbar! Hoffnung , die Umsetzung mit modernen Methoden zur Verfügung gestellt von geändert wirdRecyclerView
Akshay Chordiya
@ wouter88 Sie können an einer anderen Position wie dieser beginnen: "mSnappingRecyclerView.getLayoutManager (). scrollToPosition (YOUR_POSITION);"
Ali_dev
5

Ein sehr einfacher Ansatz, um ein Snap-to-Position-Verhalten zu erzielen -

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

        }
    });

Der einzige kleine Nachteil ist, dass es nicht rückwärts schnappt, wenn Sie weniger als die Hälfte der teilweise sichtbaren Zelle scrollen - aber wenn Sie dies nicht stört, ist es eine saubere und einfache Lösung.

Nati Dykstein
quelle
Ich glaube nicht, dass dies in die Mitte scrollen würde. Es (glattScrollToPosition) bringt lediglich den Blick in den sichtbaren Bereich.
Humblerookie
4

Verwenden Sie GravitySnapHelper ( https://github.com/rubensousa/RecyclerViewSnap/blob/master/app/src/main/java/com/github/rubensousa/recyclerviewsnap/GravitySnapel .java ).

Schnappzentrum:

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

Schnappstart mit GravitySnapHelper:

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

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

Schnappverschluss mit GravitySnapHelper:

topRecyclerView.setLayoutManager(new LinearLayoutManager(this));

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

Ich habe eine funktionierende Lösung für die horizontale Ausrichtung von RecyclerView implementiert, die nur die Koordinaten von TouchEvent, First MOVE und UP liest. Berechnen Sie auf UP die Position, zu der wir gehen müssen.

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

Funktioniert gut für mich, lassen Sie mich wissen, wenn etwas nicht stimmt.

Michail L.
quelle
2

So aktualisieren Sie die Antwort von humblerookie:

Dieser Scroll-Listener ist in der Tat für das Centerlocking von https://github.com/humblerookie/centerlockrecyclerview/ wirksam.

Es gibt jedoch eine einfachere Möglichkeit, am Anfang und am Ende der Recyclingansicht Polster hinzuzufügen, um die Elemente zu zentrieren:

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);
            }
        }
    });
Gomino
quelle
2
Hey @gomino, ich bin froh, dass du es praktisch gefunden hast, aber die von dir vorgeschlagene Polsterung würde den Bildlaufbereich der Recycling-Ansicht verkleinern. Das Hinzufügen von Polstern zu den Kindern (erste und letzte) würde jedoch fruchtbar funktionieren.
Humblerookie
Easy @humblerookie einfach hinzufügen android:clipToPadding="false"und android:clipChildren="false"zur Recycling-Ansicht in der XML-Layout-Datei
Gomino
1

Eine weitere saubere Option ist die Verwendung von benutzerdefinierten Optionen. LayoutManagerSie können https://github.com/apptik/multiview/tree/master/layoutmanagers überprüfen

Es befindet sich in der Entwicklung, funktioniert aber recht gut. Ein Schnappschuss ist verfügbar: https://oss.sonatype.org/content/repositories/snapshots/io/apptik/multiview/layoutmanagers/

Beispiel:

recyclerView.setLayoutManager(new SnapperLinearLayoutManager(getActivity()));
Kalin
quelle
Das Problem bei Ihrem Ansatz ist, dass alles LinearLayoutManager erweitert. Was ist, wenn Sie einen anderen LayoutManager benötigen? Dies ist der große Vorteil des Anbringens eines Scroll-Listeners und eines benutzerdefinierten Scrollers. Sie können es an jeden gewünschten LayoutManager anhängen. Ansonsten wäre es eine schöne Lösung, Ihre LayoutManager sind nett und sauber.
Michał K
Danke! Sie haben Recht, dass ein Scroll-Listener in vielen Fällen flexibler sein kann. Ich habe mich nur für LinearLayoutManager entschieden, da ich nicht die praktische Notwendigkeit gefunden habe, es für ein Raster zu verwenden, oder es ohnehin gestaffelt ist, und ich finde es natürlich, dass dieses Verhalten vom LayoutManager behandelt wird.
Kalin
-1

Ich brauchte etwas anderes als alle obigen Antworten.

Die Hauptanforderungen waren:

  1. Es funktioniert genauso, wenn der Benutzer seinen Finger schleudert oder einfach loslässt.
  2. Verwendet den nativen Bildlaufmechanismus, um das gleiche "Gefühl" wie ein normaler zu haben RecyclerView.
  3. Wenn es stoppt, beginnt das reibungslose Scrollen zum nächsten Fangpunkt.
  4. Keine Notwendigkeit, benutzerdefinierte LayoutManageroder zu verwenden RecyclerView. Nur ein RecyclerView.OnScrollListener, der dann von angehängt wird recyclerView.addOnScrollListener(snapScrollListener). Auf diese Weise ist der Code viel sauberer.

Und zwei sehr spezifische Anforderungen, die im folgenden Beispiel leicht zu ändern sein sollten, um zu Ihrem Fall zu passen:

  1. Arbeitet horizontal.
  2. Fängt den linken Rand des Elements an einem bestimmten Punkt in der RecyclerView.

Diese Lösung verwendet die native LinearSmoothScroller. Der Unterschied besteht darin, dass im letzten Schritt, wenn die "Zielansicht" gefunden wird, die Berechnung des Versatzes so geändert wird, dass sie an einer bestimmten Position einrastet.

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

    }
}
Michał K.
quelle
Dieser Code hat viele schwierige Punkte, an denen Bildlaufszenarien nicht korrekt behandelt werden und sich auf verschiedenen Bildschirmen unterschiedlich verhalten. Dies widerspricht auch der Philosophie von 'RecyclerView', bei der Sie die Option für benutzerdefinierte Scroller tatsächlich einschränken.
Kalin
1. Was sind die schwierigen Szenarien? Ich konnte mir keine vorstellen, die ich am Anfang nicht erwähnt habe (und die einfach zu handhaben ist, wenn Sie damit umgehen müssen). 2. Es hat kein unterschiedliches Verhalten auf verschiedenen Bildschirmen. Warum glauben Sie, dass dies der Fall ist? 3. Können Sie mir einen Link zur Philosophie von RecyclerView geben und wo steht, dass wir LayoutManager anstelle von Scroller verwenden sollten? Das Anhängen eines Scrollers erfolgt in einer öffentlichen API, daher habe ich absolut keine Ahnung, wovon Sie sprechen. Hast du den Code gelesen, weil ich das nicht glaube?)
Michał K
0) Im Allgemeinen gefällt mir Ihre Idee, einen Scroll-Listener zu haben, der in vielen Fällen flexibler ist. 1) Ich werde nicht auf Details eingehen, aber onScrollStateChanged, das performSnap aufruft, wird erneut durch die glatte Scroller-Aktion aufgerufen, die zu StackOverflow führen kann Paketüberfluss. 2) Verwenden Sie Pixel, um die Position zu ermitteln. Verwenden Sie% besser, insbesondere wenn der Benutzer den Bildschirm dreht. Dann ist der Fangpunkt definitiv nicht zu erwarten.
Kalin
3) RV ist super modular und deshalb ist es so gut. Das Anhängen von SmoothScroller ist verfügbar, aber Sie verknüpfen Ihren SmoothScroller eng mit dem Snap, der diese Flexibilität beseitigt hat. Es ist auch nur für LinearLayoutManager-Instanzen verfügbar, wodurch die gute Flexibilität Ihrer Idee beseitigt wird. Ich habe den Code gelesen :) {
Prost