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 RecyclerView
Implementierung 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 RecyclerView
ist eine Art Aufteilung zwischen dem RecyclerView
und dem LinearLayoutManager
. Es gibt zwei Fälle, die ich behandeln muss:
- Der Benutzer wirft die Ansicht. Das Standardverhalten ist, dass der
RecyclerView
Fling an einen internen übergeben wird, Scroller
der dann die Bildlaufmagie ausführt. Dies ist problematisch, da sich die dann RecyclerView
normalerweise in einer nicht eingerasteten Position niederlassen. Ich löse dieses RecyclerView
fling()
Problem, indem ich die Implementierung überschreibe und anstatt zu schleudern, die LinearLayoutManager
Position glatt streiche .
- 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
onTouchEvent
Methode ü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) {
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) {
smoothScrollToPosition(((ISnappyLayoutManager) lm).getFixScrollPos());
}
return ret;
}
}
Eine Schnittstelle für bissige Layout-Manager:
public interface ISnappyLayoutManager {
int getPositionForVelocity(int velocityX, int velocityY);
int getFixScrollPos();
}
Und hier ist ein Beispiel für eine LayoutManager
Unterklasse, die LinearLayoutManager
zu einem LayoutManager
reibungslosen Bildlauf führt:
public class SnappyLinearLayoutManager extends LinearLayoutManager implements ISnappyLayoutManager {
private static final float INFLEXION = 0.35f;
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
* 39.3700787
* 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) {
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()) {
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));
}
@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) {
return childPos + 1;
} else if (getOrientation() == VERTICAL
&& Math.abs(child.getTop()) > child.getMeasuredWidth() / 2) {
return childPos + 1;
}
return childPos;
}
}