实现思路
-
控件拉伸回弹,可通过缩放画布来达到,我们只要计算出控件在拉伸时的缩放比例即可 -
缩放比例可通过手指移动距离来计算,当控件滑动到边界时,手指继续滑动,额外滑动距离/控件总高度,即可作为拉伸比例 -
控件状态可分为三类,正常滑动状态,手越界滑动时的拉伸状态,手松开时的回弹状态,第一种状态我们使用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;
if (!isStretching()) {
boolean canScrollUp = false;
boolean canScrollDown = false;
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;
}
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
|