|
|
@@ -0,0 +1,425 @@
|
|
|
+package com.yalantis.phoenix;
|
|
|
+
|
|
|
+import android.content.Context;
|
|
|
+import android.content.res.TypedArray;
|
|
|
+import android.support.annotation.NonNull;
|
|
|
+import android.support.v4.view.MotionEventCompat;
|
|
|
+import android.support.v4.view.ViewCompat;
|
|
|
+import android.util.AttributeSet;
|
|
|
+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.yalantis.phoenix.refresh_view.BaseRefreshView;
|
|
|
+import com.yalantis.phoenix.refresh_view.SunRefreshView;
|
|
|
+import com.yalantis.phoenix.util.Utils;
|
|
|
+
|
|
|
+import java.security.InvalidParameterException;
|
|
|
+
|
|
|
+public class PullToRefreshView extends ViewGroup {
|
|
|
+
|
|
|
+ private static final int DRAG_MAX_DISTANCE = 50;
|
|
|
+ private static final float DRAG_RATE = .5f;
|
|
|
+ private static final float DECELERATE_INTERPOLATION_FACTOR = 2f;
|
|
|
+
|
|
|
+ public static final int STYLE_SUN = 0;
|
|
|
+ public static final int MAX_OFFSET_ANIMATION_DURATION = 700;
|
|
|
+
|
|
|
+ private static final int INVALID_POINTER = -1;
|
|
|
+
|
|
|
+ private View mTarget;
|
|
|
+ private ImageView mRefreshView;
|
|
|
+ private Interpolator mDecelerateInterpolator;
|
|
|
+ private int mTouchSlop;
|
|
|
+ private int mTotalDragDistance;
|
|
|
+ private BaseRefreshView mBaseRefreshView;
|
|
|
+ private float mCurrentDragPercent;
|
|
|
+ private int mCurrentOffsetTop;
|
|
|
+ private boolean mRefreshing;
|
|
|
+ private int mActivePointerId;
|
|
|
+ private boolean mIsBeingDragged;
|
|
|
+ private float mInitialMotionY;
|
|
|
+ private int mFrom;
|
|
|
+ private float mFromDragPercent;
|
|
|
+ private boolean mNotify;
|
|
|
+ private OnRefreshListener mListener;
|
|
|
+
|
|
|
+ private int mTargetPaddingTop;
|
|
|
+ private int mTargetPaddingBottom;
|
|
|
+ private int mTargetPaddingRight;
|
|
|
+ private int mTargetPaddingLeft;
|
|
|
+
|
|
|
+ public PullToRefreshView(Context context) {
|
|
|
+ this(context, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ public PullToRefreshView(Context context, AttributeSet attrs) {
|
|
|
+ super(context, attrs);
|
|
|
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RefreshView);
|
|
|
+ final int type = a.getInteger(R.styleable.RefreshView_type, STYLE_SUN);
|
|
|
+ a.recycle();
|
|
|
+
|
|
|
+ mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);
|
|
|
+ mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
|
|
|
+ mTotalDragDistance = Utils.convertDpToPixel(context, DRAG_MAX_DISTANCE);
|
|
|
+
|
|
|
+ mRefreshView = new ImageView(context);
|
|
|
+
|
|
|
+ setRefreshStyle(type);
|
|
|
+
|
|
|
+ addView(mRefreshView);
|
|
|
+
|
|
|
+ setWillNotDraw(false);
|
|
|
+ ViewCompat.setChildrenDrawingOrderEnabled(this, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ public void setRefreshStyle(int type) {
|
|
|
+ setRefreshing(false);
|
|
|
+ switch (type) {
|
|
|
+ case STYLE_SUN:
|
|
|
+ mBaseRefreshView = new SunRefreshView(getContext(), this);
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ throw new InvalidParameterException("Type does not exist");
|
|
|
+ }
|
|
|
+ mRefreshView.setImageDrawable(mBaseRefreshView);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * This method sets padding for the refresh (progress) view.
|
|
|
+ */
|
|
|
+ public void setRefreshViewPadding(int left, int top, int right, int bottom) {
|
|
|
+ mRefreshView.setPadding(left, top, right, bottom);
|
|
|
+ }
|
|
|
+
|
|
|
+ public int getTotalDragDistance() {
|
|
|
+ return mTotalDragDistance;
|
|
|
+ }
|
|
|
+
|
|
|
+ @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);
|
|
|
+ }
|
|
|
+
|
|
|
+ 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;
|
|
|
+ mTargetPaddingBottom = mTarget.getPaddingBottom();
|
|
|
+ mTargetPaddingLeft = mTarget.getPaddingLeft();
|
|
|
+ mTargetPaddingRight = mTarget.getPaddingRight();
|
|
|
+ mTargetPaddingTop = mTarget.getPaddingTop();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @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 (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(@NonNull 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 = y - mInitialMotionY;
|
|
|
+ final float scrollTop = yDiff * DRAG_RATE;
|
|
|
+ mCurrentDragPercent = scrollTop / mTotalDragDistance;
|
|
|
+ if (mCurrentDragPercent < 0) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ float boundedDragPercent = Math.min(1f, Math.abs(mCurrentDragPercent));
|
|
|
+ float extraOS = Math.abs(scrollTop) - mTotalDragDistance;
|
|
|
+ float slingshotDist = mTotalDragDistance;
|
|
|
+ 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 * boundedDragPercent) + extraMove);
|
|
|
+
|
|
|
+ mBaseRefreshView.setPercent(mCurrentDragPercent, true);
|
|
|
+ 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;
|
|
|
+ mFromDragPercent = mCurrentDragPercent;
|
|
|
+ long animationDuration = Math.abs((long) (MAX_OFFSET_ANIMATION_DURATION * mFromDragPercent));
|
|
|
+
|
|
|
+ mAnimateToStartPosition.reset();
|
|
|
+ mAnimateToStartPosition.setDuration(animationDuration);
|
|
|
+ mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator);
|
|
|
+ mAnimateToStartPosition.setAnimationListener(mToStartListener);
|
|
|
+ mRefreshView.clearAnimation();
|
|
|
+ mRefreshView.startAnimation(mAnimateToStartPosition);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void animateOffsetToCorrectPosition() {
|
|
|
+ mFrom = mCurrentOffsetTop;
|
|
|
+ mFromDragPercent = mCurrentDragPercent;
|
|
|
+
|
|
|
+ mAnimateToCorrectPosition.reset();
|
|
|
+ mAnimateToCorrectPosition.setDuration(MAX_OFFSET_ANIMATION_DURATION);
|
|
|
+ mAnimateToCorrectPosition.setInterpolator(mDecelerateInterpolator);
|
|
|
+ mRefreshView.clearAnimation();
|
|
|
+ mRefreshView.startAnimation(mAnimateToCorrectPosition);
|
|
|
+
|
|
|
+ if (mRefreshing) {
|
|
|
+ mBaseRefreshView.start();
|
|
|
+ if (mNotify) {
|
|
|
+ if (mListener != null) {
|
|
|
+ mListener.onRefresh();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ mBaseRefreshView.stop();
|
|
|
+ animateOffsetToStartPosition();
|
|
|
+ }
|
|
|
+ mCurrentOffsetTop = mTarget.getTop();
|
|
|
+ mTarget.setPadding(mTargetPaddingLeft, mTargetPaddingTop, mTargetPaddingRight, mTotalDragDistance);
|
|
|
+ }
|
|
|
+
|
|
|
+ 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;
|
|
|
+ int endTarget = mTotalDragDistance;
|
|
|
+ targetTop = (mFrom + (int) ((endTarget - mFrom) * interpolatedTime));
|
|
|
+ int offset = targetTop - mTarget.getTop();
|
|
|
+
|
|
|
+ mCurrentDragPercent = mFromDragPercent - (mFromDragPercent - 1.0f) * interpolatedTime;
|
|
|
+ mBaseRefreshView.setPercent(mCurrentDragPercent, false);
|
|
|
+
|
|
|
+ setTargetOffsetTop(offset, false /* requires update */);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ private void moveToStart(float interpolatedTime) {
|
|
|
+ int targetTop = mFrom - (int) (mFrom * interpolatedTime);
|
|
|
+ float targetPercent = mFromDragPercent * (1.0f - interpolatedTime);
|
|
|
+ int offset = targetTop - mTarget.getTop();
|
|
|
+
|
|
|
+ mCurrentDragPercent = targetPercent;
|
|
|
+ mBaseRefreshView.setPercent(mCurrentDragPercent, true);
|
|
|
+ mTarget.setPadding(mTargetPaddingLeft, mTargetPaddingTop, mTargetPaddingRight, mTargetPaddingBottom + targetTop);
|
|
|
+ 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) {
|
|
|
+ mBaseRefreshView.setPercent(1f, true);
|
|
|
+ animateOffsetToCorrectPosition();
|
|
|
+ } else {
|
|
|
+ animateOffsetToStartPosition();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ 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) {
|
|
|
+ mBaseRefreshView.stop();
|
|
|
+ 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) {
|
|
|
+ mTarget.offsetTopAndBottom(offset);
|
|
|
+ mBaseRefreshView.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);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @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(left, top, left + width - right, top + height - bottom);
|
|
|
+ }
|
|
|
+
|
|
|
+ public void setOnRefreshListener(OnRefreshListener listener) {
|
|
|
+ mListener = listener;
|
|
|
+ }
|
|
|
+
|
|
|
+ public interface OnRefreshListener {
|
|
|
+ void onRefresh();
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|