我最终想出的方案与上面稍有不同。虽然不是理想的解决方案,但对我来说工作得还算可以,可能对其他人也有帮助。我不会接受这个答案,因为我希望其他人能提供更好、更不破坏原有功能的方法(也有可能是我误解了
RecyclerView
实现,错过了一些简单的方法),但在此期间,这个方案已经足够好用了!
实现的基本思路如下:
RecyclerView
中的滚动有点分散在
RecyclerView
和
LinearLayoutManager
之间。我需要处理两种情况:
1. 用户快速滑动视图。默认行为是,
RecyclerView
将该滑动传递给内部的
Scroller
,然后执行滚动效果。这种方式存在问题,因为
RecyclerView
通常无法停留在捕捉位置。为了解决这个问题,我重写了
RecyclerView
的
fling()
实现。我不再使用快速滑动,而是平滑地滚动
LinearLayoutManager
到一个位置。
2. 用户松开手指的速度不足以启动滚动。这种情况下不会发生快速滑动。如果视图未处于捕捉位置,我希望检测到此情况。为了实现这一点,我重写了
onTouchEvent
方法。
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;
}
}
一种用于快速布局管理器的接口:
public interface ISnappyLayoutManager {
int getPositionForVelocity(int velocityX, int velocityY);
int getFixScrollPos();
}
这是一个关于LayoutManager
的示例,它继承了LinearLayoutManager
,从而得到了一个具有流畅滚动效果的LayoutManager
:
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;
}
}