@Override
 public void onAnimationEnd(Animation animation) {
   if (mRefreshing) {
     // Make sure the progress view is fully visible
     mProgress.setAlpha(MAX_ALPHA);
     mProgress.start();
     if (mNotify) {
       if (mListener != null) {
         mListener.onRefresh();
       }
     }
   } else {
     mProgress.stop();
     mCircleView.setVisibility(View.GONE);
     setColorViewAlpha(MAX_ALPHA);
     // Return the circle to its start position
     if (mScale) {
       setAnimationProgress(0 /* animation complete and view is hidden */);
     } else {
       setTargetOffsetTopAndBottom(
           mOriginalOffsetTop - mCurrentTargetOffsetTop, true /* requires update */);
     }
   }
   mCurrentTargetOffsetTop = mCircleView.getTop();
 }
 @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) {
     return;
   }
   final View child = mTarget;
   final int childLeft = getPaddingLeft();
   final int childTop = getPaddingTop();
   final int childWidth = width - getPaddingLeft() - getPaddingRight();
   final int childHeight = height - getPaddingTop() - getPaddingBottom();
   child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
   int circleWidth = mCircleView.getMeasuredWidth();
   int circleHeight = mCircleView.getMeasuredHeight();
   mCircleView.layout(
       (width / 2 - circleWidth / 2),
       mCurrentTargetOffsetTop,
       (width / 2 + circleWidth / 2),
       mCurrentTargetOffsetTop + circleHeight);
 }
 @Override
 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   super.onMeasure(widthMeasureSpec, heightMeasureSpec);
   if (mTarget == null) {
     ensureTarget();
   }
   if (mTarget == null) {
     return;
   }
   mTarget.measure(
       MeasureSpec.makeMeasureSpec(
           getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY),
       MeasureSpec.makeMeasureSpec(
           getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));
   mCircleView.measure(
       MeasureSpec.makeMeasureSpec(mCircleWidth, MeasureSpec.EXACTLY),
       MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY));
   if (!mUsingCustomStart && !mOriginalOffsetCalculated) {
     mOriginalOffsetCalculated = true;
     mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight();
   }
   mCircleViewIndex = -1;
   // Get the index of the circleview.
   for (int index = 0; index < getChildCount(); index++) {
     if (getChildAt(index) == mCircleView) {
       mCircleViewIndex = index;
       break;
     }
   }
 }
 /**
  * The refresh indicator starting and resting position is always positioned near the top of the
  * refreshing content. This position is a consistent location, but can be adjusted in either
  * direction based on whether or not there is a toolbar or actionbar present.
  *
  * @param scale Set to true if there is no view at a higher z-order than where the progress
  *     spinner is set to appear.
  * @param start The offset in pixels from the top of this view at which the progress spinner
  *     should appear.
  * @param end The offset in pixels from the top of this view at which the progress spinner should
  *     come to rest after a successful swipe gesture.
  */
 public void setProgressViewOffset(boolean scale, int start, int end) {
   mScale = scale;
   mCircleView.setVisibility(View.GONE);
   mOriginalOffsetTop = mCurrentTargetOffsetTop = start;
   mSpinnerFinalOffset = end;
   mUsingCustomStart = true;
   mCircleView.invalidate();
 }
 private void createProgressView() {
   mCircleView = new CircleImageView(getContext(), CIRCLE_BG_LIGHT, CIRCLE_DIAMETER / 2);
   mProgress = new MaterialProgressDrawable(getContext(), this);
   mProgress.setBackgroundColor(CIRCLE_BG_LIGHT);
   mCircleView.setImageDrawable(mProgress);
   mCircleView.setVisibility(View.GONE);
   addView(mCircleView);
 }
 private void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate) {
   mCircleView.bringToFront();
   mCircleView.offsetTopAndBottom(offset);
   mCurrentTargetOffsetTop = mCircleView.getTop();
   if (requiresUpdate && android.os.Build.VERSION.SDK_INT < 11) {
     invalidate();
   }
 }
 private void peek(int from, AnimationListener listener) {
   mFrom = from;
   mPeek.reset();
   mPeek.setDuration(500);
   mPeek.setInterpolator(mDecelerateInterpolator);
   if (listener != null) {
     mCircleView.setAnimationListener(listener);
   }
   mCircleView.clearAnimation();
   mCircleView.startAnimation(mPeek);
 }
 private void animateOffsetToCorrectPosition(int from, AnimationListener listener) {
   mFrom = from;
   mAnimateToCorrectPosition.reset();
   mAnimateToCorrectPosition.setDuration(ANIMATE_TO_TRIGGER_DURATION);
   mAnimateToCorrectPosition.setInterpolator(mDecelerateInterpolator);
   if (listener != null) {
     mCircleView.setAnimationListener(listener);
   }
   mCircleView.clearAnimation();
   mCircleView.startAnimation(mAnimateToCorrectPosition);
 }
 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);
   mCircleView.setAnimationListener(listener);
   mCircleView.clearAnimation();
   mCircleView.startAnimation(mScaleDownAnimation);
 }
 private void animateOffsetToStartPosition(int from, AnimationListener listener) {
   if (mScale) {
     // Scale the item back down
     startScaleDownReturnToStartAnimation(from, listener);
   } else {
     mFrom = from;
     mAnimateToStartPosition.reset();
     mAnimateToStartPosition.setDuration(ANIMATE_TO_START_DURATION);
     mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator);
     if (listener != null) {
       mCircleView.setAnimationListener(listener);
     }
     mCircleView.clearAnimation();
     mCircleView.startAnimation(mAnimateToStartPosition);
   }
 }
  private void moveSpinner(float overscrollTop) {
    mProgress.showArrow(true);
    float originalDragPercent = overscrollTop / mTotalDragDistance;

    float dragPercent = Math.min(1f, Math.abs(originalDragPercent));
    float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3;
    float extraOS = Math.abs(overscrollTop) - mTotalDragDistance;
    float slingshotDist =
        mUsingCustomStart ? mSpinnerFinalOffset - mOriginalOffsetTop : 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 = mOriginalOffsetTop + (int) ((slingshotDist * dragPercent) + extraMove);
    // where 1.0f is a full circle
    if (mCircleView.getVisibility() != View.VISIBLE) {
      mCircleView.setVisibility(View.VISIBLE);
    }
    if (!mScale) {
      ViewCompat.setScaleX(mCircleView, 1f);
      ViewCompat.setScaleY(mCircleView, 1f);
    }
    if (overscrollTop < mTotalDragDistance) {
      if (mScale) {
        setAnimationProgress(overscrollTop / mTotalDragDistance);
      }
      if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA
          && !isAnimationRunning(mAlphaStartAnimation)) {
        // Animate the alpha
        startProgressAlphaStartAnimation();
      }
      float strokeStart = adjustedPercent * .8f;
      mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart));
      mProgress.setArrowScale(Math.min(1f, adjustedPercent));
    } else {
      if (mProgress.getAlpha() < MAX_ALPHA && !isAnimationRunning(mAlphaMaxAnimation)) {
        // Animate the alpha
        startProgressAlphaMaxAnimation();
      }
    }
    float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f;
    mProgress.setProgressRotation(rotation);
    setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, true /* requires update */);
  }
 /** One of DEFAULT, or LARGE. */
 public void setSize(int size) {
   if (size != MaterialProgressDrawable.LARGE && size != MaterialProgressDrawable.DEFAULT) {
     return;
   }
   final DisplayMetrics metrics = getResources().getDisplayMetrics();
   if (size == MaterialProgressDrawable.LARGE) {
     mCircleHeight = mCircleWidth = (int) (CIRCLE_DIAMETER_LARGE * metrics.density);
   } else {
     mCircleHeight = mCircleWidth = (int) (CIRCLE_DIAMETER * metrics.density);
   }
   // force the bounds of the progress circle inside the circle view to
   // update by setting it to null before updating its size and then
   // re-setting it
   mCircleView.setImageDrawable(null);
   mProgress.updateSizes(size);
   mCircleView.setImageDrawable(mProgress);
 }
  @Override
  public boolean onInterceptTouchEvent(MotionEvent ev) {
    ensureTarget();

    final int action = MotionEventCompat.getActionMasked(ev);

    if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
      mReturningToStart = false;
    }

    if (!isEnabled() || mReturningToStart || canChildScrollUp() || mRefreshing) {
      // Fail fast if we're not in a state where a swipe is possible
      return false;
    }

    switch (action) {
      case MotionEvent.ACTION_DOWN:
        setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true);
        mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
        mIsBeingDragged = false;
        final float initialDownY = getMotionEventY(ev, mActivePointerId);
        if (initialDownY == -1) {
          return false;
        }
        mInitialDownY = initialDownY;
        break;

      case MotionEvent.ACTION_MOVE:
        if (mActivePointerId == INVALID_POINTER) {
          Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
          return false;
        }

        final float y = getMotionEventY(ev, mActivePointerId);
        if (y == -1) {
          return false;
        }
        final float yDiff = y - mInitialDownY;
        if (yDiff > mTouchSlop && !mIsBeingDragged) {
          mInitialMotionY = mInitialDownY + mTouchSlop;
          mIsBeingDragged = true;
          mProgress.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;
  }
 private Animation startAlphaAnimation(final int startingAlpha, final int endingAlpha) {
   // Pre API 11, alpha is used in place of scale. Don't also use it to
   // show the trigger point.
   if (mScale && isAlphaUsedForScale()) {
     return null;
   }
   Animation alpha =
       new Animation() {
         @Override
         public void applyTransformation(float interpolatedTime, Transformation t) {
           mProgress.setAlpha(
               (int) (startingAlpha + ((endingAlpha - startingAlpha) * interpolatedTime)));
         }
       };
   alpha.setDuration(ALPHA_ANIMATION_DURATION);
   // Clear out the previous animation listeners.
   mCircleView.setAnimationListener(null);
   mCircleView.clearAnimation();
   mCircleView.startAnimation(alpha);
   return alpha;
 }
 private void startScaleUpAnimation(AnimationListener listener) {
   mCircleView.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.setAlpha(MAX_ALPHA);
   }
   mScaleAnimation =
       new Animation() {
         @Override
         public void applyTransformation(float interpolatedTime, Transformation t) {
           setAnimationProgress(interpolatedTime);
         }
       };
   mScaleAnimation.setDuration(mMediumAnimationDuration);
   if (listener != null) {
     mCircleView.setAnimationListener(listener);
   }
   mCircleView.clearAnimation();
   mCircleView.startAnimation(mScaleAnimation);
 }
 @Override
 public void applyTransformation(float interpolatedTime, Transformation t) {
   int targetTop = 0;
   int endTarget = 0;
   if (!mUsingCustomStart) {
     endTarget = (int) (mSpinnerFinalOffset - Math.abs(mOriginalOffsetTop));
   } else {
     endTarget = (int) mSpinnerFinalOffset; // mSpinnerFinalOffset;
   }
   targetTop = (mFrom + (int) ((endTarget - mFrom) * interpolatedTime));
   int offset = targetTop - mCircleView.getTop();
   setTargetOffsetTopAndBottom(offset, false /* requires update */);
   mProgress.setArrowScale(1 - interpolatedTime);
 }
 private void startScaleDownReturnToStartAnimation(
     int from, Animation.AnimationListener listener) {
   mFrom = from;
   if (isAlphaUsedForScale()) {
     mStartingScale = mProgress.getAlpha();
   } else {
     mStartingScale = ViewCompat.getScaleX(mCircleView);
   }
   mScaleDownToStartAnimation =
       new Animation() {
         @Override
         public void applyTransformation(float interpolatedTime, Transformation t) {
           float targetScale = (mStartingScale + (-mStartingScale * interpolatedTime));
           setAnimationProgress(targetScale);
           moveToStart(interpolatedTime);
         }
       };
   mScaleDownToStartAnimation.setDuration(SCALE_DOWN_DURATION);
   if (listener != null) {
     mCircleView.setAnimationListener(listener);
   }
   mCircleView.clearAnimation();
   mCircleView.startAnimation(mScaleDownToStartAnimation);
 }
 /**
  * Get the diameter of the progress circle that is displayed as part of the swipe to refresh
  * layout. This is not valid until a measure pass has completed.
  *
  * @return Diameter in pixels of the progress circle view.
  */
 public int getProgressCircleDiameter() {
   return mCircleView != null ? mCircleView.getMeasuredHeight() : 0;
 }
 private void moveToStart(float interpolatedTime) {
   int targetTop = 0;
   targetTop = (mFrom + (int) ((mOriginalOffsetTop - mFrom) * interpolatedTime));
   int offset = targetTop - mCircleView.getTop();
   setTargetOffsetTopAndBottom(offset, false /* requires update */);
 }
 /**
  * Set the background color of the progress spinner disc.
  *
  * @param color
  */
 public void setProgressBackgroundColorSchemeColor(@ColorInt int color) {
   mCircleView.setBackgroundColor(color);
   mProgress.setBackgroundColor(color);
 }
 private void setColorViewAlpha(int targetAlpha) {
   mCircleView.getBackground().setAlpha(targetAlpha);
   mProgress.setAlpha(targetAlpha);
 }
 /**
  * The refresh indicator resting position is always positioned near the top of the refreshing
  * content. This position is a consistent location, but can be adjusted in either direction based
  * on whether or not there is a toolbar or actionbar present.
  *
  * @param scale Set to true if there is no view at a higher z-order than where the progress
  *     spinner is set to appear.
  * @param end The offset in pixels from the top of this view at which the progress spinner should
  *     come to rest after a successful swipe gesture.
  */
 public void setProgressViewEndTarget(boolean scale, int end) {
   mSpinnerFinalOffset = end;
   mScale = scale;
   mCircleView.invalidate();
 }