content: package com.mxt.anitrend.base.custom.view.container;
  
  import android.annotation.SuppressLint;
  import android.content.Context;
  import android.content.res.TypedArray;
  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;
  
  import androidx.annotation.ColorInt;
  import androidx.annotation.ColorRes;
  import androidx.annotation.NonNull;
  import androidx.core.content.ContextCompat;
  import androidx.core.view.MotionEventCompat;
  import androidx.core.view.NestedScrollingChild;
  import androidx.core.view.NestedScrollingChildHelper;
  import androidx.core.view.NestedScrollingParent;
  import androidx.core.view.NestedScrollingParentHelper;
  import androidx.core.view.ViewCompat;
  import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
  
  import com.mxt.anitrend.base.custom.view.drawable.MaterialProgressDrawable;
  import com.mxt.anitrend.base.custom.view.image.CircleImageView;
  
  /**
   * Created by max on 2017/12/05.
   * Both way swipe refresh layout.
   *
   * This is a more powerful {@link SwipeRefreshLayout}, it can swipe
   * to refresh and load.
   *
   * @author wangdaye MySplash
   */
  
  public class CustomSwipeRefreshLayout 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 CustomSwipeRefreshLayout(Context context) {
          this(context, null);
      }
  
      public CustomSwipeRefreshLayout(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(@NonNull View child, @NonNull View target, int nestedScrollAxes) {
          return isEnabled()
                  && !mReturningToStart && !mRefreshing && !mLoading
                  && (mPermitRefresh || mPermitLoad)
                  && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
      }
  
      @Override
      public void onNestedScrollAccepted(@NonNull View child, @NonNull 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(@NonNull View target, int dx, int dy, @NonNull 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(@NonNull 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 boolean onNestedPreFling(@NonNull View target, float velocityX,
                                      float velocityY) {
          return dispatchNestedPreFling(velocityX, velocityY);
      }
  
      @Override
      public boolean onNestedFling(@NonNull View target, float velocityX, float velocityY,
                                   boolean consumed) {
          return dispatchNestedFling(velocityX, velocityY, consumed);
      }
  
      @Override
      public void onStopNestedScroll(@NonNull 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 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();
              }
          }
      };
  }