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();
            }
        }
    };
}