写点什么

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

用户头像
Android架构
关注
发布于: 2021 年 11 月 07 日

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")pub


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


lic void attach() {mRecyclerView.setOnTouchListener(this);mRecyclerView.setOverScrollMode(View.OVER_SCROLL_NEVER);}


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


@Overridepublic 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 的逻辑,主要做些校验工作,如果移动不满足要求,就将事件透出去,具体见下:


@Overridepublic 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,正式处理滑动逻辑,其方法见下:


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


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


@Overridepublic 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 具体看下,之后添加了动画监听,并开启动画:


@Overridepublic void handleTransitionAnim(IDecoratorState fromState) {Animator bounceBackAnim = createAnimator();bounceBackAnim.addListener(this);bounceBackAnim.start();}


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

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Android修炼系列(十二),自定义一个超顺滑的回弹RecyclerView