package com.marshalchen.common.uimodule.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.animation.Transformation;
import android.widget.AbsListView;
import android.widget.ImageView;
import com.marshalchen.ultimateandroiduicomponent.R;
import java.security.InvalidParameterException;
/**
* Created by baoyz on 14/10/30.
*/
public class PullRefreshLayout extends ViewGroup {
private static final float DECELERATE_INTERPOLATION_FACTOR = 2f;
private static final int DRAG_MAX_DISTANCE = 64;
private static final int INVALID_POINTER = -1;
private static final float DRAG_RATE = .5f;
public static final int STYLE_CIRCLES = 0;
public static final int STYLE_WATER_DROP = 1;
public static final int STYLE_RING = 2;
public static final int MODE_TOP = 0;
public static final int MODE_BOTTOM = 1;
private View mTarget;
private ImageView mRefreshView;
private Interpolator mDecelerateInterpolator;
private int mTouchSlop;
private int mMediumAnimationDuration;
private int mSpinnerFinalOffset;
private int mTotalDragDistance;
private RefreshDrawable mRefreshDrawable;
private int mCurrentOffsetTop;
private boolean mRefreshing;
private int mActivePointerId;
private boolean mIsBeingDragged;
private float mInitialMotionY;
private int mFrom;
private boolean mNotify;
private OnRefreshListener mListener;
private int[] mColorSchemeColors;
private int mMode;
public PullRefreshLayout(Context context) {
this(context, null);
}
public PullRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomPullRefreshLayout);
final int type = a.getInteger(R.styleable.CustomPullRefreshLayout_cprlType, STYLE_CIRCLES);
final int colorsId = a.getResourceId(R.styleable.CustomPullRefreshLayout_cprlColors, R.array.pull_colors);
a.recycle();
mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mMediumAnimationDuration = getResources().getInteger(
android.R.integer.config_mediumAnimTime);
mSpinnerFinalOffset = mTotalDragDistance = dp2px(DRAG_MAX_DISTANCE);
mRefreshView = new ImageView(context);
mColorSchemeColors = context.getResources().getIntArray(colorsId);
setRefreshStyle(type);
// mRefreshDrawable.setColorSchemeColors(new int[]{Color.rgb(0xC9, 0x34, 0x37), Color.rgb(0x37, 0x5B, 0xF1), Color.rgb(0xF7, 0xD2, 0x3E), Color.rgb(0x34, 0xA3, 0x50)});
mRefreshView.setVisibility(View.GONE);
addView(mRefreshView);
setWillNotDraw(false);
ViewCompat.setChildrenDrawingOrderEnabled(this, true);
}
public void setColorSchemeColors(int[] colorSchemeColors){
mColorSchemeColors = colorSchemeColors;
mRefreshDrawable.setColorSchemeColors(colorSchemeColors);
}
public void setRefreshStyle(int type){
setRefreshing(false);
switch (type){
case STYLE_CIRCLES:
mRefreshDrawable = new CirclesDrawable(getContext(), this);
break;
case STYLE_WATER_DROP:
mRefreshDrawable = new WaterDropDrawable(getContext(), this);
break;
case STYLE_RING:
mRefreshDrawable = new RingDrawable(getContext(), this);
break;
default:
throw new InvalidParameterException("Type does not exist");
}
mRefreshDrawable.setColorSchemeColors(mColorSchemeColors);
mRefreshView.setImageDrawable(mRefreshDrawable);
}
public int getFinalOffset(){
return mSpinnerFinalOffset;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
ensureTarget();
if (mTarget == null)
return;
widthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() - getPaddingRight() - getPaddingLeft(), MeasureSpec.EXACTLY);
heightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY);
mTarget.measure(widthMeasureSpec, heightMeasureSpec);
mRefreshView.measure(widthMeasureSpec, heightMeasureSpec);
// mRefreshView.measure(MeasureSpec.makeMeasureSpec(mRefreshViewWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mRefreshViewHeight, MeasureSpec.EXACTLY));
}
private void ensureTarget() {
if (mTarget != null)
return;
if (getChildCount() > 0) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child != mRefreshView)
mTarget = child;
}
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (!isEnabled() || canChildScrollUp() || mRefreshing) {
return false;
}
final int action = MotionEventCompat.getActionMasked(ev);
switch (action) {
case MotionEvent.ACTION_DOWN:
setTargetOffsetTop(0, true);
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
mIsBeingDragged = false;
final float initialMotionY = getMotionEventY(ev, mActivePointerId);
if (initialMotionY == -1) {
return false;
}
mInitialMotionY = initialMotionY;
break;
case MotionEvent.ACTION_MOVE:
if (mActivePointerId == INVALID_POINTER) {
return false;
}
final float y = getMotionEventY(ev, mActivePointerId);
if (y == -1) {
return false;
}
final float yDiff = y - mInitialMotionY;
// if (Math.abs(yDiff) > mTouchSlop && !mIsBeingDragged) {
// mIsBeingDragged = true;
// mMode = yDiff > 0 ? MODE_TOP : MODE_BOTTOM;
// }
if (yDiff > mTouchSlop && !mIsBeingDragged) {
mIsBeingDragged = true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
break;
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
}
return mIsBeingDragged;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (!mIsBeingDragged) {
return super.onTouchEvent(ev);
}
final int action = MotionEventCompat.getActionMasked(ev);
switch (action) {
case MotionEvent.ACTION_MOVE: {
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
if (pointerIndex < 0) {
return false;
}
final float y = MotionEventCompat.getY(ev, pointerIndex);
// final float yDiff = Math.abs(y - mInitialMotionY);
final float yDiff = y - mInitialMotionY;
final float scrollTop = yDiff * DRAG_RATE;
float originalDragPercent = scrollTop / mTotalDragDistance;
if (originalDragPercent < 0) {
return false;
}
float dragPercent = Math.min(1f, Math.abs(originalDragPercent));
// float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3;
// float adjustedPercent = dragPercent;
float extraOS = Math.abs(scrollTop) - mTotalDragDistance;
float slingshotDist = mSpinnerFinalOffset;
float tensionSlingshotPercent = Math.max(0,
Math.min(extraOS, slingshotDist * 2) / slingshotDist);
float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow(
(tensionSlingshotPercent / 4), 2)) * 2f;
float extraMove = (slingshotDist) * tensionPercent * 2;
int targetY = (int) ((slingshotDist * dragPercent) + extraMove);
if (mRefreshView.getVisibility() != View.VISIBLE) {
mRefreshView.setVisibility(View.VISIBLE);
}
if (scrollTop < mTotalDragDistance) {
mRefreshDrawable.setPercent(dragPercent);
}
setTargetOffsetTop(targetY - mCurrentOffsetTop, true);
break;
}
case MotionEventCompat.ACTION_POINTER_DOWN:
final int index = MotionEventCompat.getActionIndex(ev);
mActivePointerId = MotionEventCompat.getPointerId(ev, index);
break;
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
if (mActivePointerId == INVALID_POINTER) {
return false;
}
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
final float y = MotionEventCompat.getY(ev, pointerIndex);
final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
mIsBeingDragged = false;
if (overscrollTop > mTotalDragDistance) {
setRefreshing(true, true);
} else {
mRefreshing = false;
animateOffsetToStartPosition();
}
mActivePointerId = INVALID_POINTER;
return false;
}
}
return true;
}
private void animateOffsetToStartPosition() {
mFrom = mCurrentOffsetTop;
mAnimateToStartPosition.reset();
mAnimateToStartPosition.setDuration(mMediumAnimationDuration);
mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator);
mAnimateToStartPosition.setAnimationListener(mToStartListener);
mRefreshView.clearAnimation();
mRefreshView.startAnimation(mAnimateToStartPosition);
}
private void animateOffsetToCorrectPosition() {
mFrom = mCurrentOffsetTop;
mAnimateToCorrectPosition.reset();
mAnimateToCorrectPosition.setDuration(mMediumAnimationDuration);
mAnimateToCorrectPosition.setInterpolator(mDecelerateInterpolator);
mAnimateToCorrectPosition.setAnimationListener(mRefreshListener);
mRefreshView.clearAnimation();
mRefreshView.startAnimation(mAnimateToCorrectPosition);
}
private final Animation mAnimateToStartPosition = new Animation() {
@Override
public void applyTransformation(float interpolatedTime, Transformation t) {
moveToStart(interpolatedTime);
}
};
private final Animation mAnimateToCorrectPosition = new Animation() {
@Override
public void applyTransformation(float interpolatedTime, Transformation t) {
int targetTop = 0;
int endTarget = (int) mSpinnerFinalOffset;
targetTop = (mFrom + (int) ((endTarget - mFrom) * interpolatedTime));
int offset = targetTop - mTarget.getTop();
setTargetOffsetTop(offset, false /* requires update */);
}
};
private void moveToStart(float interpolatedTime) {
int targetTop = mFrom - (int) (mFrom * interpolatedTime);
int offset = targetTop - mTarget.getTop();
setTargetOffsetTop(offset, false);
}
public void setRefreshing(boolean refreshing) {
if (mRefreshing != refreshing) {
setRefreshing(refreshing, false /* notify */);
}
}
private void setRefreshing(boolean refreshing, final boolean notify) {
if (mRefreshing != refreshing) {
mNotify = notify;
ensureTarget();
mRefreshing = refreshing;
if (mRefreshing) {
animateOffsetToCorrectPosition();
} else {
animateOffsetToStartPosition();
}
}
}
private Animation.AnimationListener mRefreshListener = new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
mRefreshView.setVisibility(View.VISIBLE);
}
@Override
public void onAnimationRepeat(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
if (mRefreshing) {
mRefreshDrawable.start();
if (mNotify) {
if (mListener != null) {
mListener.onRefresh();
}
}
} else {
mRefreshDrawable.stop();
mRefreshView.setVisibility(View.GONE);
animateOffsetToStartPosition();
}
mCurrentOffsetTop = mTarget.getTop();
}
};
private Animation.AnimationListener mToStartListener = new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationRepeat(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
mRefreshDrawable.stop();
mRefreshView.setVisibility(View.GONE);
mCurrentOffsetTop = mTarget.getTop();
}
};
private void onSecondaryPointerUp(MotionEvent ev) {
final int pointerIndex = MotionEventCompat.getActionIndex(ev);
final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
if (pointerId == mActivePointerId) {
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
}
}
private float getMotionEventY(MotionEvent ev, int activePointerId) {
final int index = MotionEventCompat.findPointerIndex(ev, activePointerId);
if (index < 0) {
return -1;
}
return MotionEventCompat.getY(ev, index);
}
private void setTargetOffsetTop(int offset, boolean requiresUpdate) {
mRefreshView.bringToFront();
// mRefreshView.offsetTopAndBottom(offset);
mTarget.offsetTopAndBottom(offset);
mRefreshDrawable.offsetTopAndBottom(offset);
mCurrentOffsetTop = mTarget.getTop();
if (requiresUpdate && android.os.Build.VERSION.SDK_INT < 11) {
invalidate();
}
}
private boolean canChildScrollUp() {
if (android.os.Build.VERSION.SDK_INT < 14) {
if (mTarget instanceof AbsListView) {
final AbsListView absListView = (AbsListView) mTarget;
return absListView.getChildCount() > 0
&& (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
.getTop() < absListView.getPaddingTop());
} else {
return mTarget.getScrollY() > 0;
}
} else {
return ViewCompat.canScrollVertically(mTarget, -1);
}
}
private boolean canChildScrollDown() {
if (android.os.Build.VERSION.SDK_INT < 14) {
if (mTarget instanceof AbsListView) {
final AbsListView absListView = (AbsListView) mTarget;
return absListView.getChildCount() > 0
&& (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
.getTop() < absListView.getPaddingTop());
} else {
return mTarget.getScrollY() > 0;
}
} else {
return ViewCompat.canScrollVertically(mTarget, -1);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
ensureTarget();
if (mTarget == null)
return;
int height = getMeasuredHeight();
int width = getMeasuredWidth();
int left = getPaddingLeft();
int top = getPaddingTop();
int right = getPaddingRight();
int bottom = getPaddingBottom();
mTarget.layout(left, top + mCurrentOffsetTop, left + width - right, top + height - bottom + mCurrentOffsetTop);
// mRefreshView.layout(width / 2 - mRefreshViewWidth / 2, -mRefreshViewHeight + mCurrentOffsetTop, width / 2 + mRefreshViewHeight / 2, mCurrentOffsetTop);
mRefreshView.layout(left, top, left + width - right, top + height - bottom);
}
private int dp2px(int dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getContext().getResources().getDisplayMetrics());
}
public void setOnRefreshListener(OnRefreshListener listener) {
mListener = listener;
}
public static interface OnRefreshListener {
public void onRefresh();
}
}