【高级UI】【026】手动为RecyclerView添加橡皮筋拉伸回弹效果

实现思路

  • 控件拉伸回弹,可通过缩放画布来达到,我们只要计算出控件在拉伸时的缩放比例即可

  • 缩放比例可通过手指移动距离来计算,当控件滑动到边界时,手指继续滑动,额外滑动距离/控件总高度,即可作为拉伸比例

  • 控件状态可分为三类,正常滑动状态,手越界滑动时的拉伸状态,手松开时的回弹状态,第一种状态我们使用RecyclerView默认的滑动处理即可

  • 手松开时,我们可以通过一个渐变值动画,来让额外滑动距离逐渐减少到0

  • 回弹过程中欧冠,如果手指重新按下,我们则立刻取消渐变值动画,并让控件重新进入到拉伸状态

  • 控件滑动的临界值,可以通过computeVerticalScrollRange、computeVerticalScrollExtent、computeVerticalScrollOffset这三个方法来计算

核心代码


	package com.android.architecture;
	
	import android.content.Context;
	import android.graphics.Canvas;
	import android.util.AttributeSet;
	import android.view.MotionEvent;
	import android.view.View;
	import android.view.animation.Animation;
	import android.view.animation.Interpolator;
	import android.view.animation.Transformation;
	
	import androidx.core.view.MotionEventCompat;
	import androidx.recyclerview.widget.LinearLayoutManager;
	import androidx.recyclerview.widget.RecyclerView;
	
	//实现橡皮筋拉伸回弹效果
	//支持多指操控,一只手指滑到屏幕边界时,可以通过其它手指继续滑动
	@SuppressWarnings("all")
	public class ElasticListView extends RecyclerView {
	
	    //正常状态
	    private static final int STATE_NORMAL = 0;
	    //沿顶部拉伸
	    private static final int STATE_STRETCHING_TOP = 1;
	    //沿底部拉伸
	    private static final int STATE_STRETCHING_BOTTOM = 2;
	    //回弹
	    private static final int STATE_BOUNCING_BACK = 3;
	
	    //滑动状态
	    int state = STATE_NORMAL;
	
	    //当前起作用的手指
	    int activePointerId = -1;
	
	    //记录上次手指位置
	    float preY;
	
	    //记录手指松开时的拉伸距离
	    float maxOffset;
	
	    //记录当前的拉伸距离,用于回弹动画
	    float nowOffset;
	
	    //回弹动画
	    Animation bounceBackAnimation;
	    Interpolator bounceBackInterpolator;
	
	    public ElasticListView(Context context) {
	        this(context, null);
	    }
	
	    public ElasticListView(Context context, AttributeSet attributeSet) {
	        super(context, attributeSet);
	        init(context, attributeSet);
	    }
	
	    protected void init(Context context, AttributeSet attributeSet) {
	        //设置竖向布局
	        LinearLayoutManager layoutManager = new LinearLayoutManager(context);
	        layoutManager.setOrientation(RecyclerView.VERTICAL);
	        setLayoutManager(layoutManager);
	        //启用默认的越界滚动效果,即滚动到边界时,继续滚动会产生一个水纹效果
	        //手动滚动到边界时,使用橡皮筋拉伸效果,通过惯性自动滚动到边界时,仍然使用默认的水纹效果
	        setOverScrollMode(View.OVER_SCROLL_ALWAYS);
	        //创建回弹动画
	        bounceBackAnimation = new Animation() {
	            @Override
	            protected void applyTransformation(float progress, Transformation transformation) {
	                nowOffset = maxOffset * progress;
	                if (hasEnded()) {
	                    nowOffset = 0;
	                    state = STATE_NORMAL;
	                }
	                invalidate();
	            }
	        };
	        //设置动画插值器
	        bounceBackInterpolator = new Interpolator() {
	            @Override
	            public float getInterpolation(float timePercent) {
	                float progress = (float) Math.cos(Math.PI * timePercent / 2);
	                return progress;
	            }
	        };
	        bounceBackAnimation.setInterpolator(bounceBackInterpolator);
	        bounceBackAnimation.setDuration(300);
	    }
	
	    @Override
	    public void draw(Canvas canvas) {
	        //普通状态下,正常绘制
	        if (state == STATE_NORMAL) {
	            super.draw(canvas);
	            return;
	        }
	        //拉伸或回弹状态下,拉伸画布
	        int saveCount = canvas.save();
	        int height = getHeight();
	        float scale = 1 + Math.abs(nowOffset) / height * 0.3F;
	        canvas.scale(1, scale, 0, nowOffset >= 0 ? 0 : height);
	        super.draw(canvas);
	        canvas.restoreToCount(saveCount);
	    }
	
	    @Override
	    public boolean onInterceptTouchEvent(MotionEvent e) {
	        if (onInterceptTouchEventInternal(e))
	            return true;
	        return super.onInterceptTouchEvent(e);
	    }
	
	    protected boolean onInterceptTouchEventInternal(MotionEvent e) {
	        int action = MotionEventCompat.getActionMasked(e);
	        switch (action) {
	            case MotionEvent.ACTION_DOWN: {
	                preY = e.getY();
	                activePointerId = e.getPointerId(0);
	                //暂停回弹状态,或恢复到正常状态
	                if (state == STATE_BOUNCING_BACK) {
	                    //回弹动画已结束,则恢复到正常状态
	                    if (nowOffset == 0) {
	                        state = STATE_NORMAL;
	                        invalidate();
	                        break;
	                    }
	                    //回弹动画尚未结束,切换到拉伸状态
	                    //如果此时立刻将手松开,会开始一个新的回弹动画,从上次位置继续回弹
	                    clearAnimation();
	                    state = nowOffset > 0 ? STATE_STRETCHING_TOP : STATE_STRETCHING_BOTTOM;
	                    invalidate();
	                }
	                break;
	            }
	        }
	        //如果处于拉伸状态,则自己处理该事件,不分发给子节点
	        //如果处于普通状态,则调用基类方法去处理
	        boolean stretching = isStretching();
	        return stretching;
	    }
	
	    @Override
	    public boolean onTouchEvent(MotionEvent e) {
	        if (onTouchEventInternal(e))
	            return true;
	        return super.onTouchEvent(e);
	    }
	
	    protected boolean onTouchEventInternal(MotionEvent e) {
	        int action = MotionEventCompat.getActionMasked(e);
	        switch (action) {
	
	            case MotionEvent.ACTION_MOVE: {
	                int pointerIndex = e.findPointerIndex(activePointerId);
	                float nowY = e.getY(pointerIndex);
	                float deltaY = nowY - preY;
	                preY = nowY;
	                //非拉伸状态下,需要处理切换到拉伸状态的case
	                if (!isStretching()) {
	                    //判断控件能不能滑动,有没有到达边界
	                    boolean canScrollUp = false;
	                    boolean canScrollDown = false;
	                    //computeVerticalScrollRange返回控件全部内容的高度
	                    //computeVerticalScrollExtent返回控件在屏幕上可展示区域的高度
	                    //computeVerticalScrollOffset返回控件已滑过的距离
	                    int range = computeVerticalScrollRange() - computeVerticalScrollExtent();
	                    if (range == 0) {
	                        //内容较少,已经全部展示,不需要滑动
	                        canScrollUp = false;
	                        canScrollDown = false;
	                    } else {
	                        int offset = computeVerticalScrollOffset();
	                        canScrollUp = offset > 0;
	                        canScrollDown = offset < range;
	                    }
	                    //未到达边界,上下都可以滑动
	                    if (canScrollUp && canScrollDown)
	                        break;
	                    //达到边界时,再继续滑动,即进入越界滚动状态
	                    if (!canScrollUp && deltaY > 0)
	                        state = STATE_STRETCHING_TOP;
	                    if (!canScrollDown && deltaY < 0)
	                        state = STATE_STRETCHING_BOTTOM;
	                }
	                //拉伸状态下,需要处理重绘控件,和切换到非拉伸状态的case
	                if (isStretching()) {
	                    nowOffset = nowOffset + deltaY;
	                    if ((state == STATE_STRETCHING_TOP && nowOffset < 0) || (state == STATE_STRETCHING_BOTTOM && nowOffset > 0)) {
	                        state = STATE_NORMAL;
	                        nowOffset = 0;
	                    }
	                    invalidate();
	                }
	                break;
	            }
	
	            case MotionEventCompat.ACTION_POINTER_DOWN: {
	                int index = MotionEventCompat.getActionIndex(e);
	                preY = e.getY(index);
	                activePointerId = e.getPointerId(index);
	                break;
	            }
	
	            case MotionEventCompat.ACTION_POINTER_UP: {
	                onPointerUp(e);
	                int pointerIndex = e.findPointerIndex(activePointerId);
	                preY = e.getY(pointerIndex);
	                break;
	            }
	
	            case MotionEvent.ACTION_UP:
	            case MotionEvent.ACTION_CANCEL: {
	                if (nowOffset != 0) {
	                    maxOffset = nowOffset;
	                    startAnimation(bounceBackAnimation);
	                    state = STATE_BOUNCING_BACK;
	                }
	            }
	        }
	        //如果处于拉伸状态,则自己处理该事件,不分发给子节点
	        //如果处于普通状态,则调用基类方法去处理
	        boolean stretching = isStretching();
	        return stretching;
	    }
	
	    //多指触控时,有一只手指松开
	    protected void onPointerUp(MotionEvent e) {
	        int pointerIndex = e.getActionIndex();
	        int pointerId = e.getPointerId(pointerIndex);
	        if (pointerId == activePointerId) {
	            //选取一个新的有效手指
	            int newPointerIndex = pointerIndex == 0 ? 1 : 0;
	            activePointerId = e.getPointerId(newPointerIndex);
	        }
	    }
	
	    //判断控件是否处于拉伸状态
	    protected boolean isStretching() {
	        return state == STATE_STRETCHING_TOP || state == STATE_STRETCHING_BOTTOM;
	    }
	}



源码下载

带橡皮筋效果的RecyclerView

你可能感兴趣的:(android-高级UI,android,橡皮筋效果,拉伸回弹效果,自定义控件,RecyclerView)