package com.wangdaye.mysplash.common.ui.widget.swipeRefreshView; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.support.annotation.ColorInt; import android.support.annotation.ColorRes; import android.support.v4.content.ContextCompat; import android.support.v4.view.MotionEventCompat; import android.support.v4.view.NestedScrollingChild; import android.support.v4.view.NestedScrollingChildHelper; import android.support.v4.view.NestedScrollingParent; import android.support.v4.view.NestedScrollingParentHelper; 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.ViewConfiguration; import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.DecelerateInterpolator; import android.view.animation.Transformation; import android.widget.AbsListView; /** * Both way swipe refresh layout. * * This is a more powerful {@link android.support.v4.widget.SwipeRefreshLayout}, it can swipe * to refresh and load. * * */ public class BothWaySwipeRefreshLayout extends ViewGroup implements NestedScrollingParent, NestedScrollingChild { // direction public static final int DIRECTION_TOP = 0; public static final int DIRECTION_BOTTOM = 1; private static final int MAX_ALPHA = 255; private static final int STARTING_PROGRESS_ALPHA = (int) (.3f * MAX_ALPHA); private static final int CIRCLE_DIAMETER = 40; private static final float DECELERATE_INTERPOLATION_FACTOR = 2f; private static final float DRAG_RATE = .5f; // Max amount of circle that can be filled by progress during swipe gesture, // where 1.0 is a full circle private static final float MAX_PROGRESS_ANGLE = .8f; private static final int SCALE_DOWN_DURATION = 150; private static final int ALPHA_ANIMATION_DURATION = 300; private static final int ANIMATE_TO_TRIGGER_DURATION = 200; private static final int ANIMATE_TO_START_DURATION = 200; // Default background for the progress spinner private static final int CIRCLE_BG_LIGHT = 0xFFFAFAFA; // Default offset in dips from the top of the view to where the progress spinner should stop private static final int DEFAULT_CIRCLE_TARGET = 64; private View mTarget; // the target of the gesture private OnRefreshAndLoadListener mListener; private boolean mRefreshing = false; private boolean mLoading = false; private boolean mPermitRefresh = true; private boolean mPermitLoad = true; private int mTouchSlop; private float[] mDragTriggerDistances = new float[] {-1, -1}; // If nested scrolling is enabled, the total amount that needed to be // consumed by this as the nested scrolling parent is used in place of the // overscroll determined by MOVE events in the onTouch handler private float mTotalUnconsumed; private final NestedScrollingParentHelper mNestedScrollingParentHelper; private final NestedScrollingChildHelper mNestedScrollingChildHelper; private final int[] mParentScrollConsumed = new int[2]; private final int[] mParentOffsetInWindow = new int[2]; private boolean mNestedScrollInProgress; private int mMediumAnimationDuration; private int mDragOffsetDistance; // Whether or not the starting offset has been determined. private boolean mOriginalOffsetCalculated = false; private float mInitialDownY; private boolean mIsBeingDragged; // Whether this item is scaled up rather than clipped. private boolean mScale = false; // Target is returning to its start offset because it was cancelled or a // refresh was triggered. private boolean mReturningToStart; private final DecelerateInterpolator mDecelerateInterpolator; private static final int[] LAYOUT_ATTRS = new int[] { android.R.attr.enabled }; private CircleImageView[] mCircleViews; protected int mFrom; private float mStartingScale; private MaterialProgressDrawable[] mProgress; private Animation mScaleAnimation; private Animation mScaleDownAnimation; private Animation mAlphaStartAnimation; private Animation mAlphaMaxAnimation; private Animation mScaleDownToStartAnimation; private boolean mNotify; private int mCircleWidth; private int mCircleHeight; public BothWaySwipeRefreshLayout(Context context) { this(context, null); } public BothWaySwipeRefreshLayout(Context context, AttributeSet attrs) { super(context, attrs); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); mMediumAnimationDuration = getResources().getInteger( android.R.integer.config_mediumAnimTime); setWillNotDraw(false); mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR); final DisplayMetrics metrics = getResources().getDisplayMetrics(); mCircleWidth = (int) (CIRCLE_DIAMETER * metrics.density); mCircleHeight = (int) (CIRCLE_DIAMETER * metrics.density); mDragTriggerDistances[DIRECTION_TOP] = DEFAULT_CIRCLE_TARGET * metrics.density; mDragTriggerDistances[DIRECTION_BOTTOM] = DEFAULT_CIRCLE_TARGET * metrics.density; createProgressView(); ViewCompat.setChildrenDrawingOrderEnabled(this, true); mNestedScrollingParentHelper = new NestedScrollingParentHelper(this); mNestedScrollingChildHelper = new NestedScrollingChildHelper(this); setNestedScrollingEnabled(true); final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); setEnabled(a.getBoolean(0, true)); a.recycle(); } private void createProgressView() { this.mCircleViews = new CircleImageView[] { new CircleImageView(getContext(), CIRCLE_BG_LIGHT, CIRCLE_DIAMETER/2), new CircleImageView(getContext(), CIRCLE_BG_LIGHT, CIRCLE_DIAMETER/2) }; this.mProgress = new MaterialProgressDrawable[] { new MaterialProgressDrawable(getContext(), this), new MaterialProgressDrawable(getContext(), this) }; for (int i = 0; i < 2; i ++) { mProgress[i].setBackgroundColor(CIRCLE_BG_LIGHT); mCircleViews[i].setImageDrawable(mProgress[i]); mCircleViews[i].setVisibility(View.GONE); addView(mCircleViews[i]); } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); reset(); } // control. private 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(mCircleViews[0]) && !child.equals(mCircleViews[1])) { mTarget = child; break; } } } } public void setDragTriggerDistance(int dir, int distance) { if (dir == DIRECTION_BOTTOM) { distance += mCircleHeight; } mDragTriggerDistances[dir] = distance; } // draw. @Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (mTarget == null) { ensureTarget(); } if (mTarget != null) { mTarget.measure( View.MeasureSpec.makeMeasureSpec( getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec( getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), View.MeasureSpec.EXACTLY)); } for (int i = 0; i < 2; i ++) { mCircleViews[i].measure( View.MeasureSpec.makeMeasureSpec(mCircleWidth, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(mCircleHeight, View.MeasureSpec.EXACTLY)); } if (!mOriginalOffsetCalculated) { mOriginalOffsetCalculated = true; mDragOffsetDistance = 0; } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { final int width = getMeasuredWidth(); final int height = getMeasuredHeight(); if (getChildCount() == 0) { return; } if (mTarget == null) { ensureTarget(); } if (mTarget != null) { final int childLeft = getPaddingLeft(); final int childTop = getPaddingTop(); final int childWidth = width - getPaddingLeft() - getPaddingRight(); final int childHeight = height - getPaddingTop() - getPaddingBottom(); mTarget.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); } if (mDragOffsetDistance == 0) { mCircleViews[0].layout( (width / 2 - mCircleWidth / 2), -mCircleHeight, (width / 2 + mCircleWidth / 2), 0); mCircleViews[1].layout( (width / 2 - mCircleWidth / 2), getMeasuredHeight(), (width / 2 + mCircleWidth / 2), getMeasuredHeight() + mCircleHeight); } else if (mDragOffsetDistance > 0) { mCircleViews[0].layout( (width / 2 - mCircleWidth / 2), mDragOffsetDistance - mCircleHeight, (width / 2 + mCircleWidth / 2), mDragOffsetDistance); mCircleViews[1].layout( (width / 2 - mCircleWidth / 2), getMeasuredHeight(), (width / 2 + mCircleWidth / 2), getMeasuredHeight() + mCircleHeight); } else if (mDragOffsetDistance < 0) { mCircleViews[0].layout( (width / 2 - mCircleWidth / 2), -mCircleHeight, (width / 2 + mCircleWidth / 2), 0); mCircleViews[1].layout( (width / 2 - mCircleWidth / 2), getMeasuredHeight() + mDragOffsetDistance, (width / 2 + mCircleWidth / 2), getMeasuredHeight() + mCircleHeight + mDragOffsetDistance); } } // touch @Override public boolean onInterceptTouchEvent(MotionEvent ev) { ensureTarget(); final int action = MotionEventCompat.getActionMasked(ev); if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { mReturningToStart = false; } if (!isEnabled() || mReturningToStart || mNestedScrollInProgress || mRefreshing || mLoading) { return false; } switch (action) { case MotionEvent.ACTION_DOWN: int oldOffset = mDragOffsetDistance; for (int i = 0; i < 2; i ++) { setTargetOffsetTopAndBottom(i, -oldOffset); } mIsBeingDragged = false; final float initialDownY = ev.getY(); if (initialDownY == -1) { return false; } mInitialDownY = initialDownY; break; case MotionEvent.ACTION_MOVE: final float yDiff = ev.getY() - mInitialDownY; if (yDiff > mTouchSlop && !mIsBeingDragged && !canChildScrollUp() && mPermitRefresh) { mIsBeingDragged = true; mProgress[DIRECTION_TOP].setAlpha(STARTING_PROGRESS_ALPHA); } else if (yDiff < -mTouchSlop && !mIsBeingDragged && !canChildScrollDown() && mPermitLoad) { mIsBeingDragged = true; mProgress[DIRECTION_BOTTOM].setAlpha(STARTING_PROGRESS_ALPHA); } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mIsBeingDragged = false; break; } return mIsBeingDragged; } @Override public boolean onTouchEvent(MotionEvent ev) { final int action = MotionEventCompat.getActionMasked(ev); if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { mReturningToStart = false; } if (!isEnabled() || mReturningToStart || mNestedScrollInProgress || mRefreshing || mLoading) { return false; } switch (action) { case MotionEvent.ACTION_DOWN: mIsBeingDragged = false; break; case MotionEvent.ACTION_MOVE: { final float y = ev.getY(); final float offset = (y - mInitialDownY) * DRAG_RATE; if (mIsBeingDragged) { if (offset > 0 && !canChildScrollUp()) { moveSpinner(DIRECTION_TOP, offset); } else if (offset < 0 && !canChildScrollDown()) { moveSpinner(DIRECTION_BOTTOM, offset); } } break; } case MotionEvent.ACTION_UP: { final float y = ev.getY(); final float offset = (y - mInitialDownY) * DRAG_RATE; mIsBeingDragged = false; if (offset > 0 && !canChildScrollUp()) { finishSpinner(DIRECTION_TOP, offset); } else if (offset < 0 && !canChildScrollDown()) { finishSpinner(DIRECTION_BOTTOM, offset); } return false; } case MotionEvent.ACTION_CANCEL: return false; } return true; } private void moveSpinner(int dir, float dragDistance) { mProgress[dir].showArrow(true); float originalDragPercent = Math.abs(dragDistance) / mDragTriggerDistances[dir]; float dragPercent = Math.min(1f, Math.abs(originalDragPercent)); float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3; float extraOS = Math.abs(dragDistance) - mDragTriggerDistances[dir]; float tensionSlingshotPercent = Math.max(0, Math.min(extraOS, mDragTriggerDistances[dir] * 2) / mDragTriggerDistances[dir]); float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow((tensionSlingshotPercent / 4), 2)) * 2f; float extraMove = (mDragTriggerDistances[dir]) * tensionPercent * 2; int offset = (int) ((mDragTriggerDistances[dir] * dragPercent) + extraMove) * (dir == DIRECTION_TOP ? 1 : -1); // where 1.0f is a full circle if (mCircleViews[dir].getVisibility() != View.VISIBLE) { mCircleViews[dir].setVisibility(View.VISIBLE); } if (!mScale) { ViewCompat.setScaleX(mCircleViews[dir], 1f); ViewCompat.setScaleY(mCircleViews[dir], 1f); } if (mScale) { setAnimationProgress(dir, Math.min(1f, Math.abs(dragDistance / mDragTriggerDistances[dir]))); } if (Math.abs(dragDistance) < mDragTriggerDistances[dir]) { if (mProgress[dir].getAlpha() > STARTING_PROGRESS_ALPHA && !isAnimationRunning(mAlphaStartAnimation)) { // Animate the alpha startProgressAlphaStartAnimation(dir); } } else { if (mProgress[dir].getAlpha() < MAX_ALPHA && !isAnimationRunning(mAlphaMaxAnimation)) { // Animate the alpha startProgressAlphaMaxAnimation(dir); } } float strokeStart = adjustedPercent * .8f; mProgress[dir].setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart)); mProgress[dir].setArrowScale(Math.min(1f, adjustedPercent)); float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f; mProgress[dir].setProgressRotation(rotation); setTargetOffsetTopAndBottom(dir, offset - mDragOffsetDistance); } private void finishSpinner(final int dir, float dragDistance) { if (Math.abs(dragDistance) > mDragTriggerDistances[dir]) { if (dir == DIRECTION_TOP) { setRefreshing(true, true /* notify */); } else { setLoading(true, true); } } else { // cancel refresh if (dir == DIRECTION_TOP) { mRefreshing = false; } else { mLoading = false; } mProgress[dir].setStartEndTrim(0f, 0f); Animation.AnimationListener listener = null; if (!mScale) { listener = new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { if (!mScale) { startScaleDownAnimation(dir, null); } } @Override public void onAnimationRepeat(Animation animation) { } }; } animateOffsetToStartPosition(dir, mDragOffsetDistance, listener); mProgress[dir].showArrow(false); } } private boolean isAnimationRunning(Animation animation) { return animation != null && animation.hasStarted() && !animation.hasEnded(); } /** * @return Whether it is possible for the child view of this layout to * scroll up. Override this if the child view is a custom view. */ public boolean canChildScrollUp() { return ViewCompat.canScrollVertically(mTarget, -1); } /** * @return Whether it is possible for the child view of this layout to * scroll down. Override this if the child view is a custom view. */ public boolean canChildScrollDown() { return ViewCompat.canScrollVertically(mTarget, 1); } @Override public void requestDisallowInterceptTouchEvent(boolean b) { if ((android.os.Build.VERSION.SDK_INT < 21 && mTarget instanceof AbsListView) || (mTarget != null && !ViewCompat.isNestedScrollingEnabled(mTarget))) { // do nothing. } else { super.requestDisallowInterceptTouchEvent(b); } } // state. /** * Notify the widget that refresh state has changed. Do not call this when * refresh is triggered by a swipe gesture. * * @param refreshing Whether or not the view should show refresh progress. */ public void setRefreshing(boolean refreshing) { if (refreshing && (mRefreshing || mLoading)) { return; } if (refreshing) { mCircleViews[DIRECTION_BOTTOM].setVisibility(GONE); // scale and show mRefreshing = true; setTargetOffsetTopAndBottom(DIRECTION_TOP, (int) (mDragTriggerDistances[DIRECTION_TOP] - mDragOffsetDistance)); mNotify = false; startScaleUpAnimation(DIRECTION_TOP, mRefreshListener); } else { setRefreshing(false, false /* notify */); } } /** * Notify the widget that load state has changed. Do not call this when * load is triggered by a swipe gesture. * * @param loading Whether or not the view should show load progress. */ public void setLoading(boolean loading) { if (loading && (mRefreshing || mLoading)) { return; } if (loading) { mCircleViews[DIRECTION_TOP].setVisibility(GONE); // scale and show mLoading = true; setTargetOffsetTopAndBottom(DIRECTION_BOTTOM, (int) (-mDragTriggerDistances[DIRECTION_BOTTOM] - mDragOffsetDistance)); mNotify = false; startScaleUpAnimation(DIRECTION_BOTTOM, mLoadListener); } else { setLoading(false, false /* notify */); } } private void setRefreshing(boolean refreshing, final boolean notify) { if (refreshing && (mRefreshing || mLoading)) { return; } if (mRefreshing != refreshing) { mNotify = notify; ensureTarget(); mRefreshing = refreshing; if (mRefreshing) { animateOffsetToCorrectPosition(DIRECTION_TOP, mDragOffsetDistance, mRefreshListener); } else { startScaleDownAnimation(DIRECTION_TOP, mRefreshListener); } } } private void setLoading(boolean loading, final boolean notify) { if (loading && (mRefreshing || mLoading)) { return; } if (mLoading != loading) { mNotify = notify; ensureTarget(); mLoading = loading; if (mLoading) { animateOffsetToCorrectPosition(DIRECTION_BOTTOM, mDragOffsetDistance, mLoadListener); } else { startScaleDownAnimation(DIRECTION_BOTTOM, mLoadListener); } } } /** * @return Whether the BothWaySwipeRefreshWidget is actively showing refresh * progress. */ public boolean isRefreshing() { return mRefreshing; } /** * @return Whether the BothWaySwipeRefreshWidget is actively showing load * progress. */ public boolean isLoading() { return mLoading; } /** * Set the BothWaySwipeRefreshLayoutWidget is allowed to refresh. * * @param permit Whether it is allowed to refresh. * */ public void setPermitRefresh(boolean permit) { mPermitRefresh = permit; if (!mPermitRefresh && !mPermitLoad) { setEnabled(false); } } /** * Set the BothWaySwipeRefreshLayoutWidget is allowed to load. * * @param permit Whether it is allowed to load. * */ public void setPermitLoad(boolean permit) { mPermitLoad = permit; if (!mPermitRefresh && !mPermitLoad) { setEnabled(false); } } // position. private void moveToStart(int dir, float interpolatedTime) { int offset = (int) (mFrom * (1 - interpolatedTime)); setTargetOffsetTopAndBottom(dir, offset - mDragOffsetDistance); } private void setTargetOffsetTopAndBottom(int dir, int offset) { mCircleViews[dir].bringToFront(); mCircleViews[dir].offsetTopAndBottom(offset); mDragOffsetDistance += offset; } private void reset() { int oldOffset = mDragOffsetDistance; for (int i = 0; i < 2; i ++) { mCircleViews[i].clearAnimation(); mProgress[i].stop(); mCircleViews[i].setVisibility(View.GONE); setColorViewAlpha(i, MAX_ALPHA); // Return the circle to its start position if (mScale) { setAnimationProgress(i, 0 /* animation complete and view is hidden */); } else { setTargetOffsetTopAndBottom(i, -oldOffset); } } mDragOffsetDistance = 0; } // color. private void setColorViewAlpha(int dir, int targetAlpha) { mCircleViews[dir].getBackground().setAlpha(targetAlpha); mProgress[dir].setAlpha(targetAlpha); } /** * Set the background color of the progress spinner disc. * * @param colorRes Resource id of the color. */ public void setProgressBackgroundColorSchemeResource(@ColorRes int colorRes) { setProgressBackgroundColorSchemeColor(ContextCompat.getColor(getContext(), colorRes)); } /** * Set the background color of the progress spinner disc. * * @param color Color. */ public void setProgressBackgroundColorSchemeColor(@ColorInt int color) { for (int i = 0; i < 2; i ++) { mCircleViews[i].setBackgroundColor(color); mProgress[i].setBackgroundColor(color); } } /** * Set the color resources used in the progress animation from color resources. * The first color will also be the color of the bar that grows in response * to a user swipe gesture. * * @param colorResIds Colors. */ public void setColorSchemeResources(@ColorRes int... colorResIds) { int[] colorRes = new int[colorResIds.length]; for (int i = 0; i < colorResIds.length; i++) { colorRes[i] = ContextCompat.getColor(getContext(), colorResIds[i]); } setColorSchemeColors(colorRes); } /** * Set the colors used in the progress animation. The first * color will also be the color of the bar that grows in response to a user * swipe gesture. * * @param colors Color. */ @SuppressLint("SupportAnnotationUsage") @ColorInt public void setColorSchemeColors(int... colors) { ensureTarget(); for (int i = 0; i < 2; i ++) { mProgress[i].setColorSchemeColors(colors); } } // animation. // to correct position. private void animateOffsetToCorrectPosition(int dir, int from, Animation.AnimationListener listener) { mFrom = from; if (dir == DIRECTION_TOP) { mAnimateToTopCorrectPosition.reset(); mAnimateToTopCorrectPosition.setDuration(ANIMATE_TO_TRIGGER_DURATION); mAnimateToTopCorrectPosition.setInterpolator(mDecelerateInterpolator); } else { mAnimateToBottomCorrectPosition.reset(); mAnimateToBottomCorrectPosition.setDuration(ANIMATE_TO_TRIGGER_DURATION); mAnimateToBottomCorrectPosition.setInterpolator(mDecelerateInterpolator); } if (listener != null) { mCircleViews[dir].setAnimationListener(listener); } mCircleViews[dir].clearAnimation(); mCircleViews[dir].startAnimation( dir == DIRECTION_TOP ? mAnimateToTopCorrectPosition : mAnimateToBottomCorrectPosition); } private final Animation mAnimateToTopCorrectPosition = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { setTargetOffsetTopAndBottom( DIRECTION_TOP, (int) (mFrom + (mDragTriggerDistances[DIRECTION_TOP] - mFrom) * interpolatedTime - mDragOffsetDistance)); mProgress[DIRECTION_TOP].setArrowScale(1 - interpolatedTime); } }; private final Animation mAnimateToBottomCorrectPosition = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { setTargetOffsetTopAndBottom( DIRECTION_BOTTOM, (int) (mFrom + (-mDragTriggerDistances[DIRECTION_BOTTOM] - mFrom) * interpolatedTime - mDragOffsetDistance)); mProgress[DIRECTION_BOTTOM].setArrowScale(1 - interpolatedTime); } }; // to start position. private void animateOffsetToStartPosition(int dir, int from, Animation.AnimationListener listener) { if (mScale) { // Scale the item back down if (dir == DIRECTION_TOP) { startScaleDownReturnToTopStartAnimation(from, listener); } else { startScaleDownReturnToBottomStartAnimation(from, listener); } } else { mFrom = from; if (dir == DIRECTION_TOP) { mAnimateToTopStartPosition.reset(); mAnimateToTopStartPosition.setDuration(ANIMATE_TO_START_DURATION); mAnimateToTopStartPosition.setInterpolator(mDecelerateInterpolator); } else { mAnimateToBottomStartPosition.reset(); mAnimateToBottomStartPosition.setDuration(ANIMATE_TO_START_DURATION); mAnimateToBottomStartPosition.setInterpolator(mDecelerateInterpolator); } if (listener != null) { mCircleViews[dir].setAnimationListener(listener); } mCircleViews[dir].clearAnimation(); mCircleViews[dir].startAnimation( dir == DIRECTION_TOP ? mAnimateToTopStartPosition : mAnimateToBottomStartPosition); } } private final Animation mAnimateToTopStartPosition = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { moveToStart(DIRECTION_TOP, interpolatedTime); } }; private final Animation mAnimateToBottomStartPosition = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { moveToStart(DIRECTION_BOTTOM, interpolatedTime); } }; private void startScaleDownReturnToTopStartAnimation(int from, Animation.AnimationListener listener) { mFrom = from; mStartingScale = ViewCompat.getScaleX(mCircleViews[DIRECTION_TOP]); mScaleDownToStartAnimation = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { float targetScale = (mStartingScale + (-mStartingScale * interpolatedTime)); setAnimationProgress(DIRECTION_TOP, targetScale); moveToStart(DIRECTION_TOP, interpolatedTime); } }; mScaleDownToStartAnimation.setDuration(SCALE_DOWN_DURATION); if (listener != null) { mCircleViews[DIRECTION_TOP].setAnimationListener(listener); } mCircleViews[DIRECTION_TOP].clearAnimation(); mCircleViews[DIRECTION_TOP].startAnimation(mScaleDownToStartAnimation); } private void startScaleDownReturnToBottomStartAnimation(int from, Animation.AnimationListener listener) { mFrom = from; mStartingScale = ViewCompat.getScaleX(mCircleViews[DIRECTION_BOTTOM]); mScaleDownToStartAnimation = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { float targetScale = (mStartingScale + (-mStartingScale * interpolatedTime)); setAnimationProgress(DIRECTION_BOTTOM, targetScale); moveToStart(DIRECTION_BOTTOM, interpolatedTime); } }; mScaleDownToStartAnimation.setDuration(SCALE_DOWN_DURATION); if (listener != null) { mCircleViews[DIRECTION_BOTTOM].setAnimationListener(listener); } mCircleViews[DIRECTION_BOTTOM].clearAnimation(); mCircleViews[DIRECTION_BOTTOM].startAnimation(mScaleDownToStartAnimation); } private void startScaleUpAnimation(final int dir, Animation.AnimationListener listener) { mCircleViews[dir].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. mProgress[dir].setAlpha(MAX_ALPHA); } mScaleAnimation = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { setAnimationProgress(dir, interpolatedTime); } }; mScaleAnimation.setDuration(mMediumAnimationDuration); if (listener != null) { mCircleViews[dir].setAnimationListener(listener); } mCircleViews[dir].clearAnimation(); mCircleViews[dir].startAnimation(mScaleAnimation); } private void setAnimationProgress(int dir, float progress) { ViewCompat.setScaleX(mCircleViews[dir], progress); ViewCompat.setScaleY(mCircleViews[dir], progress); } private void startScaleDownAnimation(final int dir, Animation.AnimationListener listener) { mScaleDownAnimation = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { setAnimationProgress(dir, 1 - interpolatedTime); } }; mScaleDownAnimation.setDuration(SCALE_DOWN_DURATION); mCircleViews[dir].setAnimationListener(listener); mCircleViews[dir].clearAnimation(); mCircleViews[dir].startAnimation(mScaleDownAnimation); } private void startProgressAlphaStartAnimation(int dir) { mAlphaStartAnimation = startAlphaAnimation(dir, mProgress[dir].getAlpha(), STARTING_PROGRESS_ALPHA); } private void startProgressAlphaMaxAnimation(int dir) { mAlphaMaxAnimation = startAlphaAnimation(dir, mProgress[dir].getAlpha(), MAX_ALPHA); } private Animation startAlphaAnimation(final int dir, final int startingAlpha, final int endingAlpha) { Animation alpha = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { mProgress[dir].setAlpha((int) (startingAlpha+ ((endingAlpha - startingAlpha) * interpolatedTime))); } }; alpha.setDuration(ALPHA_ANIMATION_DURATION); // Clear out the previous animation listeners. mCircleViews[dir].setAnimationListener(null); mCircleViews[dir].clearAnimation(); mCircleViews[dir].startAnimation(alpha); return alpha; } // listener. // on refresh and load listener. public interface OnRefreshAndLoadListener { void onRefresh(); void onLoad(); } public void setOnRefreshAndLoadListener(OnRefreshAndLoadListener listener) { mListener = listener; } // nested scroll parent. @Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { return isEnabled() && !mReturningToStart && !mRefreshing && !mLoading && (mPermitRefresh || mPermitLoad) && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; } @Override public void onNestedScrollAccepted(View child, View target, int axes) { // Reset the counter of how much leftover scroll needs to be consumed. mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes); // Dispatch up to the nested parent startNestedScroll(axes & ViewCompat.SCROLL_AXIS_VERTICAL); mTotalUnconsumed = 0; mNestedScrollInProgress = true; } @Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { // If we are in the middle of consuming, a scroll, then we want to move the spinner back up // before allowing the list to scroll if (dy > 0 && mTotalUnconsumed > 0) { if (dy > mTotalUnconsumed) { consumed[1] = (int) mTotalUnconsumed; mTotalUnconsumed = 0; } else { mTotalUnconsumed -= dy; consumed[1] = dy; } moveSpinner(DIRECTION_TOP, mTotalUnconsumed); } else if (dy < 0 && mTotalUnconsumed < 0) { if (dy < mTotalUnconsumed) { consumed[1] = (int) mTotalUnconsumed; mTotalUnconsumed = 0; } else { mTotalUnconsumed -= dy; consumed[1] = dy; } moveSpinner(DIRECTION_BOTTOM, mTotalUnconsumed); } // Now let our nested parent consume the leftovers final int[] parentConsumed = mParentScrollConsumed; if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) { consumed[0] += parentConsumed[0]; consumed[1] += parentConsumed[1]; } } @Override public int getNestedScrollAxes() { return mNestedScrollingParentHelper.getNestedScrollAxes(); } @Override public void onNestedScroll(final View target, final int dxConsumed, final int dyConsumed, final int dxUnconsumed, final int dyUnconsumed) { // Dispatch up to the nested parent first dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, mParentOffsetInWindow); // This is a bit of a hack. Nested scrolling works from the bottom up, and as we are // sometimes between two nested scrolling views, we need a way to be able to know when any // nested scrolling parent has stopped handling events. We do that by using the // 'offset in window 'functionality to see if we have been moved from the event. // This is a decent indication of whether we should take over the event stream or not. final int dy = dyUnconsumed + mParentOffsetInWindow[1]; if (dy < 0 && !canChildScrollUp() && !mRefreshing && mPermitRefresh) { mTotalUnconsumed -= dy; moveSpinner(DIRECTION_TOP, mTotalUnconsumed); } else if (dy > 0 && !canChildScrollDown() && !mLoading && mPermitLoad) { mTotalUnconsumed -= dy; moveSpinner(DIRECTION_BOTTOM, mTotalUnconsumed); } } @Override public void onStopNestedScroll(View target) { mNestedScrollingParentHelper.onStopNestedScroll(target); mNestedScrollInProgress = false; // Finish the spinner for nested scrolling if we ever consumed any // unconsumed nested scroll if (mTotalUnconsumed > 0) { finishSpinner(DIRECTION_TOP, mTotalUnconsumed); } else if (mTotalUnconsumed < 0) { finishSpinner(DIRECTION_BOTTOM, mTotalUnconsumed); } mTotalUnconsumed = 0; // Dispatch up our nested parent stopNestedScroll(); } // nested scrolling child. @Override public void setNestedScrollingEnabled(boolean enabled) { mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled); } @Override public boolean isNestedScrollingEnabled() { return mNestedScrollingChildHelper.isNestedScrollingEnabled(); } @Override public boolean startNestedScroll(int axes) { return mNestedScrollingChildHelper.startNestedScroll(axes); } @Override public void stopNestedScroll() { mNestedScrollingChildHelper.stopNestedScroll(); } @Override public boolean hasNestedScrollingParent() { return mNestedScrollingChildHelper.hasNestedScrollingParent(); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { return mNestedScrollingChildHelper.dispatchNestedPreScroll( dx, dy, consumed, offsetInWindow); } @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { return dispatchNestedPreFling(velocityX, velocityY); } @Override public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { return dispatchNestedFling(velocityX, velocityY, consumed); } @Override public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { return mNestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); } @Override public boolean dispatchNestedPreFling(float velocityX, float velocityY) { return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY); } // animation listener. 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 (mRefreshing) { // Make sure the progress view is fully visible mProgress[DIRECTION_TOP].setAlpha(MAX_ALPHA); mProgress[DIRECTION_TOP].start(); if (mNotify) { if (mListener != null) { mListener.onRefresh(); } } mDragOffsetDistance = (int) mDragTriggerDistances[DIRECTION_TOP]; } else { reset(); } } }; private Animation.AnimationListener mLoadListener = new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationRepeat(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { if (mLoading) { // Make sure the progress view is fully visible mProgress[DIRECTION_BOTTOM].setAlpha(MAX_ALPHA); mProgress[DIRECTION_BOTTOM].start(); if (mNotify) { if (mListener != null) { mListener.onLoad(); } } mDragOffsetDistance = (int) -mDragTriggerDistances[DIRECTION_BOTTOM]; } else { reset(); } } }; }