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