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:
- 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 .
- 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) {
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 LayoutManagerUnterklasse, die LinearLayoutManagerzu einem LayoutManagerreibungslosen 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;
}
}