package com.imooc.res.ui.view.refresh; import android.content.Context; import android.support.v4.view.MotionEventCompat; import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.MotionEvent; import android.view.View; import android.view.animation.Animation; import android.view.animation.Transformation; /** * time: 15/7/27 * description:上下都能刷新的refreshLayout * * @author fandong */ public class SwipeRefreshLayout extends SwipeRefresh { private CircleImageView mBottomCircleView; private MaterialProgressDrawable mBottomProgress; private int mCircleViewIndexForBottom = -1; private int mTargetIndex = -1; private int mCurrentTargetOffsetTopForBottom = -1; private int mOriginalOffsetTopForBottom = -1; private int mActivePointerId = INVALID_POINTER; private static final int INVALID_POINTER = -1; private boolean mIsBeingDragged; private float mInitialMotionY; private float mSpinnerFinalOffsetForBottom; private float mTotalDragDistanceForBottom = -1; private Animation mScaleAnimation; private Animation mScaleDownAnimation; private Animation mAlphaStartAnimation; private Animation mAlphaMaxAnimation; private boolean mNotify; private OnPullUpRefreshListener mListener; private int mFrom; private boolean mOriginalOffsetCalculatedForBottom; private Animation.AnimationListener mRefreshListener = new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationRepeat(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { if (mRefreshingForBottom) { // Make sure the progress view is fully visible mBottomProgress.setAlpha(MAX_ALPHA); mBottomProgress.start(); if (mNotify) { if (mListener != null) { mListener.onPullUpRefresh(); } } } else { mBottomProgress.stop(); mBottomCircleView.setVisibility(View.GONE); setColorViewAlpha(MAX_ALPHA); setTargetOffsetTopAndBottom(mOriginalOffsetTopForBottom - mCurrentTargetOffsetTopForBottom, true /* requires update */); } mCurrentTargetOffsetTopForBottom = mBottomCircleView.getTop(); } }; public SwipeRefreshLayout(Context context) { super(context); setColorSchemeColors(0xff2f2f2f); } public SwipeRefreshLayout(Context context, AttributeSet attrs) { super(context, attrs); createBottomProgressView(); final DisplayMetrics metrics = getResources().getDisplayMetrics(); // the absolute offset has to take into account that the circle starts at an offset mSpinnerFinalOffsetForBottom = DEFAULT_CIRCLE_TARGET * metrics.density; mTotalDragDistanceForBottom = mSpinnerFinalOffsetForBottom; //默认的颜色值 setColorSchemeColors(0xff2f2f2f); } @Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int childCount = getChildCount(); if (childCount != 3) { throw new RuntimeException("child count must be == 3"); } mBottomCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY)); mCircleViewIndexForBottom = -1; mTargetIndex = -1; mCircleViewIndex = -1; for (int index = 0; index < getChildCount(); index++) { if (getChildAt(index) == mBottomCircleView) { mCircleViewIndexForBottom = index; } else if (getChildAt(index) == mCircleView) { mCircleViewIndex = index; } else if (getChildAt(index) == mTarget) { mTargetIndex = index; } } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { if (!mOriginalOffsetCalculatedForBottom) { mOriginalOffsetCalculatedForBottom = true; mCurrentTargetOffsetTopForBottom = mOriginalOffsetTopForBottom = getMeasuredHeight(); } super.onLayout(changed, left, top, right, bottom); final int width = getMeasuredWidth(); final int height = getMeasuredHeight(); int circleWidth = mBottomCircleView.getMeasuredWidth(); int circleHeight = mBottomCircleView.getMeasuredHeight(); int bottomViewTopOffset = mCurrentTargetOffsetTopForBottom; mBottomCircleView.layout((width / 2 - circleWidth / 2), bottomViewTopOffset, (width / 2 + circleWidth / 2), bottomViewTopOffset + circleHeight); } @Override protected void ensureTarget() { // Don't bother getting the parent height if the parent hasn't been laid // out yet. if (mTarget == null) { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); if (!child.equals(mCircleView) && !child.equals(mBottomCircleView)) { mTarget = child; break; } } } } private void setColorViewAlpha(int targetAlpha) { mBottomCircleView.getBackground().setAlpha(targetAlpha); mBottomProgress.setAlpha(targetAlpha); } private void createBottomProgressView() { mBottomCircleView = new CircleImageView(getContext(), CIRCLE_BG_LIGHT, CIRCLE_DIAMETER / 2); mBottomProgress = new MaterialProgressDrawable(getContext(), this); mBottomProgress.setBackgroundColor(CIRCLE_BG_LIGHT); mBottomCircleView.setImageDrawable(mBottomProgress); mBottomCircleView.setVisibility(View.GONE); addView(mBottomCircleView); } public void setColorSchemeColors(int... colors) { ensureTarget(); mProgress.setColorSchemeColors(colors); mBottomProgress.setColorSchemeColors(colors); } @Override protected int getChildDrawingOrder(int childCount, int i) { //目前写死的 if (i == 0) { return mTargetIndex; } else if (i == 1) { return mCircleViewIndex; } else { return mCircleViewIndexForBottom; } } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { mSuperInterceptEvent = super.onInterceptTouchEvent(ev); if (mSuperInterceptEvent) { return true; } else { mChildInterceptEvent = mineInterceptTouchEvent(ev); return mChildInterceptEvent; } } @Override public boolean onTouchEvent(MotionEvent ev) { boolean superHandler = super.onTouchEvent(ev); if (superHandler || mSuperInterceptEvent) { return true; } else { return mineTouchEvent(ev); } } private boolean mineInterceptTouchEvent(MotionEvent ev) { if (!canExecuteUpRefresh()) { return false; } ensureTarget(); final int action = MotionEventCompat.getActionMasked(ev); if (!isEnabled() || canChildScrollDown() || mRefreshingForBottom || mRefreshing) { // Fail fast if we're not in a state where a swipe is possible return false; } switch (action) { case MotionEvent.ACTION_DOWN: setTargetOffsetTopAndBottomForBottom(mOriginalOffsetTopForBottom - mBottomCircleView.getTop(), true); mActivePointerId = MotionEventCompat.getPointerId(ev, 0); mIsBeingDragged = false; final float initialMotionY = getMotionEventY(ev, mActivePointerId); if (initialMotionY == -1) { return false; } mInitialMotionY = initialMotionY; 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; //上拉<0 if (Math.abs(yDiff) > (mTouchSlop * 2.0f) && yDiff < 0 && !mIsBeingDragged) { mIsBeingDragged = true; mBottomProgress.setAlpha(STARTING_PROGRESS_ALPHA); } break; case MotionEventCompat.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mIsBeingDragged = false; mActivePointerId = INVALID_POINTER; break; } return mIsBeingDragged; } //只适配4.0以上的机器 public boolean canChildScrollDown() { return ViewCompat.canScrollVertically(mTarget, 1); } private boolean mineTouchEvent(MotionEvent ev) { if (!canExecuteUpRefresh()) { return false; } final int action = MotionEventCompat.getActionMasked(ev); if (!isEnabled() || canChildScrollDown()) { // Fail fast if we're not in a state where a swipe is possible return false; } switch (action) { case MotionEvent.ACTION_DOWN: mActivePointerId = MotionEventCompat.getPointerId(ev, 0); mIsBeingDragged = false; break; case MotionEvent.ACTION_MOVE: { final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); if (pointerIndex < 0) { return false; } final float y = MotionEventCompat.getY(ev, pointerIndex); float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; if (mIsBeingDragged) { mBottomProgress.showArrow(true); float originalDragPercent = overscrollTop / mTotalDragDistanceForBottom; if (originalDragPercent > 0) { return false; } overscrollTop = Math.abs(overscrollTop); float dragPercent = Math.min(1f, Math.abs(originalDragPercent)); float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3; float extraOS = Math.abs(overscrollTop) - mTotalDragDistanceForBottom; float slingshotDist = mSpinnerFinalOffsetForBottom; 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 = mOriginalOffsetTopForBottom - (int) ((slingshotDist * dragPercent) + extraMove); // where 1.0f is a full circle if (mBottomCircleView.getVisibility() != View.VISIBLE) { mBottomCircleView.setVisibility(View.VISIBLE); } ViewCompat.setScaleX(mBottomCircleView, 1f); ViewCompat.setScaleY(mBottomCircleView, 1f); if (overscrollTop < mTotalDragDistanceForBottom) { if (mBottomProgress.getAlpha() > STARTING_PROGRESS_ALPHA && !isAnimationRunning(mAlphaStartAnimation)) { // Animate the alpha startProgressAlphaStartAnimation(); } float strokeStart = (float) (adjustedPercent * .8f); mBottomProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart)); mBottomProgress.setArrowScale(Math.min(1f, adjustedPercent)); } else { if (mBottomProgress.getAlpha() < MAX_ALPHA && !isAnimationRunning(mAlphaMaxAnimation)) { // Animate the alpha startProgressAlphaMaxAnimation(); } } float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f; mBottomProgress.setProgressRotation(rotation); setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTopForBottom, true /* requires update */); } 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) { if (action == MotionEvent.ACTION_UP) { } 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 (Math.abs(overscrollTop) > mTotalDragDistanceForBottom) { setRefreshing(true, true /* notify */); } else { // cancel refresh mRefreshingForBottom = false; mBottomProgress.setStartEndTrim(0f, 0f); Animation.AnimationListener listener = null; listener = new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { startScaleDownAnimation(null); } @Override public void onAnimationRepeat(Animation animation) { } }; animateOffsetToStartPosition(mCurrentTargetOffsetTopForBottom, listener); mBottomProgress.showArrow(false); } mActivePointerId = INVALID_POINTER; return false; } } return true; } private void setRefreshing(boolean refreshing, final boolean notify) { if (mRefreshingForBottom != refreshing) { mNotify = notify; ensureTarget(); mRefreshingForBottom = refreshing; if (mRefreshingForBottom) { animateOffsetToCorrectPosition(mCurrentTargetOffsetTopForBottom, mRefreshListener); } else { startScaleDownAnimation(mRefreshListener); } } } private void animateOffsetToCorrectPosition(int from, Animation.AnimationListener listener) { mFrom = from; mAnimateToCorrectPosition.reset(); mAnimateToCorrectPosition.setDuration(ANIMATE_TO_TRIGGER_DURATION); mAnimateToCorrectPosition.setInterpolator(mDecelerateInterpolator); if (listener != null) { mBottomCircleView.setAnimationListener(listener); } mBottomCircleView.clearAnimation(); mBottomCircleView.startAnimation(mAnimateToCorrectPosition); } private final Animation mAnimateToCorrectPosition = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { int targetTop = 0; int endTarget = 0; if (!mUsingCustomStart) { endTarget = (int) (-mSpinnerFinalOffsetForBottom + Math.abs(mOriginalOffsetTopForBottom)); } else { endTarget = (int) mSpinnerFinalOffsetForBottom; } targetTop = (mFrom + (int) ((endTarget - mFrom) * interpolatedTime)); int offset = targetTop - mBottomCircleView.getTop(); setTargetOffsetTopAndBottom(offset, false /* requires update */); } }; private void animateOffsetToStartPosition(int from, Animation.AnimationListener listener) { mFrom = from; mAnimateToStartPosition.reset(); mAnimateToStartPosition.setDuration(ANIMATE_TO_START_DURATION); mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator); if (listener != null) { mBottomCircleView.setAnimationListener(listener); } mBottomCircleView.clearAnimation(); mBottomCircleView.startAnimation(mAnimateToStartPosition); } private final Animation mAnimateToStartPosition = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { moveToStart(interpolatedTime); } }; private void moveToStart(float interpolatedTime) { int targetTop = 0; targetTop = (mFrom + (int) ((mOriginalOffsetTopForBottom - mFrom) * interpolatedTime)); int offset = targetTop - mBottomCircleView.getTop(); setTargetOffsetTopAndBottom(offset, false /* requires update */); } private void startScaleDownAnimation(Animation.AnimationListener listener) { mScaleDownAnimation = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { setAnimationProgress(1 - interpolatedTime); } }; mScaleDownAnimation.setDuration(SCALE_DOWN_DURATION); mBottomCircleView.setAnimationListener(listener); mBottomCircleView.clearAnimation(); mBottomCircleView.startAnimation(mScaleDownAnimation); } private void setAnimationProgress(float progress) { ViewCompat.setScaleX(mBottomCircleView, progress); ViewCompat.setScaleY(mBottomCircleView, progress); } private void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate) { mBottomCircleView.bringToFront(); mBottomCircleView.offsetTopAndBottom(offset); mCurrentTargetOffsetTopForBottom = mBottomCircleView.getTop(); if (requiresUpdate && android.os.Build.VERSION.SDK_INT < 11) { invalidate(); } } private void startProgressAlphaMaxAnimation() { mAlphaMaxAnimation = startAlphaAnimation(mBottomProgress.getAlpha(), MAX_ALPHA); } private void startProgressAlphaStartAnimation() { mAlphaStartAnimation = startAlphaAnimation(mBottomProgress.getAlpha(), STARTING_PROGRESS_ALPHA); } private Animation startAlphaAnimation(final int startingAlpha, final int endingAlpha) { Animation alpha = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { mBottomProgress .setAlpha((int) (startingAlpha + ((endingAlpha - startingAlpha) * interpolatedTime))); } }; alpha.setDuration(ALPHA_ANIMATION_DURATION); // Clear out the previous animation listeners. mBottomCircleView.setAnimationListener(null); mBottomCircleView.clearAnimation(); mBottomCircleView.startAnimation(alpha); return alpha; } private boolean isAnimationRunning(Animation animation) { return animation != null && animation.hasStarted() && !animation.hasEnded(); } private void setTargetOffsetTopAndBottomForBottom(int offset, boolean requiresUpdate) { mBottomCircleView.bringToFront(); mBottomCircleView.offsetTopAndBottom(offset); mCurrentTargetOffsetTopForBottom = mBottomCircleView.getTop(); if (requiresUpdate && android.os.Build.VERSION.SDK_INT < 11) { invalidate(); } } private void onSecondaryPointerUp(MotionEvent ev) { final int pointerIndex = MotionEventCompat.getActionIndex(ev); final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); if (pointerId == mActivePointerId) { // This was our active pointer going up. Choose a new // active pointer and adjust accordingly. final int newPointerIndex = pointerIndex == 0 ? 1 : 0; mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); } } public void setPullUpRefreshing(boolean refreshing) { if (refreshing && mRefreshingForBottom != refreshing) { // scale and show mRefreshingForBottom = refreshing; int endTarget = 0; if (!mUsingCustomStart) { endTarget = (int) (mSpinnerFinalOffsetForBottom + mOriginalOffsetTopForBottom); } else { endTarget = (int) mSpinnerFinalOffsetForBottom; } setTargetOffsetTopAndBottom(endTarget - mCurrentTargetOffsetTopForBottom, true /* requires update */); mNotify = false; startScaleUpAnimation(mRefreshListener); } else { setRefreshing(refreshing, false /* notify */); } } private void startScaleUpAnimation(Animation.AnimationListener listener) { mBottomCircleView.setVisibility(View.VISIBLE); if (android.os.Build.VERSION.SDK_INT >= 11) { // Pre API 11, alpha is used in place of scale up to show the // progress circle appearing. // Don't adjust the alpha during appearance otherwise. mBottomProgress.setAlpha(MAX_ALPHA); } mScaleAnimation = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { setAnimationProgress(interpolatedTime); } }; mScaleAnimation.setDuration(mMediumAnimationDuration); if (listener != null) { mBottomCircleView.setAnimationListener(listener); } mBottomCircleView.clearAnimation(); mBottomCircleView.startAnimation(mScaleAnimation); } /** * Set the listener to be notified when a refresh is triggered via the swipe * gesture. */ public void setOnPullUpRefreshListener(OnPullUpRefreshListener listener) { mListener = listener; } public interface OnPullUpRefreshListener { void onPullUpRefresh(); } }