注册

Android修炼系列(十二),自定义一个超顺滑的回弹RecyclerView

前面写了一个嵌套滑动框架和分析了ViewDragHelper的事件分发,本节主要自定义一个带有回弹效果的RecyclerView,看看事件和动画的配合,这在各大App中都比较常见了,效果如下:



df7e8e06a45d40a88d43321e23cd2726~tplv-k3u1fbpfcp-watermark.image


实现


这是定义的回弹类:OverScrollRecyclerView,其是RecyclerView的子类,并实现了OnTouchListener方法:


public class OverScrollRecyclerView extends RecyclerView implements View.OnTouchListener {

public OverScrollRecyclerView(Context context) {
this(context, null);
}

public OverScrollRecyclerView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public OverScrollRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initParams();
}
}
复制代码

随后会定义一些必要的属性,其中DEFAULT_TOUCH_DRAG_MOVE_RATIO表示滑动的像素数与实际view偏移量的比例,减速系数和时间也都是根据实际效果不断调整的。


```java
public class OverScrollRecyclerView extends RecyclerView implements View.OnTouchListener {

// 下拉与上拉,move px / view Translation
private static final float DEFAULT_TOUCH_DRAG_MOVE_RATIO_FWD = 2f;
private static final float DEFAULT_TOUCH_DRAG_MOVE_RATIO_BCK = 1f;
// 默认减速系数
private static final float DEFAULT_DECELERATE_FACTOR = -2f;
// 最大反弹时间
private static final int MAX_BOUNCE_BACK_DURATION_MS = 800;
private static final int MIN_BOUNCE_BACK_DURATION_MS = 200;

// 初始状态,滑动状态,回弹状态
private IDecoratorState mCurrentState;
private IdleState mIdleState;
private OverScrollingState mOverScrollingState;
private BounceBackState mBounceBackState;

private final OverScrollStartAttributes mStartAttr = new OverScrollStartAttributes();
private float mVelocity;
private final RecyclerView mRecyclerView = this;
...
public OverScrollRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initParams();
}
...
}

复制代码

这是我们的状态接口IDecoratorState,其提供了3个方法,IdleState、OverScrollingState、BounceBackState都是它的具体实现类,符合状态模式的思想:


    protected interface IDecoratorState {
// 处理move事件
boolean handleMoveTouchEvent(MotionEvent event);
// 处理up事件
boolean handleUpTouchEvent(MotionEvent event);
// 事件结束后的动画处理
void handleTransitionAnim(IDecoratorState fromState);
}
复制代码

初始化我们定义的变量,没有什么特殊的操作,只是一些各自属性的赋值,具体见下文:


    private void initParams() {
mBounceBackState = new BounceBackState();
mOverScrollingState = new OverScrollingState();
mCurrentState = mIdleState = new IdleState();
attach();
}
复制代码

这是我们的attach,添加触摸监听,并去掉滚动到边缘的光晕效果:


    @SuppressLint("ClickableViewAccessibility")
public void attach() {
mRecyclerView.setOnTouchListener(this);
mRecyclerView.setOverScrollMode(View.OVER_SCROLL_NEVER);
}
复制代码

核心代码就是事件的监听了,需要我们处理onTouch事件,当手指按下滑动时,此时mCurrentState还处于初始状态,其会执行相应的handleMoveTouchEvent方法:


    @Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
return mCurrentState.handleMoveTouchEvent(event);
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
return mCurrentState.handleUpOrCancelTouchEvent(event);
}
return false;
}
复制代码

这是初始状态IdleState处理move的逻辑,主要做些校验工作,如果移动不满足要求,就将事件透出去,具体见下:


    @Override
public boolean handleMoveTouchEvent(MotionEvent event) {
// 是否符合move要求,不符合不拦截事件
if (!initMotionAttributes(mRecyclerView, mMoveAttr, event)) {
return false;
}
// 在RecyclerView顶部但不能下拉 或 在RecyclerView底部但不能上拉
if (!((isInAbsoluteStart(mRecyclerView) && mMoveAttr.mDir) ||
(isInAbsoluteEnd(mRecyclerView) && !mMoveAttr.mDir))) {
return false;
}
// 保存当前Motion信息
mStartAttr.mPointerId = event.getPointerId(0);
mStartAttr.mAbsOffset = mMoveAttr.mAbsOffset;
mStartAttr.mDir = mMoveAttr.mDir;
// 初始状态->滑动状态
issueStateTransition(mOverScrollingState);
return mOverScrollingState.handleMoveTouchEvent(event);
}
复制代码

这是initMotionAttributes方法,会计算Y方向偏移量,如果满足要求,则为MotionAttributes赋值:


    private boolean initMotionAttributes(View view, MotionAttributes attributes, MotionEvent event) {
if (event.getHistorySize() == 0) {
return false;
}
// 像素偏移量
final float dy = event.getY(0) - event.getHistoricalY(0, 0);
final float dx = event.getX(0) - event.getHistoricalX(0, 0);
if (Math.abs(dy) < Math.abs(dx)) {
return false;
}
attributes.mAbsOffset = view.getTranslationY();
attributes.mDeltaOffset = dy;
attributes.mDir = attributes.mDeltaOffset > 0;
return true;
}
复制代码

这里的isInAbsoluteStart方法用来判断,当前RecyclerView是否不能向下滑动,另一个isInAbsoluteEnd是否不能向上滑动,代码就不展示了:


    private boolean isInAbsoluteStart(View view) {
return !view.canScrollVertically(-1);
}
复制代码

当move事件通过初始状态的校验,则改变状态为滑动态OverScrollingState,正式处理滑动逻辑,其方法见下:


    @Override
public boolean handleMoveTouchEvent(MotionEvent event) {
final OverScrollStartAttributes startAttr = mStartAttr;
// 不是一个触摸点事件,则直接切到回弹状态
if (startAttr.mPointerId != event.getPointerId(0)) {
issueStateTransition(mBounceBackState);
return true;
}

final View view = mRecyclerView;

// 是否符合move要求
if (!initMotionAttributes(view, mMoveAttr, event)) {
return true;
}

// mDeltaOffset: 实际要移动的像素,可以为下拉和上拉设置不同移动比
float deltaOffset = mMoveAttr.mDeltaOffset / (mMoveAttr.mDir == startAttr.mDir
? mTouchDragRatioFwd : mTouchDragRatioBck);
// 计算偏移
float newOffset = mMoveAttr.mAbsOffset + deltaOffset;

// 上拉下拉状态与滑动方向不符,则回到初始状态,并将视图归位
if ((startAttr.mDir && !mMoveAttr.mDir && (newOffset <= startAttr.mAbsOffset)) ||
(!startAttr.mDir && mMoveAttr.mDir && (newOffset >= startAttr.mAbsOffset))) {
translateViewAndEvent(view, startAttr.mAbsOffset, event);
issueStateTransition(mIdleState);
return true;
}

// 不让父类截获move事件
if (view.getParent() != null) {
view.getParent().requestDisallowInterceptTouchEvent(true);
}

// 计算速度
long dt = event.getEventTime() - event.getHistoricalEventTime(0);
if (dt > 0) {
mVelocity = deltaOffset / dt;
}

// 改变控件位置
translateView(view, newOffset);
return true;
}
复制代码

这是translateView方法,改变view相对父布局的偏移量:


    private void translateView(View view, float offset) {
view.setTranslationY(offset);
}
复制代码

当滑动事件结束,手指抬起时,会将状态由滑动状态切换为回弹状态:


    @Override
public boolean handleUpTouchEvent(MotionEvent event) {
// 事件up切换状态,有滑动态-回弹态
issueStateTransition(mBounceBackState);
return false;
}
复制代码

上文提到的issueStateTransition方法,只是说切换了状态,但实际上它还会执行handleTransitionAnim的操作,只不过初始状态和滑动状态此接口都是空实现,只有回弹状态才会去处理动画效果罢了:


    protected void issueStateTransition(IDecoratorState state) {
IDecoratorState oldState = mCurrentState;
mCurrentState = state;
// 处理回弹动画效果
mCurrentState.handleTransitionAnim(oldState);
}
复制代码

这是我们处理动画效果的方法,核心方法createAnimator具体看下,之后添加了动画监听,并开启动画:


    @Override
public void handleTransitionAnim(IDecoratorState fromState) {
Animator bounceBackAnim = createAnimator();
bounceBackAnim.addListener(this);
bounceBackAnim.start();
}
复制代码

这是动画创建的核心类,使用了属性动画,先由当前速度mVelocity->0,随后回弹slowdownEndOffset->mStartAttr.mAbsOffset,具体代码见下:


    private Animator createAnimator() {
initAnimationAttributes(view, mAnimAttributes);

// 速度为0了或手势记录的状态与mDir不符合,直接回弹
if (mVelocity == 0f || (mVelocity < 0 && mStartAttr.mDir) || (mVelocity > 0 && !mStartAttr.mDir)) {
return createBounceBackAnimator(mAnimAttributes.mAbsOffset);
}

// 速度减到0,即到达最大距离时,需要的动画事件
float slowdownDuration = (0 - mVelocity) / mDecelerateFactor;
slowdownDuration = (slowdownDuration < 0 ? 0 : slowdownDuration);

// 速度减到0,动画的距离,dx = (Vt^2 - Vo^2) / 2a
float slowdownDistance = -mVelocity * mVelocity / mDoubleDecelerateFactor;
float slowdownEndOffset = mAnimAttributes.mAbsOffset + slowdownDistance;

// 开始动画,减速->回弹
ObjectAnimator slowdownAnim = createSlowdownAnimator(view, (int) slowdownDuration, slowdownEndOffset);
ObjectAnimator bounceBackAnim = createBounceBackAnimator(slowdownEndOffset);
AnimatorSet wholeAnim = new AnimatorSet();
wholeAnim.playSequentially(slowdownAnim, bounceBackAnim);
return wholeAnim;
}
复制代码

这是具体的减速动画方法,设置时间和差值器,就不细说了,不是本文的重点,直接见代码吧:


    private ObjectAnimator createSlowdownAnimator(View view, int slowdownDuration, float slowdownEndOffset) {
ObjectAnimator slowdownAnim = ObjectAnimator.ofFloat(view, mAnimAttributes.mProperty, slowdownEndOffset);
slowdownAnim.setDuration(slowdownDuration);
slowdownAnim.setInterpolator(mBounceBackInterpolator);
slowdownAnim.addUpdateListener(this);
return slowdownAnim;
}
复制代码

同样这是回弹动画的方法,设置时间和差值器,添加监听等,代码见下:


    private ObjectAnimator createBounceBackAnimator(float startOffset) {
float bounceBackDuration = (Math.abs(startOffset) / mAnimAttributes.mMaxOffset) * MAX_BOUNCE_BACK_DURATION_MS;
ObjectAnimator bounceBackAnim = ObjectAnimator.ofFloat(view, mAnimAttributes.mProperty, mStartAttr.mAbsOffset);
bounceBackAnim.setDuration(Math.max((int) bounceBackDuration, MIN_BOUNCE_BACK_DURATION_MS));
bounceBackAnim.setInterpolator(mBounceBackInterpolator);
bounceBackAnim.addUpdateListener(this);
return bounceBackAnim;
}
复制代码

当动画结束的时候,会将状态由回弹模式切换为初始状态,代码见下:


    @Override
public void onAnimationEnd(Animator animation) {
// 动画结束改变状态
issueStateTransition(mIdleState);
}
复制代码

好了,到这里核心逻辑就结束啦,应该不难理解吧。如果讲的不好,博客的栗子我都上传到了gitHub上,感兴趣的可以直接下载看下。



本文到这里,关于回弹效果的实现就结束了。如果本文对你有用,来点个赞吧,大家的肯定也是阿呆i坚持写作的动力。


作者:Battler
链接:https://juejin.cn/post/6953640372467662879
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

1 个评论

66666

要回复文章请先登录注册