先看看第二个ScrollView的效果图(实现一个带弹性效果的仿小米MIUI的ScrollView)(第一个ScrollView的效果图先欠着吧):
一.这里设计一个自定义View,继承了ScrollView,实现可以下拉里面的内容,松手后画面弹回,这个自定义的View可以当做ScrollView来使用
1.主要逻辑代码:
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.TranslateAnimation;
import android.widget.ScrollView;
/**
* 实现了可以有下拉弹回的ScrollView的自定义View
*/
public class SpringScrollView extends ScrollView {
private View inner;// 孩子
private float y;// 坐标
private Rect normal = new Rect();// 矩形空白
public SpringScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
/***
* 根据 XML 生成视图工作完成.该函数在生成视图的最后调用,在所有子视图添加完之后. 即使子类覆盖了 onFinishInflate
* 方法,也应该调用父类的方法,使该方法得以执行.
*/
@Override
protected void onFinishInflate() {
if (getChildCount() > 0) {
inner = getChildAt(0);// 获取其孩子
}
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (inner != null) {
commOnTouchEvent(ev);
}
return super.onTouchEvent(ev);
}
/***
* 触摸事件
*
* @param ev
*/
public void commOnTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
y = ev.getY();// 获取点击y坐标
break;
case MotionEvent.ACTION_UP:
if (isNeedAnimation()) {
animation();
}
break;
case MotionEvent.ACTION_MOVE:
final float preY = y;
float nowY = ev.getY();
int deltaY = (int) (preY - nowY);// 获取滑动距离
y = nowY;
// 当滚动到最上或者最下时就不会再滚动,这时移动布局
if (isNeedMove()) {
if (normal.isEmpty()) {
// 填充矩形,目的:就是告诉this:我现在已经有了,你松开的时候记得要执行回归动画.
normal.set(inner.getLeft(), inner.getTop(),
inner.getRight(), inner.getBottom());
}
// 移动布局
inner.layout(inner.getLeft(), inner.getTop() - deltaY / 2,
inner.getRight(), inner.getBottom() - deltaY / 2);
}
break;
default:
break;
}
}
/***
* 开启动画移动
*/
public void animation() {
// 开启移动动画
TranslateAnimation ta = new TranslateAnimation(0, 0, inner.getTop(),
normal.top);
ta.setDuration(300);
inner.startAnimation(ta);
// 设置回到正常的布局位置
inner.layout(normal.left, normal.top, normal.right, normal.bottom);
normal.setEmpty();// 清空矩形
}
/***
* 是否需要开启动画
*
* 如果矩形不为空,返回true,否则返回false.
*
* @return
*/
public boolean isNeedAnimation() {
return !normal.isEmpty();
}
/***
* 是否需要移动布局 inner.getMeasuredHeight():获取的是控件的高度
* getHeight():获取的是当前控件在屏幕中显示的高度
*
* @return
*/
public boolean isNeedMove() {
int offset = inner.getMeasuredHeight() - getHeight();
int scrollY = getScrollY();
// 0是顶部,后面那个是底部
if (scrollY == 0 || scrollY == offset) {
return true;
}
return false;
}
}
2.布局 :
二.实现一个带弹性效果的仿小米MIUI的ScrollView
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.view.animation.DecelerateInterpolator;
import android.widget.ScrollView;
public class RubberScrollView extends ScrollView {
/**
* 是否恢复原状
*/
private boolean isRestoring;
/**
* 手指接触点id
*/
private int mActivePointerId;
/**
* 手指按下时y坐标
*/
private float mInitialMotionY;
/**
* 是否被拖动
*/
private boolean isBeingDragged;
/**
* 缩放比例
*/
private float mScale;
/**
* 滑动的距离
*/
private float mDistance;
/**
* 最小移动距离
*/
private int mTouchSlop;
public RubberScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
int action = MotionEventCompat.getActionMasked(event);
if (isRestoring && action == MotionEvent.ACTION_DOWN) {
isRestoring = false;
}
if (!isEnabled() || isRestoring || (!isScrollToTop() && !isScrollToBottom())) {
return super.onInterceptTouchEvent(event);
}
switch (action) {
case MotionEvent.ACTION_DOWN: {
mActivePointerId = event.getPointerId(0);
isBeingDragged = false;
float initialMotionY = getMotionEventY(event);
if (initialMotionY == -1) {
return super.onInterceptTouchEvent(event);
}
mInitialMotionY = initialMotionY;
break;
}
case MotionEvent.ACTION_MOVE: {
if (mActivePointerId == MotionEvent.INVALID_POINTER_ID) {
return super.onInterceptTouchEvent(event);
}
final float y = getMotionEventY(event);
if (y == -1f) {
return super.onInterceptTouchEvent(event);
}
if (isScrollToTop() && !isScrollToBottom()) {
// 在顶部不在底部
float yDiff = y - mInitialMotionY;
if (yDiff > mTouchSlop && !isBeingDragged) {
isBeingDragged = true;
}
} else if (!isScrollToTop() && isScrollToBottom()) {
// 在底部不在顶部
float yDiff = mInitialMotionY - y;
if (yDiff > mTouchSlop && !isBeingDragged) {
isBeingDragged = true;
}
} else if (isScrollToTop() && isScrollToBottom()) {
// 在底部也在顶部
float yDiff = y - mInitialMotionY;
if (Math.abs(yDiff) > mTouchSlop && !isBeingDragged) {
isBeingDragged = true;
}
} else {
// 不在底部也不在顶部
return super.onInterceptTouchEvent(event);
}
break;
}
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(event);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mActivePointerId = MotionEvent.INVALID_POINTER_ID;
isBeingDragged = false;
break;
}
return isBeingDragged || super.onInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (MotionEventCompat.getActionMasked(event)) {
case MotionEvent.ACTION_DOWN:
mActivePointerId = event.getPointerId(0);
isBeingDragged = false;
break;
case MotionEvent.ACTION_MOVE: {
float y = getMotionEventY(event);
if (isScrollToTop() && !isScrollToBottom()) {
// 在顶部不在底部
mDistance = y - mInitialMotionY;
if (mDistance < 0) {
return super.onTouchEvent(event);
}
mScale = calculateRate(mDistance);
pull(mScale);
return true;
} else if (!isScrollToTop() && isScrollToBottom()) {
// 在底部不在顶部
mDistance = mInitialMotionY - y;
if (mDistance < 0) {
return super.onTouchEvent(event);
}
mScale = calculateRate(mDistance);
push(mScale);
return true;
} else if (isScrollToTop() && isScrollToBottom()) {
// 在底部也在顶部
mDistance = y - mInitialMotionY;
if (mDistance > 0) {
mScale = calculateRate(mDistance);
pull(mScale);
} else {
mScale = calculateRate(-mDistance);
push(mScale);
}
return true;
} else {
// 不在底部也不在顶部
return super.onTouchEvent(event);
}
}
case MotionEventCompat.ACTION_POINTER_DOWN:
mActivePointerId = event.getPointerId(MotionEventCompat.getActionIndex(event));
break;
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(event);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
if (isScrollToTop() && !isScrollToBottom()) {
animateRestore(true);
} else if (!isScrollToTop() && isScrollToBottom()) {
animateRestore(false);
} else if (isScrollToTop() && isScrollToBottom()) {
if (mDistance > 0) {
animateRestore(true);
} else {
animateRestore(false);
}
} else {
return super.onTouchEvent(event);
}
break;
}
}
return super.onTouchEvent(event);
}
private boolean isScrollToTop() {
return !ViewCompat.canScrollVertically(this, -1);
}
private boolean isScrollToBottom() {
return !ViewCompat.canScrollVertically(this, 1);
}
private float getMotionEventY(MotionEvent event) {
// 获取当前手指id的索引
int index = event.findPointerIndex(mActivePointerId);
// 得到触摸点相对于控件的Y坐标
return index < 0 ? -1f : event.getY(index);
}
private void onSecondaryPointerUp(MotionEvent event) {
final int pointerIndex = MotionEventCompat.getActionIndex(event);
final int pointerId = event.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mActivePointerId = event.getPointerId(newPointerIndex);
}
}
/**
* 根据滑动的距离计算缩放的比例
*/
private float calculateRate(float distance) {
float originalDragPercent = distance / (getResources().getDisplayMetrics().heightPixels);
float dragPercent = Math.min(1f, originalDragPercent);
// 二次函数 拖动比例越大 缩放比例越小
float rate = 2f * dragPercent - (float) Math.pow(dragPercent, 2f);
return 1 + rate / 5f;
}
/**
* 具体动画
* @param isPullRestore
*/
private void animateRestore(final boolean isPullRestore) {
ValueAnimator animator = ValueAnimator.ofFloat(mScale, 1f);
animator.setDuration(300);
// 设置减速插补器
animator.setInterpolator(new DecelerateInterpolator(2f));
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
if (isPullRestore) {
pull(value);
} else {
push(value);
}
}
});
animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
isRestoring = true;
}
@Override
public void onAnimationEnd(Animator animation) {
isRestoring = false;
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
animator.start();
}
private void pull(float scale) {
this.setPivotY(0);
this.setScaleY(scale);
}
private void push(float scale) {
this.setPivotY(this.getHeight());
this.setScaleY(scale);
}
}