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.desmond.libs.R; 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 = 120; 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(); } }