package com.cmeiyuan.widget;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Rect;
import android.os.Build;
import android.os.Handler;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
import android.widget.ScrollView;
import com.nineoldandroids.animation.ValueAnimator;
import com.nineoldandroids.animation.ValueAnimator.AnimatorUpdateListener;
public class ElasticScrollView extends ScrollView {
protected static final String TAG = "ElasticScrollView";
/**
* 最多拉出高度
*/
private static final float OVERSCRLL_ACTOR = 0.8f;
/**
* 弹性系数
*/
private static final float ELASTICF_ACTOR = 0.5f;
/**
* An invalid pointer id
*/
private static final int INVALID_POINTER_ID = -1;
/**
* 有效移动距离,防止点击事件被拦截
*/
private static final int VALID_MOVE_DISTANCE = 20;
/**
* 最新一次的手指触摸位置X
*/
private float mLastMotionX;
/**
* 最新一次的手指触摸位置Y
*/
private float mLastMotionY;
/**
* 按在屏幕上的手指的id
*/
private int mActivePointerId = INVALID_POINTER_ID;
/**
* ScrollView的唯一child
*/
private View mChildView;
/**
* 惯性距离
*/
private int mInertanceY;
/**
* 正常layout布局
*/
private Rect mNormalRect = new Rect();
private ValueAnimator mResumeAnimator = new ValueAnimator();
private ValueAnimator mScrollAnimator = new ValueAnimator();
private long mDuration = 200;
private Handler mHandler = new Handler();
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
scrollToNormal();
}
};
private AnimatorUpdateListener mAnimatorUpdateListener = new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
try {
int top = (Integer) valueAnimator.getAnimatedValue();
int width = mChildView.getMeasuredWidth();
int height = mChildView.getMeasuredHeight();
if (mChildView != null) {
mChildView.layout(0, top, width, top + height);
}
} catch (Exception e) {
Log.e(TAG, "onAnimationUpdate error:" + e.toString());
}
}
};
public ElasticScrollView(Context context) {
this(context, null);
}
public ElasticScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
public ElasticScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
setOverScrollMode(View.OVER_SCROLL_NEVER);
} else {
ViewCompat.setOverScrollMode(this, ViewCompat.OVER_SCROLL_NEVER);
}
mResumeAnimator.setDuration(mDuration);
mResumeAnimator.setInterpolator(new DecelerateInterpolator());
mResumeAnimator.addUpdateListener(mAnimatorUpdateListener);
mScrollAnimator.setDuration(mDuration);
mScrollAnimator.setInterpolator(new DecelerateInterpolator());
mScrollAnimator.addUpdateListener(mAnimatorUpdateListener);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (getChildCount() > 0) {
mChildView = getChildAt(0);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (mChildView != null) {
int left = mChildView.getLeft();
int top = mChildView.getTop();
int right = mChildView.getRight();
int bottom = mChildView.getBottom();
mNormalRect.set(left, top, right, bottom);
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastMotionX = ev.getX();
mLastMotionY = ev.getY();
mActivePointerId = ev.getPointerId(0);
break;
case MotionEvent.ACTION_MOVE:
float curX = ev.getX();
float curY = ev.getY();
float distanceX = Math.abs(curX - mLastMotionX);
float distanceY = Math.abs(curY - mLastMotionY);
// 如果是横向滑动,不要拦截
if (distanceX > distanceY) {
return false;
}
// 坚向滑动距离超过有效移动距离
if (distanceY > VALID_MOVE_DISTANCE) {
return true;
}
break;
case MotionEvent.ACTION_CANCEL:
break;
}
return super.onInterceptTouchEvent(ev);
}
@SuppressLint("ClickableViewAccessibility")
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction() & MotionEvent.ACTION_MASK) {
// 第一个手指按下
case MotionEvent.ACTION_DOWN:
mActivePointerId = ev.getPointerId(0);
mLastMotionY = (int) ev.getY();
break;
// 第二个手指按下
case MotionEvent.ACTION_POINTER_DOWN:
final int index = ev.getActionIndex();
mLastMotionY = ev.getY(index);
mActivePointerId = ev.getPointerId(index);
break;
// 第二个手指放开
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
if (mActivePointerId != INVALID_POINTER_ID) {
mLastMotionY = ev.getY(ev.findPointerIndex(mActivePointerId));
}
break;
// 手指移动时
case MotionEvent.ACTION_MOVE:
int activePointerIndex = ev.findPointerIndex(mActivePointerId);
if (activePointerIndex == -1) {
Log.e(TAG, "Invalid activePointerIndex = " + activePointerIndex
+ " in onTouchEvent");
break;
}
float curY = ev.getY(activePointerIndex);
float deltaY = curY - mLastMotionY;
mLastMotionY = curY;
if (deltaY > 0 && isScrollToTop()) {
overScrollBy((int) deltaY);
return true;
}
if (deltaY < 0 && isScrollToBottom()) {
overScrollBy((int) deltaY);
return true;
}
break;
case MotionEvent.ACTION_CANCEL:
// 移动到控件边界
case MotionEvent.ACTION_OUTSIDE:
// 最后一个手指放开
case MotionEvent.ACTION_UP:
// 滑动到正常状态
scrollToNormal();
mActivePointerId = INVALID_POINTER_ID;
break;
default:
break;
}
return super.onTouchEvent(ev);
}
/**
* 功能描述: 防止出现pointerIndex out of range异常<br>
*
* @param ev
*/
private void onSecondaryPointerUp(MotionEvent ev) {
final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
// TODO: Make this decision more intelligent.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastMotionY = (int) ev.getY(newPointerIndex);
mActivePointerId = ev.getPointerId(newPointerIndex);
}
}
private void overScrollTo(int targetY) {
if (mChildView != null) {
mResumeAnimator.setIntValues(0, -targetY);
mResumeAnimator.start();
}
}
private void overScrollBy(int deltaY) {
if (mChildView != null) {
int left = mChildView.getLeft();
int right = mChildView.getRight();
int top = mChildView.getTop();
int bottom = mChildView.getBottom();
float offset = Math.abs(top);
float total = getMeasuredHeight();
float percent = offset / total;
if (percent >= OVERSCRLL_ACTOR) {
return;
}
int scrollY = (int) (deltaY * (1 - percent) * ELASTICF_ACTOR);
top += scrollY;
bottom += scrollY;
mChildView.layout(left, top, right, bottom);
Log.d(TAG, "layout:" + new Rect(left, top, right, bottom));
}
}
private void scrollToNormal() {
if (mChildView != null) {
int top = mChildView.getTop();
mResumeAnimator.setIntValues(top, 0);
mResumeAnimator.start();
}
}
@Override
public void fling(int velocityY) {
super.fling(velocityY);
mInertanceY = 50 * velocityY / 2500;
}
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
@Override
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX,
boolean clampedY) {
super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
if (clampedY) {
overScrollTo(mInertanceY);
}
// mHandler.postDelayed(mRunnable, mDuration);
}
private boolean isScrollToTop() {
return getScrollY() == 0;
}
private boolean isScrollToBottom() {
if (mChildView == null) {
return true;
}
int normalY = mChildView.getMeasuredHeight() - getMeasuredHeight();
return getScrollY() == normalY;
}
}