diff --git a/app/src/main/java/androidx/swiperefreshlayout/widget/CircleImageView.java b/app/src/main/java/androidx/swiperefreshlayout/widget/CircleImageView.java
new file mode 100644
index 0000000000..9addccd9fb
--- /dev/null
+++ b/app/src/main/java/androidx/swiperefreshlayout/widget/CircleImageView.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.swiperefreshlayout.widget;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.RadialGradient;
+import android.graphics.Shader;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.OvalShape;
+import android.view.View;
+import android.view.animation.Animation;
+import android.widget.ImageView;
+
+import androidx.core.content.ContextCompat;
+import androidx.core.view.ViewCompat;
+
+/**
+ * Private class created to work around issues with AnimationListeners being
+ * called before the animation is actually complete and support shadows on older
+ * platforms.
+ */
+class CircleImageView extends ImageView {
+
+ private static final int KEY_SHADOW_COLOR = 0x1E000000;
+ private static final int FILL_SHADOW_COLOR = 0x3D000000;
+ // PX
+ private static final float X_OFFSET = 0f;
+ private static final float Y_OFFSET = 1.75f;
+ private static final float SHADOW_RADIUS = 3.5f;
+ private static final int SHADOW_ELEVATION = 4;
+
+ private Animation.AnimationListener mListener;
+ int mShadowRadius;
+
+ CircleImageView(Context context, int color) {
+ super(context);
+ final float density = getContext().getResources().getDisplayMetrics().density;
+ final int shadowYOffset = (int) (density * Y_OFFSET);
+ final int shadowXOffset = (int) (density * X_OFFSET);
+
+ mShadowRadius = (int) (density * SHADOW_RADIUS);
+
+ ShapeDrawable circle;
+ if (elevationSupported()) {
+ circle = new ShapeDrawable(new OvalShape());
+ ViewCompat.setElevation(this, SHADOW_ELEVATION * density);
+ } else {
+ OvalShape oval = new OvalShadow(mShadowRadius);
+ circle = new ShapeDrawable(oval);
+ setLayerType(View.LAYER_TYPE_SOFTWARE, circle.getPaint());
+ circle.getPaint().setShadowLayer(mShadowRadius, shadowXOffset, shadowYOffset,
+ KEY_SHADOW_COLOR);
+ final int padding = mShadowRadius;
+ // set padding so the inner image sits correctly within the shadow.
+ setPadding(padding, padding, padding, padding);
+ }
+ circle.getPaint().setColor(color);
+ ViewCompat.setBackground(this, circle);
+ }
+
+ private boolean elevationSupported() {
+ return android.os.Build.VERSION.SDK_INT >= 21;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ if (!elevationSupported()) {
+ setMeasuredDimension(getMeasuredWidth() + mShadowRadius * 2, getMeasuredHeight()
+ + mShadowRadius * 2);
+ }
+ }
+
+ public void setAnimationListener(Animation.AnimationListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public void onAnimationStart() {
+ super.onAnimationStart();
+ if (mListener != null) {
+ mListener.onAnimationStart(getAnimation());
+ }
+ }
+
+ @Override
+ public void onAnimationEnd() {
+ super.onAnimationEnd();
+ if (mListener != null) {
+ mListener.onAnimationEnd(getAnimation());
+ }
+ }
+
+ /**
+ * Update the background color of the circle image view.
+ *
+ * @param colorRes Id of a color resource.
+ */
+ public void setBackgroundColorRes(int colorRes) {
+ setBackgroundColor(ContextCompat.getColor(getContext(), colorRes));
+ }
+
+ @Override
+ public void setBackgroundColor(int color) {
+ if (getBackground() instanceof ShapeDrawable) {
+ ((ShapeDrawable) getBackground()).getPaint().setColor(color);
+ }
+ }
+
+ private class OvalShadow extends OvalShape {
+ private RadialGradient mRadialGradient;
+ private Paint mShadowPaint;
+
+ OvalShadow(int shadowRadius) {
+ super();
+ mShadowPaint = new Paint();
+ mShadowRadius = shadowRadius;
+ updateRadialGradient((int) rect().width());
+ }
+
+ @Override
+ protected void onResize(float width, float height) {
+ super.onResize(width, height);
+ updateRadialGradient((int) width);
+ }
+
+ @Override
+ public void draw(Canvas canvas, Paint paint) {
+ final int viewWidth = CircleImageView.this.getWidth();
+ final int viewHeight = CircleImageView.this.getHeight();
+ canvas.drawCircle(viewWidth / 2, viewHeight / 2, viewWidth / 2, mShadowPaint);
+ canvas.drawCircle(viewWidth / 2, viewHeight / 2, viewWidth / 2 - mShadowRadius, paint);
+ }
+
+ private void updateRadialGradient(int diameter) {
+ mRadialGradient = new RadialGradient(diameter / 2, diameter / 2,
+ mShadowRadius, new int[] { FILL_SHADOW_COLOR, Color.TRANSPARENT },
+ null, Shader.TileMode.CLAMP);
+ mShadowPaint.setShader(mRadialGradient);
+ }
+ }
+}
diff --git a/app/src/main/java/androidx/swiperefreshlayout/widget/CircularProgressDrawable.java b/app/src/main/java/androidx/swiperefreshlayout/widget/CircularProgressDrawable.java
new file mode 100644
index 0000000000..58a86cd87c
--- /dev/null
+++ b/app/src/main/java/androidx/swiperefreshlayout/widget/CircularProgressDrawable.java
@@ -0,0 +1,952 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.swiperefreshlayout.widget;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.Path;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.Animatable;
+import android.graphics.drawable.Drawable;
+import android.util.DisplayMetrics;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+/**
+ * Drawable that renders the animated indeterminate progress indicator in the Material design style
+ * without depending on API level 11.
+ *
+ *
While this may be used to draw an indeterminate spinner using {@link #start()} and {@link
+ * #stop()} methods, this may also be used to draw a progress arc using {@link
+ * #setStartEndTrim(float, float)} method. CircularProgressDrawable also supports adding an arrow
+ * at the end of the arc by {@link #setArrowEnabled(boolean)} and {@link #setArrowDimensions(float,
+ * float)} methods.
+ *
+ *
To use one of the pre-defined sizes instead of using your own, {@link #setStyle(int)} should
+ * be called with one of the {@link #DEFAULT} or {@link #LARGE} styles as its parameter. Doing it
+ * so will update the arrow dimensions, ring size and stroke width to fit the one specified.
+ *
+ *
If no center radius is set via {@link #setCenterRadius(float)} or {@link #setStyle(int)}
+ * methods, CircularProgressDrawable will fill the bounds set via {@link #setBounds(Rect)}.
+ */
+public class CircularProgressDrawable extends Drawable implements Animatable {
+ private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
+ private static final Interpolator MATERIAL_INTERPOLATOR = new FastOutSlowInInterpolator();
+
+ /** @hide */
+ @RestrictTo(LIBRARY_GROUP)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({LARGE, DEFAULT})
+ public @interface ProgressDrawableSize {
+ }
+
+ /** Maps to ProgressBar.Large style. */
+ public static final int LARGE = 0;
+
+ private static final float CENTER_RADIUS_LARGE = 11f;
+ private static final float STROKE_WIDTH_LARGE = 3f;
+ private static final int ARROW_WIDTH_LARGE = 12;
+ private static final int ARROW_HEIGHT_LARGE = 6;
+
+ /** Maps to ProgressBar default style. */
+ public static final int DEFAULT = 1;
+
+ private static final float CENTER_RADIUS = 7.5f;
+ private static final float STROKE_WIDTH = 2.5f;
+ private static final int ARROW_WIDTH = 10;
+ private static final int ARROW_HEIGHT = 5;
+
+ /**
+ * This is the default set of colors that's used in spinner. {@link
+ * #setColorSchemeColors(int...)} allows modifying colors.
+ */
+ private static final int[] COLORS = new int[]{
+ Color.BLACK
+ };
+
+ /**
+ * The value in the linear interpolator for animating the drawable at which
+ * the color transition should start
+ */
+ private static final float COLOR_CHANGE_OFFSET = 0.75f;
+ private static final float SHRINK_OFFSET = 0.5f;
+
+ /** The duration of a single progress spin in milliseconds. */
+ private static final int ANIMATION_DURATION = 1332;
+
+ /** Full rotation that's done for the animation duration in degrees. */
+ private static final float GROUP_FULL_ROTATION = 1080f / 5f;
+
+ /** The indicator ring, used to manage animation state. */
+ private final Ring mRing;
+
+ /** Canvas rotation in degrees. */
+ private float mRotation;
+
+ /** Maximum length of the progress arc during the animation. */
+ private static final float MAX_PROGRESS_ARC = .8f;
+ /** Minimum length of the progress arc during the animation. */
+ private static final float MIN_PROGRESS_ARC = .01f;
+
+ /** Rotation applied to ring during the animation, to complete it to a full circle. */
+ private static final float RING_ROTATION = 1f - (MAX_PROGRESS_ARC - MIN_PROGRESS_ARC);
+
+ private Resources mResources;
+ private Animator mAnimator;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ float mRotationCount;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ boolean mFinishing;
+
+ /**
+ * @param context application context
+ */
+ public CircularProgressDrawable(@NonNull Context context) {
+ mResources = Preconditions.checkNotNull(context).getResources();
+
+ mRing = new Ring();
+ mRing.setColors(COLORS);
+
+ setStrokeWidth(STROKE_WIDTH);
+ setupAnimators();
+ }
+
+ /** Sets all parameters at once in dp. */
+ private void setSizeParameters(float centerRadius, float strokeWidth, float arrowWidth,
+ float arrowHeight) {
+ final Ring ring = mRing;
+ final DisplayMetrics metrics = mResources.getDisplayMetrics();
+ final float screenDensity = metrics.density;
+
+ ring.setStrokeWidth(strokeWidth * screenDensity);
+ ring.setCenterRadius(centerRadius * screenDensity);
+ ring.setColorIndex(0);
+ ring.setArrowDimensions(arrowWidth * screenDensity, arrowHeight * screenDensity);
+ }
+
+ /**
+ * Sets the overall size for the progress spinner. This updates the radius
+ * and stroke width of the ring, and arrow dimensions.
+ *
+ * @param size one of {@link #LARGE} or {@link #DEFAULT}
+ */
+ public void setStyle(@ProgressDrawableSize int size) {
+ if (size == LARGE) {
+ setSizeParameters(CENTER_RADIUS_LARGE, STROKE_WIDTH_LARGE, ARROW_WIDTH_LARGE,
+ ARROW_HEIGHT_LARGE);
+ } else {
+ setSizeParameters(CENTER_RADIUS, STROKE_WIDTH, ARROW_WIDTH, ARROW_HEIGHT);
+ }
+ invalidateSelf();
+ }
+
+ /**
+ * Returns the stroke width for the progress spinner in pixels.
+ *
+ * @return stroke width in pixels
+ */
+ public float getStrokeWidth() {
+ return mRing.getStrokeWidth();
+ }
+
+ /**
+ * Sets the stroke width for the progress spinner in pixels.
+ *
+ * @param strokeWidth stroke width in pixels
+ */
+ public void setStrokeWidth(float strokeWidth) {
+ mRing.setStrokeWidth(strokeWidth);
+ invalidateSelf();
+ }
+
+ /**
+ * Returns the center radius for the progress spinner in pixels.
+ *
+ * @return center radius in pixels
+ */
+ public float getCenterRadius() {
+ return mRing.getCenterRadius();
+ }
+
+ /**
+ * Sets the center radius for the progress spinner in pixels. If set to 0, this drawable will
+ * fill the bounds when drawn.
+ *
+ * @param centerRadius center radius in pixels
+ */
+ public void setCenterRadius(float centerRadius) {
+ mRing.setCenterRadius(centerRadius);
+ invalidateSelf();
+ }
+
+ /**
+ * Sets the stroke cap of the progress spinner. Default stroke cap is {@link Paint.Cap#SQUARE}.
+ *
+ * @param strokeCap stroke cap
+ */
+ public void setStrokeCap(@NonNull Paint.Cap strokeCap) {
+ mRing.setStrokeCap(strokeCap);
+ invalidateSelf();
+ }
+
+ /**
+ * Returns the stroke cap of the progress spinner.
+ *
+ * @return stroke cap
+ */
+ @NonNull
+ public Paint.Cap getStrokeCap() {
+ return mRing.getStrokeCap();
+ }
+
+ /**
+ * Returns the arrow width in pixels.
+ *
+ * @return arrow width in pixels
+ */
+ public float getArrowWidth() {
+ return mRing.getArrowWidth();
+ }
+
+ /**
+ * Returns the arrow height in pixels.
+ *
+ * @return arrow height in pixels
+ */
+ public float getArrowHeight() {
+ return mRing.getArrowHeight();
+ }
+
+ /**
+ * Sets the dimensions of the arrow at the end of the spinner in pixels.
+ *
+ * @param width width of the baseline of the arrow in pixels
+ * @param height distance from tip of the arrow to its baseline in pixels
+ */
+ public void setArrowDimensions(float width, float height) {
+ mRing.setArrowDimensions(width, height);
+ invalidateSelf();
+ }
+
+ /**
+ * Returns {@code true} if the arrow at the end of the spinner is shown.
+ *
+ * @return {@code true} if the arrow is shown, {@code false} otherwise.
+ */
+ public boolean getArrowEnabled() {
+ return mRing.getShowArrow();
+ }
+
+ /**
+ * Sets if the arrow at the end of the spinner should be shown.
+ *
+ * @param show {@code true} if the arrow should be drawn, {@code false} otherwise
+ */
+ public void setArrowEnabled(boolean show) {
+ mRing.setShowArrow(show);
+ invalidateSelf();
+ }
+
+ /**
+ * Returns the scale of the arrow at the end of the spinner.
+ *
+ * @return scale of the arrow
+ */
+ public float getArrowScale() {
+ return mRing.getArrowScale();
+ }
+
+ /**
+ * Sets the scale of the arrow at the end of the spinner.
+ *
+ * @param scale scaling that will be applied to the arrow's both width and height when drawing.
+ */
+ public void setArrowScale(float scale) {
+ mRing.setArrowScale(scale);
+ invalidateSelf();
+ }
+
+ /**
+ * Returns the start trim for the progress spinner arc
+ *
+ * @return start trim from [0..1]
+ */
+ public float getStartTrim() {
+ return mRing.getStartTrim();
+ }
+
+ /**
+ * Returns the end trim for the progress spinner arc
+ *
+ * @return end trim from [0..1]
+ */
+ public float getEndTrim() {
+ return mRing.getEndTrim();
+ }
+
+ /**
+ * Sets the start and end trim for the progress spinner arc. 0 corresponds to the geometric
+ * angle of 0 degrees (3 o'clock on a watch) and it increases clockwise, coming to a full circle
+ * at 1.
+ *
+ * @param start starting position of the arc from [0..1]
+ * @param end ending position of the arc from [0..1]
+ */
+ public void setStartEndTrim(float start, float end) {
+ mRing.setStartTrim(start);
+ mRing.setEndTrim(end);
+ invalidateSelf();
+ }
+
+ /**
+ * Returns the amount of rotation applied to the progress spinner.
+ *
+ * @return amount of rotation from [0..1]
+ */
+ public float getProgressRotation() {
+ return mRing.getRotation();
+ }
+
+ /**
+ * Sets the amount of rotation to apply to the progress spinner.
+ *
+ * @param rotation rotation from [0..1]
+ */
+ public void setProgressRotation(float rotation) {
+ mRing.setRotation(rotation);
+ invalidateSelf();
+ }
+
+ /**
+ * Returns the background color of the circle drawn inside the drawable.
+ *
+ * @return an ARGB color
+ */
+ public int getBackgroundColor() {
+ return mRing.getBackgroundColor();
+ }
+
+ /**
+ * Sets the background color of the circle inside the drawable. Calling {@link
+ * #setAlpha(int)} does not affect the visibility background color, so it should be set
+ * separately if it needs to be hidden or visible.
+ *
+ * @param color an ARGB color
+ */
+ public void setBackgroundColor(int color) {
+ mRing.setBackgroundColor(color);
+ invalidateSelf();
+ }
+
+ /**
+ * Returns the colors used in the progress animation
+ *
+ * @return list of ARGB colors
+ */
+ @NonNull
+ public int[] getColorSchemeColors() {
+ return mRing.getColors();
+ }
+
+ /**
+ * Sets the colors used in the progress animation from a color list. The first color will also
+ * be the color to be used if animation is not started yet.
+ *
+ * @param colors list of ARGB colors to be used in the spinner
+ */
+ public void setColorSchemeColors(@NonNull int... colors) {
+ mRing.setColors(colors);
+ mRing.setColorIndex(0);
+ invalidateSelf();
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ final Rect bounds = getBounds();
+ canvas.save();
+ canvas.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY());
+ mRing.draw(canvas, bounds);
+ canvas.restore();
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ mRing.setAlpha(alpha);
+ invalidateSelf();
+ }
+
+ @Override
+ public int getAlpha() {
+ return mRing.getAlpha();
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {
+ mRing.setColorFilter(colorFilter);
+ invalidateSelf();
+ }
+
+ private void setRotation(float rotation) {
+ mRotation = rotation;
+ }
+
+ private float getRotation() {
+ return mRotation;
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ @Override
+ public boolean isRunning() {
+ return mAnimator.isRunning();
+ }
+
+ /**
+ * Starts the animation for the spinner.
+ */
+ @Override
+ public void start() {
+ mAnimator.cancel();
+ mRing.storeOriginals();
+ // Already showing some part of the ring
+ if (mRing.getEndTrim() != mRing.getStartTrim()) {
+ mFinishing = true;
+ mAnimator.setDuration(ANIMATION_DURATION / 2);
+ mAnimator.start();
+ } else {
+ mRing.setColorIndex(0);
+ mRing.resetOriginals();
+ mAnimator.setDuration(ANIMATION_DURATION);
+ mAnimator.start();
+ }
+ }
+
+ /**
+ * Stops the animation for the spinner.
+ */
+ @Override
+ public void stop() {
+ mAnimator.cancel();
+ setRotation(0);
+ mRing.setShowArrow(false);
+ mRing.setColorIndex(0);
+ mRing.resetOriginals();
+ invalidateSelf();
+ }
+
+ // Adapted from ArgbEvaluator.java
+ private int evaluateColorChange(float fraction, int startValue, int endValue) {
+ int startA = (startValue >> 24) & 0xff;
+ int startR = (startValue >> 16) & 0xff;
+ int startG = (startValue >> 8) & 0xff;
+ int startB = startValue & 0xff;
+
+ int endA = (endValue >> 24) & 0xff;
+ int endR = (endValue >> 16) & 0xff;
+ int endG = (endValue >> 8) & 0xff;
+ int endB = endValue & 0xff;
+
+ return (startA + (int) (fraction * (endA - startA))) << 24
+ | (startR + (int) (fraction * (endR - startR))) << 16
+ | (startG + (int) (fraction * (endG - startG))) << 8
+ | (startB + (int) (fraction * (endB - startB)));
+ }
+
+ /**
+ * Update the ring color if this is within the last 25% of the animation.
+ * The new ring color will be a translation from the starting ring color to
+ * the next color.
+ */
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ void updateRingColor(float interpolatedTime, Ring ring) {
+ if (interpolatedTime > COLOR_CHANGE_OFFSET) {
+ ring.setColor(evaluateColorChange((interpolatedTime - COLOR_CHANGE_OFFSET)
+ / (1f - COLOR_CHANGE_OFFSET), ring.getStartingColor(),
+ ring.getNextColor()));
+ } else {
+ ring.setColor(ring.getStartingColor());
+ }
+ }
+
+ /**
+ * Update the ring start and end trim if the animation is finishing (i.e. it started with
+ * already visible progress, so needs to shrink back down before starting the spinner).
+ */
+ private void applyFinishTranslation(float interpolatedTime, Ring ring) {
+ // shrink back down and complete a full rotation before
+ // starting other circles
+ // Rotation goes between [0..1].
+ updateRingColor(interpolatedTime, ring);
+ float targetRotation = (float) (Math.floor(ring.getStartingRotation() / MAX_PROGRESS_ARC)
+ + 1f);
+ final float startTrim = ring.getStartingStartTrim()
+ + (ring.getStartingEndTrim() - MIN_PROGRESS_ARC - ring.getStartingStartTrim())
+ * interpolatedTime;
+ ring.setStartTrim(startTrim);
+ ring.setEndTrim(ring.getStartingEndTrim());
+ final float rotation = ring.getStartingRotation()
+ + ((targetRotation - ring.getStartingRotation()) * interpolatedTime);
+ ring.setRotation(rotation);
+ }
+
+ /**
+ * Update the ring start and end trim according to current time of the animation.
+ */
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ void applyTransformation(float interpolatedTime, Ring ring, boolean lastFrame) {
+ if (mFinishing) {
+ applyFinishTranslation(interpolatedTime, ring);
+ // Below condition is to work around a ValueAnimator issue where onAnimationRepeat is
+ // called before last frame (1f).
+ } else if (interpolatedTime != 1f || lastFrame) {
+ final float startingRotation = ring.getStartingRotation();
+ float startTrim, endTrim;
+
+ if (interpolatedTime < SHRINK_OFFSET) { // Expansion occurs on first half of animation
+ final float scaledTime = interpolatedTime / SHRINK_OFFSET;
+ startTrim = ring.getStartingStartTrim();
+ endTrim = startTrim + ((MAX_PROGRESS_ARC - MIN_PROGRESS_ARC)
+ * MATERIAL_INTERPOLATOR.getInterpolation(scaledTime) + MIN_PROGRESS_ARC);
+ } else { // Shrinking occurs on second half of animation
+ float scaledTime = (interpolatedTime - SHRINK_OFFSET) / (1f - SHRINK_OFFSET);
+ endTrim = ring.getStartingStartTrim() + (MAX_PROGRESS_ARC - MIN_PROGRESS_ARC);
+ startTrim = endTrim - ((MAX_PROGRESS_ARC - MIN_PROGRESS_ARC)
+ * (1f - MATERIAL_INTERPOLATOR.getInterpolation(scaledTime))
+ + MIN_PROGRESS_ARC);
+ }
+
+ final float rotation = startingRotation + (RING_ROTATION * interpolatedTime);
+ float groupRotation = GROUP_FULL_ROTATION * (interpolatedTime + mRotationCount);
+
+ ring.setStartTrim(startTrim);
+ ring.setEndTrim(endTrim);
+ ring.setRotation(rotation);
+ setRotation(groupRotation);
+ }
+ }
+
+ private void setupAnimators() {
+ final Ring ring = mRing;
+ final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
+ animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ float interpolatedTime = (float) animation.getAnimatedValue();
+ updateRingColor(interpolatedTime, ring);
+ applyTransformation(interpolatedTime, ring, false);
+ invalidateSelf();
+ }
+ });
+ animator.setRepeatCount(ValueAnimator.INFINITE);
+ animator.setRepeatMode(ValueAnimator.RESTART);
+ animator.setInterpolator(LINEAR_INTERPOLATOR);
+ animator.addListener(new Animator.AnimatorListener() {
+
+ @Override
+ public void onAnimationStart(Animator animator) {
+ mRotationCount = 0;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ // do nothing
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ // do nothing
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animator) {
+ applyTransformation(1f, ring, true);
+ ring.storeOriginals();
+ ring.goToNextColor();
+ if (mFinishing) {
+ // finished closing the last ring from the swipe gesture; go
+ // into progress mode
+ mFinishing = false;
+ animator.cancel();
+ animator.setDuration(ANIMATION_DURATION);
+ animator.start();
+ ring.setShowArrow(false);
+ } else {
+ mRotationCount = mRotationCount + 1;
+ }
+ }
+ });
+ mAnimator = animator;
+ }
+
+ /**
+ * A private class to do all the drawing of CircularProgressDrawable, which includes background,
+ * progress spinner and the arrow. This class is to separate drawing from animation.
+ */
+ private static class Ring {
+ final RectF mTempBounds = new RectF();
+ final Paint mPaint = new Paint();
+ final Paint mArrowPaint = new Paint();
+ final Paint mCirclePaint = new Paint();
+
+ float mStartTrim = 0f;
+ float mEndTrim = 0f;
+ float mRotation = 0f;
+ float mStrokeWidth = 5f;
+
+ int[] mColors;
+ // mColorIndex represents the offset into the available mColors that the
+ // progress circle should currently display. As the progress circle is
+ // animating, the mColorIndex moves by one to the next available color.
+ int mColorIndex;
+ float mStartingStartTrim;
+ float mStartingEndTrim;
+ float mStartingRotation;
+ boolean mShowArrow;
+ Path mArrow;
+ float mArrowScale = 1;
+ float mRingCenterRadius;
+ int mArrowWidth;
+ int mArrowHeight;
+ int mAlpha = 255;
+ int mCurrentColor;
+
+ Ring() {
+ mPaint.setStrokeCap(Paint.Cap.SQUARE);
+ mPaint.setAntiAlias(true);
+ mPaint.setStyle(Style.STROKE);
+
+ mArrowPaint.setStyle(Style.FILL);
+ mArrowPaint.setAntiAlias(true);
+
+ mCirclePaint.setColor(Color.TRANSPARENT);
+ }
+
+ /**
+ * Sets the dimensions of the arrowhead.
+ *
+ * @param width width of the hypotenuse of the arrow head
+ * @param height height of the arrow point
+ */
+ void setArrowDimensions(float width, float height) {
+ mArrowWidth = (int) width;
+ mArrowHeight = (int) height;
+ }
+
+ void setStrokeCap(Paint.Cap strokeCap) {
+ mPaint.setStrokeCap(strokeCap);
+ }
+
+ Paint.Cap getStrokeCap() {
+ return mPaint.getStrokeCap();
+ }
+
+ float getArrowWidth() {
+ return mArrowWidth;
+ }
+
+ float getArrowHeight() {
+ return mArrowHeight;
+ }
+
+ /**
+ * Draw the progress spinner
+ */
+ void draw(Canvas c, Rect bounds) {
+ final RectF arcBounds = mTempBounds;
+ float arcRadius = mRingCenterRadius + mStrokeWidth / 2f;
+ if (mRingCenterRadius <= 0) {
+ // If center radius is not set, fill the bounds
+ arcRadius = Math.min(bounds.width(), bounds.height()) / 2f - Math.max(
+ (mArrowWidth * mArrowScale) / 2f, mStrokeWidth / 2f);
+ }
+ arcBounds.set(bounds.centerX() - arcRadius,
+ bounds.centerY() - arcRadius,
+ bounds.centerX() + arcRadius,
+ bounds.centerY() + arcRadius);
+
+ final float startAngle = (mStartTrim + mRotation) * 360;
+ final float endAngle = (mEndTrim + mRotation) * 360;
+ float sweepAngle = endAngle - startAngle;
+
+ mPaint.setColor(mCurrentColor);
+ mPaint.setAlpha(mAlpha);
+
+ // Draw the background first
+ float inset = mStrokeWidth / 2f; // Calculate inset to draw inside the arc
+ arcBounds.inset(inset, inset); // Apply inset
+ c.drawCircle(arcBounds.centerX(), arcBounds.centerY(), arcBounds.width() / 2f,
+ mCirclePaint);
+ arcBounds.inset(-inset, -inset); // Revert the inset
+
+ c.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint);
+
+ drawTriangle(c, startAngle, sweepAngle, arcBounds);
+ }
+
+ void drawTriangle(Canvas c, float startAngle, float sweepAngle, RectF bounds) {
+ if (mShowArrow) {
+ if (mArrow == null) {
+ mArrow = new Path();
+ mArrow.setFillType(Path.FillType.EVEN_ODD);
+ } else {
+ mArrow.reset();
+ }
+ float centerRadius = Math.min(bounds.width(), bounds.height()) / 2f;
+ float inset = mArrowWidth * mArrowScale / 2f;
+ // Update the path each time. This works around an issue in SKIA
+ // where concatenating a rotation matrix to a scale matrix
+ // ignored a starting negative rotation. This appears to have
+ // been fixed as of API 21.
+ mArrow.moveTo(0, 0);
+ mArrow.lineTo(mArrowWidth * mArrowScale, 0);
+ mArrow.lineTo((mArrowWidth * mArrowScale / 2), (mArrowHeight
+ * mArrowScale));
+ mArrow.offset(centerRadius + bounds.centerX() - inset,
+ bounds.centerY() + mStrokeWidth / 2f);
+ mArrow.close();
+ // draw a triangle
+ mArrowPaint.setColor(mCurrentColor);
+ mArrowPaint.setAlpha(mAlpha);
+ c.save();
+ c.rotate(startAngle + sweepAngle, bounds.centerX(),
+ bounds.centerY());
+ c.drawPath(mArrow, mArrowPaint);
+ c.restore();
+ }
+ }
+
+ /**
+ * Sets the colors the progress spinner alternates between.
+ *
+ * @param colors array of ARGB colors. Must be non-{@code null}.
+ */
+ void setColors(@NonNull int[] colors) {
+ mColors = colors;
+ // if colors are reset, make sure to reset the color index as well
+ setColorIndex(0);
+ }
+
+ int[] getColors() {
+ return mColors;
+ }
+
+ /**
+ * Sets the absolute color of the progress spinner. This is should only
+ * be used when animating between current and next color when the
+ * spinner is rotating.
+ *
+ * @param color an ARGB color
+ */
+ void setColor(int color) {
+ mCurrentColor = color;
+ }
+
+ /**
+ * Sets the background color of the circle inside the spinner.
+ */
+ void setBackgroundColor(int color) {
+ mCirclePaint.setColor(color);
+ }
+
+ int getBackgroundColor() {
+ return mCirclePaint.getColor();
+ }
+
+ /**
+ * @param index index into the color array of the color to display in
+ * the progress spinner.
+ */
+ void setColorIndex(int index) {
+ mColorIndex = index;
+ mCurrentColor = mColors[mColorIndex];
+ }
+
+ /**
+ * @return int describing the next color the progress spinner should use when drawing.
+ */
+ int getNextColor() {
+ return mColors[getNextColorIndex()];
+ }
+
+ int getNextColorIndex() {
+ return (mColorIndex + 1) % (mColors.length);
+ }
+
+ /**
+ * Proceed to the next available ring color. This will automatically
+ * wrap back to the beginning of colors.
+ */
+ void goToNextColor() {
+ setColorIndex(getNextColorIndex());
+ }
+
+ void setColorFilter(ColorFilter filter) {
+ mPaint.setColorFilter(filter);
+ }
+
+ /**
+ * @param alpha alpha of the progress spinner and associated arrowhead.
+ */
+ void setAlpha(int alpha) {
+ mAlpha = alpha;
+ }
+
+ /**
+ * @return current alpha of the progress spinner and arrowhead
+ */
+ int getAlpha() {
+ return mAlpha;
+ }
+
+ /**
+ * @param strokeWidth set the stroke width of the progress spinner in pixels.
+ */
+ void setStrokeWidth(float strokeWidth) {
+ mStrokeWidth = strokeWidth;
+ mPaint.setStrokeWidth(strokeWidth);
+ }
+
+ float getStrokeWidth() {
+ return mStrokeWidth;
+ }
+
+ void setStartTrim(float startTrim) {
+ mStartTrim = startTrim;
+ }
+
+ float getStartTrim() {
+ return mStartTrim;
+ }
+
+ float getStartingStartTrim() {
+ return mStartingStartTrim;
+ }
+
+ float getStartingEndTrim() {
+ return mStartingEndTrim;
+ }
+
+ int getStartingColor() {
+ return mColors[mColorIndex];
+ }
+
+ void setEndTrim(float endTrim) {
+ mEndTrim = endTrim;
+ }
+
+ float getEndTrim() {
+ return mEndTrim;
+ }
+
+ void setRotation(float rotation) {
+ mRotation = rotation;
+ }
+
+ float getRotation() {
+ return mRotation;
+ }
+
+ /**
+ * @param centerRadius inner radius in px of the circle the progress spinner arc traces
+ */
+ void setCenterRadius(float centerRadius) {
+ mRingCenterRadius = centerRadius;
+ }
+
+ float getCenterRadius() {
+ return mRingCenterRadius;
+ }
+
+ /**
+ * @param show {@code true} if should show the arrow head on the progress spinner
+ */
+ void setShowArrow(boolean show) {
+ if (mShowArrow != show) {
+ mShowArrow = show;
+ }
+ }
+
+ boolean getShowArrow() {
+ return mShowArrow;
+ }
+
+ /**
+ * @param scale scale of the arrowhead for the spinner
+ */
+ void setArrowScale(float scale) {
+ if (scale != mArrowScale) {
+ mArrowScale = scale;
+ }
+ }
+
+ float getArrowScale() {
+ return mArrowScale;
+ }
+
+ /**
+ * @return The amount the progress spinner is currently rotated, between [0..1].
+ */
+ float getStartingRotation() {
+ return mStartingRotation;
+ }
+
+ /**
+ * If the start / end trim are offset to begin with, store them so that animation starts
+ * from that offset.
+ */
+ void storeOriginals() {
+ mStartingStartTrim = mStartTrim;
+ mStartingEndTrim = mEndTrim;
+ mStartingRotation = mRotation;
+ }
+
+ /**
+ * Reset the progress spinner to default rotation, start and end angles.
+ */
+ void resetOriginals() {
+ mStartingStartTrim = 0;
+ mStartingEndTrim = 0;
+ mStartingRotation = 0;
+ setStartTrim(0);
+ setEndTrim(0);
+ setRotation(0);
+ }
+ }
+}
diff --git a/app/src/main/java/androidx/swiperefreshlayout/widget/SwipeRefreshLayout.java b/app/src/main/java/androidx/swiperefreshlayout/widget/SwipeRefreshLayout.java
new file mode 100644
index 0000000000..bf1c742482
--- /dev/null
+++ b/app/src/main/java/androidx/swiperefreshlayout/widget/SwipeRefreshLayout.java
@@ -0,0 +1,1212 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.swiperefreshlayout.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+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.Animation.AnimationListener;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Transformation;
+import android.widget.AbsListView;
+import android.widget.ListView;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.ColorRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.Px;
+import androidx.annotation.VisibleForTesting;
+import androidx.core.content.ContextCompat;
+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.core.widget.ListViewCompat;
+
+/**
+ * The SwipeRefreshLayout should be used whenever the user can refresh the
+ * contents of a view via a vertical swipe gesture. The activity that
+ * instantiates this view should add an OnRefreshListener to be notified
+ * whenever the swipe to refresh gesture is completed. The SwipeRefreshLayout
+ * will notify the listener each and every time the gesture is completed again;
+ * the listener is responsible for correctly determining when to actually
+ * initiate a refresh of its content. If the listener determines there should
+ * not be a refresh, it must call setRefreshing(false) to cancel any visual
+ * indication of a refresh. If an activity wishes to show just the progress
+ * animation, it should call setRefreshing(true). To disable the gesture and
+ * progress animation, call setEnabled(false) on the view.
+ *
+ * This layout should be made the parent of the view that will be refreshed as a
+ * result of the gesture and can only support one direct child. This view will
+ * also be made the target of the gesture and will be forced to match both the
+ * width and the height supplied in this layout. The SwipeRefreshLayout does not
+ * provide accessibility events; instead, a menu item must be provided to allow
+ * refresh of the content wherever this gesture is used.
+ *
+ */
+public class SwipeRefreshLayout extends ViewGroup implements NestedScrollingParent,
+ NestedScrollingChild {
+ // Maps to ProgressBar.Large style
+ public static final int LARGE = CircularProgressDrawable.LARGE;
+ // Maps to ProgressBar default style
+ public static final int DEFAULT = CircularProgressDrawable.DEFAULT;
+
+ public static final int DEFAULT_SLINGSHOT_DISTANCE = -1;
+
+ @VisibleForTesting
+ static final int CIRCLE_DIAMETER = 40;
+ @VisibleForTesting
+ static final int CIRCLE_DIAMETER_LARGE = 56;
+
+ private static final String LOG_TAG = SwipeRefreshLayout.class.getSimpleName();
+
+ private static final int MAX_ALPHA = 255;
+ private static final int STARTING_PROGRESS_ALPHA = (int) (.3f * MAX_ALPHA);
+
+ private static final float DECELERATE_INTERPOLATION_FACTOR = 2f;
+ private static final int INVALID_POINTER = -1;
+ 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
+ OnRefreshListener mListener;
+ boolean mRefreshing = false;
+ private int mTouchSlop;
+ private float mTotalDragDistance = -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;
+ int mCurrentTargetOffsetTop;
+
+ private float mInitialMotionY;
+ private float mInitialDownY;
+ private boolean mIsBeingDragged;
+ private int mActivePointerId = INVALID_POINTER;
+ // Whether this item is scaled up rather than clipped
+ boolean mScale;
+
+ // 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
+ };
+
+ CircleImageView mCircleView;
+ private int mCircleViewIndex = -1;
+
+ protected int mFrom;
+
+ float mStartingScale;
+
+ protected int mOriginalOffsetTop;
+
+ int mSpinnerOffsetEnd;
+
+ int mCustomSlingshotDistance;
+
+ CircularProgressDrawable mProgress;
+
+ private Animation mScaleAnimation;
+
+ private Animation mScaleDownAnimation;
+
+ private Animation mAlphaStartAnimation;
+
+ private Animation mAlphaMaxAnimation;
+
+ private Animation mScaleDownToStartAnimation;
+
+ boolean mNotify;
+
+ private int mCircleDiameter;
+
+ // Whether the client has set a custom starting position;
+ boolean mUsingCustomStart;
+
+ private OnChildScrollUpCallback mChildScrollUpCallback;
+
+ private AnimationListener mRefreshListener = new 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.setAlpha(MAX_ALPHA);
+ mProgress.start();
+ if (mNotify) {
+ if (mListener != null) {
+ mListener.onRefresh();
+ }
+ }
+ mCurrentTargetOffsetTop = mCircleView.getTop();
+ } else {
+ reset();
+ }
+ }
+ };
+
+ void reset() {
+ mCircleView.clearAnimation();
+ 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);
+ }
+ mCurrentTargetOffsetTop = mCircleView.getTop();
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ if (!enabled) {
+ reset();
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ reset();
+ }
+
+ private void setColorViewAlpha(int targetAlpha) {
+ mCircleView.getBackground().setAlpha(targetAlpha);
+ mProgress.setAlpha(targetAlpha);
+ }
+
+ /**
+ * 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.
+ *
+ * Note: Calling this will reset the position of the refresh indicator to
+ * start.
+ *
+ *
+ * @param scale Set to true if there is no view at a higher z-order than where the progress
+ * spinner is set to appear. Setting it to true will cause indicator to be scaled
+ * up rather than clipped.
+ * @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;
+ mOriginalOffsetTop = start;
+ mSpinnerOffsetEnd = end;
+ mUsingCustomStart = true;
+ reset();
+ mRefreshing = false;
+ }
+
+ /**
+ * @return The offset in pixels from the top of this view at which the progress spinner should
+ * appear.
+ */
+ public int getProgressViewStartOffset() {
+ return mOriginalOffsetTop;
+ }
+
+ /**
+ * @return 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 int getProgressViewEndOffset() {
+ return mSpinnerOffsetEnd;
+ }
+
+ /**
+ * 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. Setting it to true will cause indicator to be scaled
+ * up rather than clipped.
+ * @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) {
+ mSpinnerOffsetEnd = end;
+ mScale = scale;
+ mCircleView.invalidate();
+ }
+
+ /**
+ * Sets a custom slingshot distance.
+ *
+ * @param slingshotDistance The distance in pixels that the refresh indicator can be pulled
+ * beyond its resting position. Use
+ * {@link #DEFAULT_SLINGSHOT_DISTANCE} to reset to the default value.
+ *
+ */
+ public void setSlingshotDistance(@Px int slingshotDistance) {
+ mCustomSlingshotDistance = slingshotDistance;
+ }
+
+ /**
+ * One of DEFAULT, or LARGE.
+ */
+ public void setSize(int size) {
+ if (size != CircularProgressDrawable.LARGE && size != CircularProgressDrawable.DEFAULT) {
+ return;
+ }
+ final DisplayMetrics metrics = getResources().getDisplayMetrics();
+ if (size == CircularProgressDrawable.LARGE) {
+ mCircleDiameter = (int) (CIRCLE_DIAMETER_LARGE * metrics.density);
+ } else {
+ mCircleDiameter = (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.setStyle(size);
+ mCircleView.setImageDrawable(mProgress);
+ }
+
+ /**
+ * Simple constructor to use when creating a SwipeRefreshLayout from code.
+ *
+ * @param context
+ */
+ public SwipeRefreshLayout(@NonNull Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Constructor that is called when inflating SwipeRefreshLayout from XML.
+ *
+ * @param context
+ * @param attrs
+ */
+ public SwipeRefreshLayout(@NonNull Context context, @Nullable 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();
+ mCircleDiameter = (int) (CIRCLE_DIAMETER * metrics.density);
+
+ createProgressView();
+ setChildrenDrawingOrderEnabled(true);
+ // the absolute offset has to take into account that the circle starts at an offset
+ mSpinnerOffsetEnd = (int) (DEFAULT_CIRCLE_TARGET * metrics.density);
+ mTotalDragDistance = mSpinnerOffsetEnd;
+ mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
+
+ mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
+ setNestedScrollingEnabled(true);
+
+ mOriginalOffsetTop = mCurrentTargetOffsetTop = -mCircleDiameter;
+ moveToStart(1.0f);
+
+ final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
+ setEnabled(a.getBoolean(0, true));
+ a.recycle();
+ }
+
+ @Override
+ protected int getChildDrawingOrder(int childCount, int i) {
+ if (mCircleViewIndex < 0) {
+ return i;
+ } else if (i == childCount - 1) {
+ // Draw the selected child last
+ return mCircleViewIndex;
+ } else if (i >= mCircleViewIndex) {
+ // Move the children after the selected child earlier one
+ return i + 1;
+ } else {
+ // Keep the children before the selected child the same
+ return i;
+ }
+ }
+
+ private void createProgressView() {
+ mCircleView = new CircleImageView(getContext(), CIRCLE_BG_LIGHT);
+ mProgress = new CircularProgressDrawable(getContext());
+ mProgress.setStyle(CircularProgressDrawable.DEFAULT);
+ mCircleView.setImageDrawable(mProgress);
+ mCircleView.setVisibility(View.GONE);
+ addView(mCircleView);
+ }
+
+ /**
+ * Set the listener to be notified when a refresh is triggered via the swipe
+ * gesture.
+ */
+ public void setOnRefreshListener(@Nullable OnRefreshListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * 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 != refreshing) {
+ // scale and show
+ mRefreshing = refreshing;
+ int endTarget = 0;
+ if (!mUsingCustomStart) {
+ endTarget = mSpinnerOffsetEnd + mOriginalOffsetTop;
+ } else {
+ endTarget = mSpinnerOffsetEnd;
+ }
+ setTargetOffsetTopAndBottom(endTarget - mCurrentTargetOffsetTop);
+ mNotify = false;
+ startScaleUpAnimation(mRefreshListener);
+ } else {
+ setRefreshing(refreshing, false /* notify */);
+ }
+ }
+
+ private void startScaleUpAnimation(AnimationListener listener) {
+ mCircleView.setVisibility(View.VISIBLE);
+ 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);
+ }
+
+ /**
+ * Pre API 11, this does an alpha animation.
+ * @param progress
+ */
+ void setAnimationProgress(float progress) {
+ mCircleView.setScaleX(progress);
+ mCircleView.setScaleY(progress);
+ }
+
+ private void setRefreshing(boolean refreshing, final boolean notify) {
+ if (mRefreshing != refreshing) {
+ mNotify = notify;
+ ensureTarget();
+ mRefreshing = refreshing;
+ if (mRefreshing) {
+ animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener);
+ } else {
+ startScaleDownAnimation(mRefreshListener);
+ }
+ }
+ }
+
+ void startScaleDownAnimation(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 startProgressAlphaStartAnimation() {
+ mAlphaStartAnimation = startAlphaAnimation(mProgress.getAlpha(), STARTING_PROGRESS_ALPHA);
+ }
+
+ private void startProgressAlphaMaxAnimation() {
+ mAlphaMaxAnimation = startAlphaAnimation(mProgress.getAlpha(), MAX_ALPHA);
+ }
+
+ private Animation startAlphaAnimation(final int startingAlpha, final int endingAlpha) {
+ 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;
+ }
+
+ /**
+ * @deprecated Use {@link #setProgressBackgroundColorSchemeResource(int)}
+ */
+ @Deprecated
+ public void setProgressBackgroundColor(int colorRes) {
+ setProgressBackgroundColorSchemeResource(colorRes);
+ }
+
+ /**
+ * 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
+ */
+ public void setProgressBackgroundColorSchemeColor(@ColorInt int color) {
+ mCircleView.setBackgroundColor(color);
+ }
+
+ /**
+ * @deprecated Use {@link #setColorSchemeResources(int...)}
+ */
+ @Deprecated
+ public void setColorScheme(@ColorRes int... colors) {
+ setColorSchemeResources(colors);
+ }
+
+ /**
+ * 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
+ */
+ public void setColorSchemeResources(@ColorRes int... colorResIds) {
+ final Context context = getContext();
+ int[] colorRes = new int[colorResIds.length];
+ for (int i = 0; i < colorResIds.length; i++) {
+ colorRes[i] = ContextCompat.getColor(context, 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
+ */
+ public void setColorSchemeColors(@ColorInt int... colors) {
+ ensureTarget();
+ mProgress.setColorSchemeColors(colors);
+ }
+
+ /**
+ * @return Whether the SwipeRefreshWidget is actively showing refresh
+ * progress.
+ */
+ public boolean isRefreshing() {
+ return mRefreshing;
+ }
+
+ 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(mCircleView)) {
+ mTarget = child;
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Set the distance to trigger a sync in dips
+ *
+ * @param distance
+ */
+ public void setDistanceToTriggerSync(int distance) {
+ mTotalDragDistance = distance;
+ }
+
+ @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(mCircleDiameter, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(mCircleDiameter, MeasureSpec.EXACTLY));
+ mCircleViewIndex = -1;
+ // Get the index of the circleview.
+ for (int index = 0; index < getChildCount(); index++) {
+ if (getChildAt(index) == mCircleView) {
+ mCircleViewIndex = index;
+ break;
+ }
+ }
+ }
+
+ /**
+ * Get the diameter of the progress circle that is displayed as part of the
+ * swipe to refresh layout.
+ *
+ * @return Diameter in pixels of the progress circle view.
+ */
+ public int getProgressCircleDiameter() {
+ return mCircleDiameter;
+ }
+
+ /**
+ * @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() {
+ if (mChildScrollUpCallback != null) {
+ return mChildScrollUpCallback.canChildScrollUp(this, mTarget);
+ }
+ if (mTarget instanceof ListView) {
+ return ListViewCompat.canScrollList((ListView) mTarget, -1);
+ }
+ return mTarget.canScrollVertically(-1);
+ }
+
+ /**
+ * Set a callback to override {@link SwipeRefreshLayout#canChildScrollUp()} method. Non-null
+ * callback will return the value provided by the callback and ignore all internal logic.
+ * @param callback Callback that should be called when canChildScrollUp() is called.
+ */
+ public void setOnChildScrollUpCallback(@Nullable OnChildScrollUpCallback callback) {
+ mChildScrollUpCallback = callback;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ ensureTarget();
+
+ final int action = ev.getActionMasked();
+ int pointerIndex;
+
+ if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
+ mReturningToStart = false;
+ }
+
+ if (!isEnabled() || mReturningToStart || canChildScrollUp()
+ || mRefreshing || mNestedScrollInProgress) {
+ // 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());
+ mActivePointerId = ev.getPointerId(0);
+ mIsBeingDragged = false;
+
+ pointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (pointerIndex < 0) {
+ return false;
+ }
+ mInitialDownY = ev.getY(pointerIndex);
+ 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;
+ }
+
+ pointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (pointerIndex < 0) {
+ return false;
+ }
+ final float y = ev.getY(pointerIndex);
+ startDragging(y);
+ break;
+
+ case MotionEvent.ACTION_POINTER_UP:
+ onSecondaryPointerUp(ev);
+ break;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ mIsBeingDragged = false;
+ mActivePointerId = INVALID_POINTER;
+ break;
+ }
+
+ return mIsBeingDragged;
+ }
+
+ @Override
+ public void requestDisallowInterceptTouchEvent(boolean b) {
+ // if this is a List < L or another view that doesn't support nested
+ // scrolling, ignore this request so that the vertical scroll event
+ // isn't stolen
+ if ((android.os.Build.VERSION.SDK_INT < 21 && mTarget instanceof AbsListView)
+ || (mTarget != null && !ViewCompat.isNestedScrollingEnabled(mTarget))) {
+ // Nope.
+ } else {
+ super.requestDisallowInterceptTouchEvent(b);
+ }
+ }
+
+ // NestedScrollingParent
+
+ @Override
+ public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
+ return isEnabled() && !mReturningToStart && !mRefreshing
+ && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
+ }
+
+ @Override
+ public void onNestedScrollAccepted(View child, View target, int axes) {
+ // Reset the counter of how much leftover scroll needs to be consumed.
+ mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes);
+ // Dispatch up to the nested parent
+ startNestedScroll(axes & ViewCompat.SCROLL_AXIS_VERTICAL);
+ mTotalUnconsumed = 0;
+ mNestedScrollInProgress = true;
+ }
+
+ @Override
+ public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
+ // If we are in the middle of consuming, a scroll, then we want to move the spinner back up
+ // before allowing the list to scroll
+ if (dy > 0 && mTotalUnconsumed > 0) {
+ if (dy > mTotalUnconsumed) {
+ consumed[1] = dy - (int) mTotalUnconsumed;
+ mTotalUnconsumed = 0;
+ } else {
+ mTotalUnconsumed -= dy;
+ consumed[1] = dy;
+ }
+ moveSpinner(mTotalUnconsumed);
+ }
+
+ // If a client layout is using a custom start position for the circle
+ // view, they mean to hide it again before scrolling the child view
+ // If we get back to mTotalUnconsumed == 0 and there is more to go, hide
+ // the circle so it isn't exposed if its blocking content is moved
+ if (mUsingCustomStart && dy > 0 && mTotalUnconsumed == 0
+ && Math.abs(dy - consumed[1]) > 0) {
+ mCircleView.setVisibility(View.GONE);
+ }
+
+ // 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 onStopNestedScroll(View target) {
+ mNestedScrollingParentHelper.onStopNestedScroll(target);
+ mNestedScrollInProgress = false;
+ // Finish the spinner for nested scrolling if we ever consumed any
+ // unconsumed nested scroll
+ if (mTotalUnconsumed > 0) {
+ finishSpinner(mTotalUnconsumed);
+ mTotalUnconsumed = 0;
+ }
+ // Dispatch up our nested parent
+ stopNestedScroll();
+ }
+
+ @Override
+ public void onNestedScroll(final View target, final int dxConsumed, final int dyConsumed,
+ final int dxUnconsumed, final int dyUnconsumed) {
+ // Dispatch up to the nested parent first
+ dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
+ mParentOffsetInWindow);
+
+ // This is a bit of a hack. Nested scrolling works from the bottom up, and as we are
+ // sometimes between two nested scrolling views, we need a way to be able to know when any
+ // nested scrolling parent has stopped handling events. We do that by using the
+ // 'offset in window 'functionality to see if we have been moved from the event.
+ // This is a decent indication of whether we should take over the event stream or not.
+ final int dy = dyUnconsumed + mParentOffsetInWindow[1];
+ if (dy < 0 && !canChildScrollUp()) {
+ mTotalUnconsumed += Math.abs(dy);
+ moveSpinner(mTotalUnconsumed);
+ }
+ }
+
+ // NestedScrollingChild
+
+ @Override
+ public void setNestedScrollingEnabled(boolean enabled) {
+ mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled);
+ }
+
+ @Override
+ public boolean isNestedScrollingEnabled() {
+ return mNestedScrollingChildHelper.isNestedScrollingEnabled();
+ }
+
+ @Override
+ public boolean startNestedScroll(int axes) {
+ return mNestedScrollingChildHelper.startNestedScroll(axes);
+ }
+
+ @Override
+ public void stopNestedScroll() {
+ mNestedScrollingChildHelper.stopNestedScroll();
+ }
+
+ @Override
+ public boolean hasNestedScrollingParent() {
+ return mNestedScrollingChildHelper.hasNestedScrollingParent();
+ }
+
+ @Override
+ public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
+ int dyUnconsumed, int[] offsetInWindow) {
+ return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed,
+ dxUnconsumed, dyUnconsumed, offsetInWindow);
+ }
+
+ @Override
+ public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
+ return mNestedScrollingChildHelper.dispatchNestedPreScroll(
+ dx, dy, consumed, offsetInWindow);
+ }
+
+ @Override
+ public boolean onNestedPreFling(View target, float velocityX,
+ float velocityY) {
+ return dispatchNestedPreFling(velocityX, velocityY);
+ }
+
+ @Override
+ public boolean onNestedFling(View target, float velocityX, float velocityY,
+ boolean consumed) {
+ return dispatchNestedFling(velocityX, velocityY, consumed);
+ }
+
+ @Override
+ public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
+ return mNestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
+ }
+
+ @Override
+ public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
+ return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
+ }
+
+ private boolean isAnimationRunning(Animation animation) {
+ return animation != null && animation.hasStarted() && !animation.hasEnded();
+ }
+
+ private void moveSpinner(float overscrollTop) {
+ mProgress.setArrowEnabled(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 = mCustomSlingshotDistance > 0
+ ? mCustomSlingshotDistance
+ : (mUsingCustomStart
+ ? mSpinnerOffsetEnd - mOriginalOffsetTop
+ : mSpinnerOffsetEnd);
+ 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) {
+ mCircleView.setScaleX(1f);
+ mCircleView.setScaleY(1f);
+ }
+
+ if (mScale) {
+ setAnimationProgress(Math.min(1f, overscrollTop / mTotalDragDistance));
+ }
+ if (overscrollTop < mTotalDragDistance) {
+ if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA
+ && !isAnimationRunning(mAlphaStartAnimation)) {
+ // Animate the alpha
+ startProgressAlphaStartAnimation();
+ }
+ } else {
+ if (mProgress.getAlpha() < MAX_ALPHA && !isAnimationRunning(mAlphaMaxAnimation)) {
+ // Animate the alpha
+ startProgressAlphaMaxAnimation();
+ }
+ }
+ float strokeStart = adjustedPercent * .8f;
+ mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart));
+ mProgress.setArrowScale(Math.min(1f, adjustedPercent));
+
+ float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f;
+ mProgress.setProgressRotation(rotation);
+ setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop);
+ }
+
+ private void finishSpinner(float overscrollTop) {
+ if (overscrollTop > mTotalDragDistance) {
+ setRefreshing(true, true /* notify */);
+ } else {
+ // cancel refresh
+ mRefreshing = false;
+ mProgress.setStartEndTrim(0f, 0f);
+ AnimationListener listener = null;
+ if (!mScale) {
+ listener = new AnimationListener() {
+
+ @Override
+ public void onAnimationStart(Animation animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ if (!mScale) {
+ startScaleDownAnimation(null);
+ }
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ }
+
+ };
+ }
+ animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener);
+ mProgress.setArrowEnabled(false);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ final int action = ev.getActionMasked();
+ int pointerIndex = -1;
+
+ if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
+ mReturningToStart = false;
+ }
+
+ if (!isEnabled() || mReturningToStart || canChildScrollUp()
+ || mRefreshing || mNestedScrollInProgress) {
+ // Fail fast if we're not in a state where a swipe is possible
+ return false;
+ }
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mActivePointerId = ev.getPointerId(0);
+ mIsBeingDragged = false;
+ break;
+
+ case MotionEvent.ACTION_MOVE: {
+ pointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (pointerIndex < 0) {
+ Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
+ return false;
+ }
+
+ final float y = ev.getY(pointerIndex);
+ startDragging(y);
+
+ if (mIsBeingDragged) {
+ final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
+ if (overscrollTop > 0) {
+ moveSpinner(overscrollTop);
+ } else {
+ return false;
+ }
+ }
+ break;
+ }
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ pointerIndex = ev.getActionIndex();
+ if (pointerIndex < 0) {
+ Log.e(LOG_TAG,
+ "Got ACTION_POINTER_DOWN event but have an invalid action index.");
+ return false;
+ }
+ mActivePointerId = ev.getPointerId(pointerIndex);
+ break;
+ }
+
+ case MotionEvent.ACTION_POINTER_UP:
+ onSecondaryPointerUp(ev);
+ break;
+
+ case MotionEvent.ACTION_UP: {
+ pointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (pointerIndex < 0) {
+ Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id.");
+ return false;
+ }
+
+ if (mIsBeingDragged) {
+ final float y = ev.getY(pointerIndex);
+ final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
+ mIsBeingDragged = false;
+ finishSpinner(overscrollTop);
+ }
+ mActivePointerId = INVALID_POINTER;
+ return false;
+ }
+ case MotionEvent.ACTION_CANCEL:
+ return false;
+ }
+
+ return true;
+ }
+
+ private void startDragging(float y) {
+ final float yDiff = y - mInitialDownY;
+ if (yDiff > mTouchSlop && !mIsBeingDragged) {
+ mInitialMotionY = mInitialDownY + mTouchSlop;
+ mIsBeingDragged = true;
+ mProgress.setAlpha(STARTING_PROGRESS_ALPHA);
+ }
+ }
+
+ 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 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 final Animation mAnimateToCorrectPosition = new Animation() {
+ @Override
+ public void applyTransformation(float interpolatedTime, Transformation t) {
+ int targetTop = 0;
+ int endTarget = 0;
+ if (!mUsingCustomStart) {
+ endTarget = mSpinnerOffsetEnd - Math.abs(mOriginalOffsetTop);
+ } else {
+ endTarget = mSpinnerOffsetEnd;
+ }
+ targetTop = (mFrom + (int) ((endTarget - mFrom) * interpolatedTime));
+ int offset = targetTop - mCircleView.getTop();
+ setTargetOffsetTopAndBottom(offset);
+ mProgress.setArrowScale(1 - interpolatedTime);
+ }
+ };
+
+ void moveToStart(float interpolatedTime) {
+ int targetTop = 0;
+ targetTop = (mFrom + (int) ((mOriginalOffsetTop - mFrom) * interpolatedTime));
+ int offset = targetTop - mCircleView.getTop();
+ setTargetOffsetTopAndBottom(offset);
+ }
+
+ private final Animation mAnimateToStartPosition = new Animation() {
+ @Override
+ public void applyTransformation(float interpolatedTime, Transformation t) {
+ moveToStart(interpolatedTime);
+ }
+ };
+
+ private void startScaleDownReturnToStartAnimation(int from,
+ AnimationListener listener) {
+ mFrom = from;
+ mStartingScale = mCircleView.getScaleX();
+ 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);
+ }
+
+ void setTargetOffsetTopAndBottom(int offset) {
+ mCircleView.bringToFront();
+ ViewCompat.offsetTopAndBottom(mCircleView, offset);
+ mCurrentTargetOffsetTop = mCircleView.getTop();
+ }
+
+ private void onSecondaryPointerUp(MotionEvent ev) {
+ final int pointerIndex = ev.getActionIndex();
+ final int pointerId = ev.getPointerId(pointerIndex);
+ if (pointerId == mActivePointerId) {
+ // This was our active pointer going up. Choose a new
+ // active pointer and adjust accordingly.
+ final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+ mActivePointerId = ev.getPointerId(newPointerIndex);
+ }
+ }
+
+ /**
+ * Classes that wish to be notified when the swipe gesture correctly
+ * triggers a refresh should implement this interface.
+ */
+ public interface OnRefreshListener {
+ /**
+ * Called when a swipe gesture triggers a refresh.
+ */
+ void onRefresh();
+ }
+
+ /**
+ * Classes that wish to override {@link SwipeRefreshLayout#canChildScrollUp()} method
+ * behavior should implement this interface.
+ */
+ public interface OnChildScrollUpCallback {
+ /**
+ * Callback that will be called when {@link SwipeRefreshLayout#canChildScrollUp()} method
+ * is called to allow the implementer to override its behavior.
+ *
+ * @param parent SwipeRefreshLayout that this callback is overriding.
+ * @param child The child view of SwipeRefreshLayout.
+ *
+ * @return Whether it is possible for the child view of parent layout to scroll up.
+ */
+ boolean canChildScrollUp(@NonNull SwipeRefreshLayout parent, @Nullable View child);
+ }
+}
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_01.png b/app/src/main/res/drawable-xxhdpi/refresh_01.png
new file mode 100644
index 0000000000..bb35f53a4a
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_01.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_02.png b/app/src/main/res/drawable-xxhdpi/refresh_02.png
new file mode 100644
index 0000000000..35436b9764
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_02.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_03.png b/app/src/main/res/drawable-xxhdpi/refresh_03.png
new file mode 100644
index 0000000000..6c7c158d9a
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_03.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_04.png b/app/src/main/res/drawable-xxhdpi/refresh_04.png
new file mode 100644
index 0000000000..e29157f5cf
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_04.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_05.png b/app/src/main/res/drawable-xxhdpi/refresh_05.png
new file mode 100644
index 0000000000..75cb0a3b4c
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_05.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_06.png b/app/src/main/res/drawable-xxhdpi/refresh_06.png
new file mode 100644
index 0000000000..8180d59dc8
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_06.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_07.png b/app/src/main/res/drawable-xxhdpi/refresh_07.png
new file mode 100644
index 0000000000..bc7eb20acd
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_07.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_08.png b/app/src/main/res/drawable-xxhdpi/refresh_08.png
new file mode 100644
index 0000000000..cc57cd6352
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_08.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_09.png b/app/src/main/res/drawable-xxhdpi/refresh_09.png
new file mode 100644
index 0000000000..c9b46ebb32
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_09.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_10.png b/app/src/main/res/drawable-xxhdpi/refresh_10.png
new file mode 100644
index 0000000000..30f18cd01e
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_10.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_11.png b/app/src/main/res/drawable-xxhdpi/refresh_11.png
new file mode 100644
index 0000000000..04f073d8f6
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_11.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_12.png b/app/src/main/res/drawable-xxhdpi/refresh_12.png
new file mode 100644
index 0000000000..676c0c1b4c
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_12.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_13.png b/app/src/main/res/drawable-xxhdpi/refresh_13.png
new file mode 100644
index 0000000000..5d270afc69
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_13.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_14.png b/app/src/main/res/drawable-xxhdpi/refresh_14.png
new file mode 100644
index 0000000000..506e393571
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_14.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_15.png b/app/src/main/res/drawable-xxhdpi/refresh_15.png
new file mode 100644
index 0000000000..1b1aa35327
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_15.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_16.png b/app/src/main/res/drawable-xxhdpi/refresh_16.png
new file mode 100644
index 0000000000..a6a7281e9d
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_16.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_17.png b/app/src/main/res/drawable-xxhdpi/refresh_17.png
new file mode 100644
index 0000000000..16a1feb21a
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_17.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_18.png b/app/src/main/res/drawable-xxhdpi/refresh_18.png
new file mode 100644
index 0000000000..c7e6268eb3
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_18.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_19.png b/app/src/main/res/drawable-xxhdpi/refresh_19.png
new file mode 100644
index 0000000000..aa5b6d49a1
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_19.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_20.png b/app/src/main/res/drawable-xxhdpi/refresh_20.png
new file mode 100644
index 0000000000..0be65c7287
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_20.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_21.png b/app/src/main/res/drawable-xxhdpi/refresh_21.png
new file mode 100644
index 0000000000..23fc90b2a5
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_21.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_22.png b/app/src/main/res/drawable-xxhdpi/refresh_22.png
new file mode 100644
index 0000000000..04fe193ffa
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_22.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_23.png b/app/src/main/res/drawable-xxhdpi/refresh_23.png
new file mode 100644
index 0000000000..420f9b9005
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_23.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_24.png b/app/src/main/res/drawable-xxhdpi/refresh_24.png
new file mode 100644
index 0000000000..c0d8cf7542
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_24.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_25.png b/app/src/main/res/drawable-xxhdpi/refresh_25.png
new file mode 100644
index 0000000000..4c6c740404
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_25.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_26.png b/app/src/main/res/drawable-xxhdpi/refresh_26.png
new file mode 100644
index 0000000000..781763fbad
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_26.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_27.png b/app/src/main/res/drawable-xxhdpi/refresh_27.png
new file mode 100644
index 0000000000..968ae31f4f
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_27.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_28.png b/app/src/main/res/drawable-xxhdpi/refresh_28.png
new file mode 100644
index 0000000000..0cddd7e9b5
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_28.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_29.png b/app/src/main/res/drawable-xxhdpi/refresh_29.png
new file mode 100644
index 0000000000..ca8656d0eb
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_29.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_30.png b/app/src/main/res/drawable-xxhdpi/refresh_30.png
new file mode 100644
index 0000000000..e7601fb837
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_30.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_31.png b/app/src/main/res/drawable-xxhdpi/refresh_31.png
new file mode 100644
index 0000000000..b788c666c0
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_31.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_32.png b/app/src/main/res/drawable-xxhdpi/refresh_32.png
new file mode 100644
index 0000000000..83533909da
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_32.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_33.png b/app/src/main/res/drawable-xxhdpi/refresh_33.png
new file mode 100644
index 0000000000..a6590b72aa
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_33.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_34.png b/app/src/main/res/drawable-xxhdpi/refresh_34.png
new file mode 100644
index 0000000000..a1fb79f86e
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_34.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_35.png b/app/src/main/res/drawable-xxhdpi/refresh_35.png
new file mode 100644
index 0000000000..87e99456dd
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_35.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_36.png b/app/src/main/res/drawable-xxhdpi/refresh_36.png
new file mode 100644
index 0000000000..1c0ad67dd5
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_36.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_37.png b/app/src/main/res/drawable-xxhdpi/refresh_37.png
new file mode 100644
index 0000000000..896c465f1f
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_37.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_38.png b/app/src/main/res/drawable-xxhdpi/refresh_38.png
new file mode 100644
index 0000000000..8a14b53bd1
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_38.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_39.png b/app/src/main/res/drawable-xxhdpi/refresh_39.png
new file mode 100644
index 0000000000..324ead9cf1
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_39.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_40.png b/app/src/main/res/drawable-xxhdpi/refresh_40.png
new file mode 100644
index 0000000000..04a93504bc
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_40.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_41.png b/app/src/main/res/drawable-xxhdpi/refresh_41.png
new file mode 100644
index 0000000000..1799b0be50
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_41.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_42.png b/app/src/main/res/drawable-xxhdpi/refresh_42.png
new file mode 100644
index 0000000000..6d4228c705
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_42.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_43.png b/app/src/main/res/drawable-xxhdpi/refresh_43.png
new file mode 100644
index 0000000000..d215d0df8a
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_43.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_44.png b/app/src/main/res/drawable-xxhdpi/refresh_44.png
new file mode 100644
index 0000000000..bfbd84c8c8
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_44.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_45.png b/app/src/main/res/drawable-xxhdpi/refresh_45.png
new file mode 100644
index 0000000000..6466e5d0cc
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_45.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_46.png b/app/src/main/res/drawable-xxhdpi/refresh_46.png
new file mode 100644
index 0000000000..3b26b64056
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_46.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_47.png b/app/src/main/res/drawable-xxhdpi/refresh_47.png
new file mode 100644
index 0000000000..9fbf601209
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_47.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_48.png b/app/src/main/res/drawable-xxhdpi/refresh_48.png
new file mode 100644
index 0000000000..1a6db226e3
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_48.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_49.png b/app/src/main/res/drawable-xxhdpi/refresh_49.png
new file mode 100644
index 0000000000..4af9e704b6
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_49.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_50.png b/app/src/main/res/drawable-xxhdpi/refresh_50.png
new file mode 100644
index 0000000000..a81bcc9670
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_50.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_51.png b/app/src/main/res/drawable-xxhdpi/refresh_51.png
new file mode 100644
index 0000000000..6d4574db6f
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_51.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_52.png b/app/src/main/res/drawable-xxhdpi/refresh_52.png
new file mode 100644
index 0000000000..3b9cf1bcc5
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_52.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_53.png b/app/src/main/res/drawable-xxhdpi/refresh_53.png
new file mode 100644
index 0000000000..0dcc69c35a
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_53.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_54.png b/app/src/main/res/drawable-xxhdpi/refresh_54.png
new file mode 100644
index 0000000000..f4594163ac
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_54.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_55.png b/app/src/main/res/drawable-xxhdpi/refresh_55.png
new file mode 100644
index 0000000000..2ab465961b
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_55.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_56.png b/app/src/main/res/drawable-xxhdpi/refresh_56.png
new file mode 100644
index 0000000000..01a796dd7e
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_56.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_57.png b/app/src/main/res/drawable-xxhdpi/refresh_57.png
new file mode 100644
index 0000000000..ad44ad3e68
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_57.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_58.png b/app/src/main/res/drawable-xxhdpi/refresh_58.png
new file mode 100644
index 0000000000..0a0d727f31
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_58.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_59.png b/app/src/main/res/drawable-xxhdpi/refresh_59.png
new file mode 100644
index 0000000000..8c04df1ce6
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_59.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_60.png b/app/src/main/res/drawable-xxhdpi/refresh_60.png
new file mode 100644
index 0000000000..c69dfedd8b
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_60.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_61.png b/app/src/main/res/drawable-xxhdpi/refresh_61.png
new file mode 100644
index 0000000000..4b7a71566d
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_61.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_62.png b/app/src/main/res/drawable-xxhdpi/refresh_62.png
new file mode 100644
index 0000000000..8263639170
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_62.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_63.png b/app/src/main/res/drawable-xxhdpi/refresh_63.png
new file mode 100644
index 0000000000..0377371b78
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_63.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_64.png b/app/src/main/res/drawable-xxhdpi/refresh_64.png
new file mode 100644
index 0000000000..96129d6dd3
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_64.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_65.png b/app/src/main/res/drawable-xxhdpi/refresh_65.png
new file mode 100644
index 0000000000..e5eb919457
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_65.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_66.png b/app/src/main/res/drawable-xxhdpi/refresh_66.png
new file mode 100644
index 0000000000..45d63d98e9
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_66.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_67.png b/app/src/main/res/drawable-xxhdpi/refresh_67.png
new file mode 100644
index 0000000000..b14741f5e1
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_67.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_68.png b/app/src/main/res/drawable-xxhdpi/refresh_68.png
new file mode 100644
index 0000000000..b0a2acc065
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_68.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_69.png b/app/src/main/res/drawable-xxhdpi/refresh_69.png
new file mode 100644
index 0000000000..d68f7680f8
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_69.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_70.png b/app/src/main/res/drawable-xxhdpi/refresh_70.png
new file mode 100644
index 0000000000..6936bed98a
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_70.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_71.png b/app/src/main/res/drawable-xxhdpi/refresh_71.png
new file mode 100644
index 0000000000..f3778902b6
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_71.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_72.png b/app/src/main/res/drawable-xxhdpi/refresh_72.png
new file mode 100644
index 0000000000..b441eb414a
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_72.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_73.png b/app/src/main/res/drawable-xxhdpi/refresh_73.png
new file mode 100644
index 0000000000..7d024b6762
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_73.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_74.png b/app/src/main/res/drawable-xxhdpi/refresh_74.png
new file mode 100644
index 0000000000..a427c61acf
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_74.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_75.png b/app/src/main/res/drawable-xxhdpi/refresh_75.png
new file mode 100644
index 0000000000..e9b49d82d3
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_75.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_76.png b/app/src/main/res/drawable-xxhdpi/refresh_76.png
new file mode 100644
index 0000000000..e22832a5bf
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_76.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_77.png b/app/src/main/res/drawable-xxhdpi/refresh_77.png
new file mode 100644
index 0000000000..8eae351182
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_77.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_78.png b/app/src/main/res/drawable-xxhdpi/refresh_78.png
new file mode 100644
index 0000000000..07953e15d6
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_78.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_79.png b/app/src/main/res/drawable-xxhdpi/refresh_79.png
new file mode 100644
index 0000000000..a18cdc0d3e
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_79.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_80.png b/app/src/main/res/drawable-xxhdpi/refresh_80.png
new file mode 100644
index 0000000000..1b181cf50a
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_80.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_81.png b/app/src/main/res/drawable-xxhdpi/refresh_81.png
new file mode 100644
index 0000000000..be1417f489
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_81.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_82.png b/app/src/main/res/drawable-xxhdpi/refresh_82.png
new file mode 100644
index 0000000000..edd1604964
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_82.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_83.png b/app/src/main/res/drawable-xxhdpi/refresh_83.png
new file mode 100644
index 0000000000..e15d26119e
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_83.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_84.png b/app/src/main/res/drawable-xxhdpi/refresh_84.png
new file mode 100644
index 0000000000..55dcd5113c
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_84.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_85.png b/app/src/main/res/drawable-xxhdpi/refresh_85.png
new file mode 100644
index 0000000000..ac144bf33b
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_85.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_86.png b/app/src/main/res/drawable-xxhdpi/refresh_86.png
new file mode 100644
index 0000000000..33b8751dd0
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_86.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_87.png b/app/src/main/res/drawable-xxhdpi/refresh_87.png
new file mode 100644
index 0000000000..bf0fdb0884
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_87.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_88.png b/app/src/main/res/drawable-xxhdpi/refresh_88.png
new file mode 100644
index 0000000000..bb35f53a4a
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_88.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_89.png b/app/src/main/res/drawable-xxhdpi/refresh_89.png
new file mode 100644
index 0000000000..bb35f53a4a
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_89.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_90.png b/app/src/main/res/drawable-xxhdpi/refresh_90.png
new file mode 100644
index 0000000000..bb35f53a4a
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_90.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_91.png b/app/src/main/res/drawable-xxhdpi/refresh_91.png
new file mode 100644
index 0000000000..bb35f53a4a
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_91.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_92.png b/app/src/main/res/drawable-xxhdpi/refresh_92.png
new file mode 100644
index 0000000000..bb35f53a4a
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_92.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_93.png b/app/src/main/res/drawable-xxhdpi/refresh_93.png
new file mode 100644
index 0000000000..bb35f53a4a
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_93.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_94.png b/app/src/main/res/drawable-xxhdpi/refresh_94.png
new file mode 100644
index 0000000000..bb35f53a4a
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_94.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_95.png b/app/src/main/res/drawable-xxhdpi/refresh_95.png
new file mode 100644
index 0000000000..bb35f53a4a
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_95.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_96.png b/app/src/main/res/drawable-xxhdpi/refresh_96.png
new file mode 100644
index 0000000000..bb35f53a4a
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_96.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_97.png b/app/src/main/res/drawable-xxhdpi/refresh_97.png
new file mode 100644
index 0000000000..bb35f53a4a
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_97.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_98.png b/app/src/main/res/drawable-xxhdpi/refresh_98.png
new file mode 100644
index 0000000000..bb35f53a4a
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_98.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/refresh_icon.png b/app/src/main/res/drawable-xxhdpi/refresh_icon.png
new file mode 100644
index 0000000000..e9b49d82d3
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/refresh_icon.png differ