Android 列表越界回弹效果实现

一、越界回弹效果的例子
二、效果拆分
  1. 下拉、上拉越界回弹:已到达列表顶部,执行下拉操作,此时会进行越界回弹
  2. 惯性滑动 越界回弹:快速滑动列表,列表已到达底部,但由于惯性会继续前进一部分,此时会根据速度决定越界的距离以及回弹的速度
三、下拉、上拉越界回弹实现方式及代码
  1. 在实际项目中可能在多个页面都需要回弹的效果,所以需要尽可能的减少与需要回弹view的耦合代码。所以可以自定义一个ViewGroup来包裹需要回弹的view。
public class PullOverLayout extends RelativeLayout {
    //允许的越界回弹的高度
    protected float mOverScrollHeight;
    //被包裹的子View
    private View mChildView;
    //~~~~~
    public PullOverLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mOverScrollHeight = DensityUtil.dp2px(context, 240);
    }
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        //获得子控件,也就是被包裹的recycleview等
        mChildView = getChildAt(0);
    }
}

  1. 重写事件拦截方法onInterceptTouchEvent()
  1. 通过竖直和水平方向Move的距离来判断是否拦截该事件
  2. dy大于0为手指向下滑动,dy小于0为手指向上滑动,通过setStatePTD、setStatePBU方法记录Down或者Up
  3. 当被包裹的子view无法滑动时即发生越界时拦截并响应该Move事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mTouchX = ev.getX();
            mTouchY = ev.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            float dx = ev.getX() - mTouchX;
            float dy = ev.getY() - mTouchY;
            if (Math.abs(dx) <= Math.abs(dy)) {
                if (dy > 0 && !ScrollingUtil.canChildScrollUp(mChildView)) {
                    //1、下拉操作
                    LogUtil.d(TAG, "setStatePTD");
                    cp.setStatePTD();
                    return true;
                } else if (dy < 0 && !ScrollingUtil.canChildScrollDown(cp.getContent())) {
                    //2、上拉操作
                    LogUtil.d(TAG, "setStatePBU");
                    cp.setStatePBU();
                    return true;
                }
            }
            break;
    }
    return super.onInterceptTouchEvent(ev);
}
  1. 重写onTouchEvent()方法响应拦截的事件
  1. onTouchEvent用来响应事件,当move和up时,做相应的“越界”操作和“回弹”操作
  2. 下拉和上拉操作分别执行对应的越界和回弹操作即可
@Override
public boolean onTouchEvent(MotionEvent e) {
    switch (e.getAction()) {
        //当发生越界时由viewGroup响应Move事件
        case MotionEvent.ACTION_MOVE:
            float dy = e.getY() - mTouchY;
            if (cp.isStatePTD()) {
                //1、如果是下拉操作的move事件,则执行顶部越界操作
                dy = Math.min(cp.getOsHeight() * 2, dy);
                dy = Math.max(0, dy);
                cp.getAnimProcessor().scrollHeadByMove(dy);
            } else if (cp.isStatePBU()) {
                //2、如果是上拉操作的move事件,则执行底部越界的操作
                dy = Math.min(cp.getOsHeight() * 2, Math.abs(dy));
                dy = Math.max(0, dy);
                cp.getAnimProcessor().scrollBottomByMove(dy);
            }
            return true;
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            if (cp.isStatePTD()) {
                //3、如果是下拉操作,当收到Up事件时,进行释放操作下拉操作,即执行下拉回弹操作
                cp.getAnimProcessor().dealPullDownRelease();
            } else if (cp.isStatePBU()) {
                //4、如果是上拉操作,当收到up事件时,进行释放上拉操作,执行上拉回弹操作
                cp.getAnimProcessor().dealPullUpRelease();
            }
            return true;
    }
    return super.onTouchEvent(e);
}
  1. 第三步onTouchEvent中的下拉越界方法scrollHeadByMove(float moveY)和下拉释放的回弹方法dealPullDownRelease()
public void scrollHeadByMove(float moveY) {
    float offsetY = decelerateInterpolator.getInterpolation(moveY / cp.getOsHeight() / 2) * moveY / 2;
    LogUtil.d(TAG, "scrollHeadByMove ", "offsetY:", String.valueOf(offsetY));
    cp.getHeader().setVisibility(GONE);
    cp.getHeader().getLayoutParams().height = (int) Math.abs(offsetY);
    cp.getHeader().requestLayout();
    cp.getContent().setTranslationY(offsetY);
}
public void dealPullDownRelease() {
    int start = getVisibleHeadHeight();
    int end = 0;
    LogUtil.d(TAG, "dealPullDownRelease  ", "start:", String.valueOf(start), "end:", String.valueOf(end));
    ValueAnimator va = ValueAnimator.ofInt(start, end);
    va.setInterpolator(new DecelerateInterpolator());
    va.addUpdateListener(animHeadUpListener);
    va.setDuration((int) (Math.abs(start - end) * animFraction));
    va.start();
}
四、惯性滑动 越界回弹实现方式及代码
  1. 给被包裹的子view设置监听
  1. 给定一个最低速度的阈值,当滑动速度超过这个值时根据惯性执行越界回弹操作
  2. 下方代码只实现了recycleview的越界回弹,listview类似。即被包裹的子view必须是recycleview
public void initChildViewFlingListener() {
    final View mChildView = cp.getContent();
    //1、给被包裹的子view设置监听事件,监听fling操作,并记录速度
    final GestureDetector gestureDetector = new GestureDetector(cp.getContext(), new GestureDetector.SimpleOnGestureListener() {
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            LogUtil.d(TAG, "onScroll  ", "distanceY:", String.valueOf(distanceY));
            return super.onScroll(e1, e2, distanceX, distanceY);
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            LogUtil.d(TAG, "onFling  ", "velocityY:", String.valueOf(velocityY));
            mVelocityY = velocityY;
            return false;
        }
    });
    mChildView.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            //手势监听的两个任务:1.监听fling动作,获取速度  2.监听滚动状态变化
            return gestureDetector.onTouchEvent(event);
        }
    });
    if (mChildView instanceof RecyclerView) {
        ((RecyclerView) mChildView).addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                    //2、到达越界速度阈值后,执行fling的顶部越界操作
                    if (mVelocityY >= OVER_SCROLL_MIN_VX && ScrollingUtil.isRecyclerViewToTop((RecyclerView) mChildView)) {
                        cp.getAnimProcessor().animOverScrollTop(mVelocityY, cur_delay_times);
                        mVelocityY = 0;
                        cur_delay_times = ALL_DELAY_TIMES;
                    }
                    if (mVelocityY <= -OVER_SCROLL_MIN_VX && ScrollingUtil.isRecyclerViewToBottom((RecyclerView) mChildView)) {
                        cp.getAnimProcessor().animOverScrollBottom(mVelocityY, cur_delay_times);
                        mVelocityY = 0;
                        cur_delay_times = ALL_DELAY_TIMES;
                    }
                }
                super.onScrollStateChanged(recyclerView, newState);
            }
        });
    }
}
  1. 执行越界动画

底部越界和顶部越界代码类似,此处只讲解顶部越界即可

/**
 * 执行顶部越界
 * 越界高度height ∝ vy/computeTimes,此处采用的模型是height=A*(vy + B)/computeTimes
 */
public void animOverScrollTop(float vy, int computeTimes) {
    LogUtil.d(TAG, "animOverScrollTop ", "vy:", String.valueOf(vy), "computeTimes:", String.valueOf(computeTimes));
    if (cp.isOsTopLocked()) return;
    cp.lockOsTop();
    cp.setStatePTD();
    //1、计算越界高度,最大不超过设定的越界高度
    int oh = (int) Math.abs(vy / computeTimes / 2);
    final int overHeight = Math.min(oh, cp.getOsHeight());
    //2、计算越界时间
    final int time = overHeight <= 50 ? 115 : (int) (0.3 * overHeight + 100);
    //3、执行越界操作
    animLayoutByTime(0, overHeight, time, overScrollTopUpListener, new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            //4、执行回弹操作
            animLayoutByTime(overHeight, 0, 2 * time, overScrollTopUpListener, new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    cp.releaseOsTopLock();
                }
            });
        }
    });
}
//最终执行的就是个属性动画
public void animLayoutByTime(int start, int end, long time, AnimatorUpdateListener listener, AnimatorListener animatorListener) {
    ValueAnimator va = ValueAnimator.ofInt(start, end);
    va.setInterpolator(new DecelerateInterpolator());
    va.addUpdateListener(listener);
    va.addListener(animatorListener);
    va.setDuration(time);
    va.start();
}
五、效果

附件。

六、总结

越界回弹的主要原理就是自定义一个ViewGroup,然后1、重写onInterceptTouchEvent、onTouchEvent进行事件拦截,响应Move事件进行越界操作,响应Up事件进行释放执行回弹操作。2、对于惯性滑动使用fling监听,根据速度计算回弹的时间、距离等然后执行属性动画实现越界回弹即可。

七、参考和建议

参考:https://github.com/lcodecorex/TwinklingRefreshLayout
建议:如果仅实现越界回弹操作可以自己实现即可,如果既有回弹操作,又需要有下拉刷新上拉加载操作可以直接使用github上的第三方库即可。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值