From 0086d5dc80dd95b1e8e4dcfe2d3d3090d260bb88 Mon Sep 17 00:00:00 2001 From: steckbrief Date: Fri, 4 Dec 2015 23:18:36 +0100 Subject: Implements FS#83: Reload from last received message --- libs/SwipyRefreshLayout/.gitignore | 1 + libs/SwipyRefreshLayout/build.gradle | 24 + libs/SwipyRefreshLayout/proguard-rules.pro | 17 + .../src/main/AndroidManifest.xml | 8 + .../library/CircleImageView.java | 149 +++ .../library/MaterialProgressDrawable.java | 722 ++++++++++++ .../library/SwipyRefreshLayout.java | 1152 ++++++++++++++++++++ .../library/SwipyRefreshLayoutDirection.java | 27 + .../src/main/res/values/attrs.xml | 12 + 9 files changed, 2112 insertions(+) create mode 100644 libs/SwipyRefreshLayout/.gitignore create mode 100644 libs/SwipyRefreshLayout/build.gradle create mode 100644 libs/SwipyRefreshLayout/proguard-rules.pro create mode 100644 libs/SwipyRefreshLayout/src/main/AndroidManifest.xml create mode 100644 libs/SwipyRefreshLayout/src/main/java/com/orangegangsters/github/swipyrefreshlayout/library/CircleImageView.java create mode 100644 libs/SwipyRefreshLayout/src/main/java/com/orangegangsters/github/swipyrefreshlayout/library/MaterialProgressDrawable.java create mode 100644 libs/SwipyRefreshLayout/src/main/java/com/orangegangsters/github/swipyrefreshlayout/library/SwipyRefreshLayout.java create mode 100644 libs/SwipyRefreshLayout/src/main/java/com/orangegangsters/github/swipyrefreshlayout/library/SwipyRefreshLayoutDirection.java create mode 100644 libs/SwipyRefreshLayout/src/main/res/values/attrs.xml (limited to 'libs') diff --git a/libs/SwipyRefreshLayout/.gitignore b/libs/SwipyRefreshLayout/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/libs/SwipyRefreshLayout/.gitignore @@ -0,0 +1 @@ +/build diff --git a/libs/SwipyRefreshLayout/build.gradle b/libs/SwipyRefreshLayout/build.gradle new file mode 100644 index 00000000..68a75ec9 --- /dev/null +++ b/libs/SwipyRefreshLayout/build.gradle @@ -0,0 +1,24 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 22 + buildToolsVersion "22.0.1" + + defaultConfig { + minSdkVersion 9 + targetSdkVersion 22 + versionCode 1 + versionName "1.2.1" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:appcompat-v7:22.0.+' +} diff --git a/libs/SwipyRefreshLayout/proguard-rules.pro b/libs/SwipyRefreshLayout/proguard-rules.pro new file mode 100644 index 00000000..7c07ec21 --- /dev/null +++ b/libs/SwipyRefreshLayout/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/oliviergoutay/Documents/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/libs/SwipyRefreshLayout/src/main/AndroidManifest.xml b/libs/SwipyRefreshLayout/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a2916954 --- /dev/null +++ b/libs/SwipyRefreshLayout/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/libs/SwipyRefreshLayout/src/main/java/com/orangegangsters/github/swipyrefreshlayout/library/CircleImageView.java b/libs/SwipyRefreshLayout/src/main/java/com/orangegangsters/github/swipyrefreshlayout/library/CircleImageView.java new file mode 100644 index 00000000..2609655b --- /dev/null +++ b/libs/SwipyRefreshLayout/src/main/java/com/orangegangsters/github/swipyrefreshlayout/library/CircleImageView.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2014 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 com.orangegangsters.github.swipyrefreshlayout.library; + +import android.content.Context; +import android.content.res.Resources; +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.support.v4.view.ViewCompat; +import android.view.animation.Animation; +import android.widget.ImageView; + +/** + * Private class created to work around issues with AnimationListeners being + * called before the animation is actually complete and support shadows on older + * platforms. + * + * @hide + */ +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; + private int mShadowRadius; + + public CircleImageView(Context context, int color, final float radius) { + super(context); + final float density = getContext().getResources().getDisplayMetrics().density; + final int diameter = (int) (radius * density * 2); + 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, diameter); + circle = new ShapeDrawable(oval); + ViewCompat.setLayerType(this, ViewCompat.LAYER_TYPE_SOFTWARE, circle.getPaint()); + circle.getPaint().setShadowLayer(mShadowRadius, shadowXOffset, shadowYOffset, + KEY_SHADOW_COLOR); + final int padding = (int) mShadowRadius; + // set padding so the inner image sits correctly within the shadow. + setPadding(padding, padding, padding, padding); + } + circle.getPaint().setColor(color); + setBackgroundDrawable(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. + */ + public void setBackgroundColor(int colorRes) { + if (getBackground() instanceof ShapeDrawable) { + final Resources res = getResources(); + ((ShapeDrawable) getBackground()).getPaint().setColor(res.getColor(colorRes)); + } + } + + private class OvalShadow extends OvalShape { + private RadialGradient mRadialGradient; + private int mShadowRadius; + private Paint mShadowPaint; + private int mCircleDiameter; + + public OvalShadow(int shadowRadius, int circleDiameter) { + super(); + mShadowPaint = new Paint(); + mShadowRadius = shadowRadius; + mCircleDiameter = circleDiameter; + mRadialGradient = new RadialGradient(mCircleDiameter / 2, mCircleDiameter / 2, + mShadowRadius, new int[] { + FILL_SHADOW_COLOR, Color.TRANSPARENT + }, null, Shader.TileMode.CLAMP); + mShadowPaint.setShader(mRadialGradient); + } + + @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, (mCircleDiameter / 2 + mShadowRadius), + mShadowPaint); + canvas.drawCircle(viewWidth / 2, viewHeight / 2, (mCircleDiameter / 2), paint); + } + } +} diff --git a/libs/SwipyRefreshLayout/src/main/java/com/orangegangsters/github/swipyrefreshlayout/library/MaterialProgressDrawable.java b/libs/SwipyRefreshLayout/src/main/java/com/orangegangsters/github/swipyrefreshlayout/library/MaterialProgressDrawable.java new file mode 100644 index 00000000..c7eef8c1 --- /dev/null +++ b/libs/SwipyRefreshLayout/src/main/java/com/orangegangsters/github/swipyrefreshlayout/library/MaterialProgressDrawable.java @@ -0,0 +1,722 @@ +/* + * Copyright (C) 2014 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 com.orangegangsters.github.swipyrefreshlayout.library; + +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.Interpolator; +import android.view.animation.Animation; +import android.view.animation.LinearInterpolator; +import android.view.animation.Transformation; +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.Drawable; +import android.graphics.drawable.Animatable; +import android.support.annotation.IntDef; +import android.support.annotation.NonNull; +import android.util.DisplayMetrics; +import android.view.View; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; + +/** + * Fancy progress indicator for Material theme. + * + * @hide + */ +class MaterialProgressDrawable extends Drawable implements Animatable { + private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); + private static final Interpolator END_CURVE_INTERPOLATOR = new EndCurveInterpolator(); + private static final Interpolator START_CURVE_INTERPOLATOR = new StartCurveInterpolator(); + private static final Interpolator EASE_INTERPOLATOR = new AccelerateDecelerateInterpolator(); + + @Retention(RetentionPolicy.CLASS) + @IntDef({LARGE, DEFAULT}) + public @interface ProgressDrawableSize {} + // Maps to ProgressBar.Large style + static final int LARGE = 0; + // Maps to ProgressBar default style + static final int DEFAULT = 1; + + // Maps to ProgressBar default style + private static final int CIRCLE_DIAMETER = 40; + private static final float CENTER_RADIUS = 8.75f; //should add up to 10 when + stroke_width + private static final float STROKE_WIDTH = 2.5f; + + // Maps to ProgressBar.Large style + private static final int CIRCLE_DIAMETER_LARGE = 56; + private static final float CENTER_RADIUS_LARGE = 12.5f; + private static final float STROKE_WIDTH_LARGE = 3f; + + private final int[] COLORS = new int[] { + Color.BLACK + }; + + /** The duration of a single progress spin in milliseconds. */ + private static final int ANIMATION_DURATION = 1000 * 80 / 60; + + /** The number of points in the progress "star". */ + private static final float NUM_POINTS = 5f; + /** The list of animators operating on this drawable. */ + private final ArrayList mAnimators = new ArrayList(); + + /** The indicator ring, used to manage animation state. */ + private final Ring mRing; + + /** Canvas rotation in degrees. */ + private float mRotation; + + /** Layout info for the arrowhead in dp */ + private static final int ARROW_WIDTH = 10; + private static final int ARROW_HEIGHT = 5; + private static final float ARROW_OFFSET_ANGLE = 5; + + /** Layout info for the arrowhead for the large spinner in dp */ + private static final int ARROW_WIDTH_LARGE = 12; + private static final int ARROW_HEIGHT_LARGE = 6; + private static final float MAX_PROGRESS_ARC = .8f; + + private Resources mResources; + private View mParent; + private Animation mAnimation; + private float mRotationCount; + private double mWidth; + private double mHeight; + private Animation mFinishAnimation; + + public MaterialProgressDrawable(Context context, View parent) { + mParent = parent; + mResources = context.getResources(); + + mRing = new Ring(mCallback); + mRing.setColors(COLORS); + + updateSizes(DEFAULT); + setupAnimators(); + } + + private void setSizeParameters(double progressCircleWidth, double progressCircleHeight, + double centerRadius, double strokeWidth, float arrowWidth, float arrowHeight) { + final Ring ring = mRing; + final DisplayMetrics metrics = mResources.getDisplayMetrics(); + final float screenDensity = metrics.density; + + mWidth = progressCircleWidth * screenDensity; + mHeight = progressCircleHeight * screenDensity; + ring.setStrokeWidth((float) strokeWidth * screenDensity); + ring.setCenterRadius(centerRadius * screenDensity); + ring.setColorIndex(0); + ring.setArrowDimensions(arrowWidth * screenDensity, arrowHeight * screenDensity); + ring.setInsets((int) mWidth, (int) mHeight); + } + + /** + * Set the overall size for the progress spinner. This updates the radius + * and stroke width of the ring. + * + * @param size One of {@link com.orangegangsters.github.swiperefreshlayout.MaterialProgressDrawable.LARGE} or + * {@link com.orangegangsters.github.swiperefreshlayout.MaterialProgressDrawable.DEFAULT} + */ + public void updateSizes(@ProgressDrawableSize int size) { + if (size == LARGE) { + setSizeParameters(CIRCLE_DIAMETER_LARGE, CIRCLE_DIAMETER_LARGE, CENTER_RADIUS_LARGE, + STROKE_WIDTH_LARGE, ARROW_WIDTH_LARGE, ARROW_HEIGHT_LARGE); + } else { + setSizeParameters(CIRCLE_DIAMETER, CIRCLE_DIAMETER, CENTER_RADIUS, STROKE_WIDTH, + ARROW_WIDTH, ARROW_HEIGHT); + } + } + + /** + * @param show Set to true to display the arrowhead on the progress spinner. + */ + public void showArrow(boolean show) { + mRing.setShowArrow(show); + } + + /** + * @param scale Set the scale of the arrowhead for the spinner. + */ + public void setArrowScale(float scale) { + mRing.setArrowScale(scale); + } + + /** + * Set the start and end trim for the progress spinner arc. + * + * @param startAngle start angle + * @param endAngle end angle + */ + public void setStartEndTrim(float startAngle, float endAngle) { + mRing.setStartTrim(startAngle); + mRing.setEndTrim(endAngle); + } + + /** + * Set the amount of rotation to apply to the progress spinner. + * + * @param rotation Rotation is from [0..1] + */ + public void setProgressRotation(float rotation) { + mRing.setRotation(rotation); + } + + /** + * Update the background color of the circle image view. + */ + public void setBackgroundColor(int color) { + mRing.setBackgroundColor(color); + } + + /** + * Set the colors 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 colors + */ + public void setColorSchemeColors(int... colors) { + mRing.setColors(colors); + mRing.setColorIndex(0); + } + + @Override + public int getIntrinsicHeight() { + return (int) mHeight; + } + + @Override + public int getIntrinsicWidth() { + return (int) mWidth; + } + + @Override + public void draw(Canvas c) { + final Rect bounds = getBounds(); + final int saveCount = c.save(); + c.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY()); + mRing.draw(c, bounds); + c.restoreToCount(saveCount); + } + + @Override + public void setAlpha(int alpha) { + mRing.setAlpha(alpha); + } + + public int getAlpha() { + return mRing.getAlpha(); + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + mRing.setColorFilter(colorFilter); + } + + @SuppressWarnings("unused") + void setRotation(float rotation) { + mRotation = rotation; + invalidateSelf(); + } + + @SuppressWarnings("unused") + private float getRotation() { + return mRotation; + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public boolean isRunning() { + final ArrayList animators = mAnimators; + final int N = animators.size(); + for (int i = 0; i < N; i++) { + final Animation animator = animators.get(i); + if (animator.hasStarted() && !animator.hasEnded()) { + return true; + } + } + return false; + } + + @Override + public void start() { + mAnimation.reset(); + mRing.storeOriginals(); + // Already showing some part of the ring + if (mRing.getEndTrim() != mRing.getStartTrim()) { + mParent.startAnimation(mFinishAnimation); + } else { + mRing.setColorIndex(0); + mRing.resetOriginals(); + mParent.startAnimation(mAnimation); + } + } + + @Override + public void stop() { + mParent.clearAnimation(); + setRotation(0); + mRing.setShowArrow(false); + mRing.setColorIndex(0); + mRing.resetOriginals(); + } + + private void setupAnimators() { + final Ring ring = mRing; + final Animation finishRingAnimation = new Animation() { + public void applyTransformation(float interpolatedTime, Transformation t) { + // shrink back down and complete a full rotation before starting other circles + // Rotation goes between [0..1]. + float targetRotation = (float) (Math.floor(ring.getStartingRotation() + / MAX_PROGRESS_ARC) + 1f); + final float startTrim = ring.getStartingStartTrim() + + (ring.getStartingEndTrim() - ring.getStartingStartTrim()) + * interpolatedTime; + ring.setStartTrim(startTrim); + final float rotation = ring.getStartingRotation() + + ((targetRotation - ring.getStartingRotation()) * interpolatedTime); + ring.setRotation(rotation); + ring.setArrowScale(1 - interpolatedTime); + } + }; + finishRingAnimation.setInterpolator(EASE_INTERPOLATOR); + finishRingAnimation.setDuration(ANIMATION_DURATION/2); + finishRingAnimation.setAnimationListener(new Animation.AnimationListener() { + + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + ring.goToNextColor(); + ring.storeOriginals(); + ring.setShowArrow(false); + mParent.startAnimation(mAnimation); + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + }); + final Animation animation = new Animation() { + @Override + public void applyTransformation(float interpolatedTime, Transformation t) { + // The minProgressArc is calculated from 0 to create an angle that + // matches the stroke width. + final float minProgressArc = (float) Math.toRadians(ring.getStrokeWidth() + / (2 * Math.PI * ring.getCenterRadius())); + final float startingEndTrim = ring.getStartingEndTrim(); + final float startingTrim = ring.getStartingStartTrim(); + final float startingRotation = ring.getStartingRotation(); + + // Offset the minProgressArc to where the endTrim is located. + final float minArc = MAX_PROGRESS_ARC - minProgressArc; + final float endTrim = startingEndTrim + + (minArc * START_CURVE_INTERPOLATOR.getInterpolation(interpolatedTime)); + ring.setEndTrim(endTrim); + + final float startTrim = startingTrim + + (MAX_PROGRESS_ARC * END_CURVE_INTERPOLATOR + .getInterpolation(interpolatedTime)); + ring.setStartTrim(startTrim); + + final float rotation = startingRotation + (0.25f * interpolatedTime); + ring.setRotation(rotation); + + float groupRotation = ((720.0f / NUM_POINTS) * interpolatedTime) + + (720.0f * (mRotationCount / NUM_POINTS)); + setRotation(groupRotation); + } + }; + animation.setRepeatCount(Animation.INFINITE); + animation.setRepeatMode(Animation.RESTART); + animation.setInterpolator(LINEAR_INTERPOLATOR); + animation.setDuration(ANIMATION_DURATION); + animation.setAnimationListener(new Animation.AnimationListener() { + + @Override + public void onAnimationStart(Animation animation) { + mRotationCount = 0; + } + + @Override + public void onAnimationEnd(Animation animation) { + // do nothing + } + + @Override + public void onAnimationRepeat(Animation animation) { + ring.storeOriginals(); + ring.goToNextColor(); + ring.setStartTrim(ring.getEndTrim()); + mRotationCount = (mRotationCount + 1) % (NUM_POINTS); + } + }); + mFinishAnimation = finishRingAnimation; + mAnimation = animation; + } + + private final Callback mCallback = new Callback() { + @Override + public void invalidateDrawable(Drawable d) { + invalidateSelf(); + } + + @Override + public void scheduleDrawable(Drawable d, Runnable what, long when) { + scheduleSelf(what, when); + } + + @Override + public void unscheduleDrawable(Drawable d, Runnable what) { + unscheduleSelf(what); + } + }; + + private static class Ring { + private final RectF mTempBounds = new RectF(); + private final Paint mPaint = new Paint(); + private final Paint mArrowPaint = new Paint(); + + private final Callback mCallback; + + private float mStartTrim = 0.0f; + private float mEndTrim = 0.0f; + private float mRotation = 0.0f; + private float mStrokeWidth = 5.0f; + private float mStrokeInset = 2.5f; + + private 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. + private int mColorIndex; + private float mStartingStartTrim; + private float mStartingEndTrim; + private float mStartingRotation; + private boolean mShowArrow; + private Path mArrow; + private float mArrowScale; + private double mRingCenterRadius; + private int mArrowWidth; + private int mArrowHeight; + private int mAlpha; + private final Paint mCirclePaint = new Paint(); + private int mBackgroundColor; + + public Ring(Callback callback) { + mCallback = callback; + + mPaint.setStrokeCap(Paint.Cap.SQUARE); + mPaint.setAntiAlias(true); + mPaint.setStyle(Style.STROKE); + + mArrowPaint.setStyle(Style.FILL); + mArrowPaint.setAntiAlias(true); + } + + public void setBackgroundColor(int color) { + mBackgroundColor = color; + } + + /** + * Set the dimensions of the arrowhead. + * + * @param width Width of the hypotenuse of the arrow head + * @param height Height of the arrow point + */ + public void setArrowDimensions(float width, float height) { + mArrowWidth = (int) width; + mArrowHeight = (int) height; + } + + /** + * Draw the progress spinner + */ + public void draw(Canvas c, Rect bounds) { + final RectF arcBounds = mTempBounds; + arcBounds.set(bounds); + arcBounds.inset(mStrokeInset, mStrokeInset); + + final float startAngle = (mStartTrim + mRotation) * 360; + final float endAngle = (mEndTrim + mRotation) * 360; + float sweepAngle = endAngle - startAngle; + + mPaint.setColor(mColors[mColorIndex]); + c.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint); + + drawTriangle(c, startAngle, sweepAngle, bounds); + + if (mAlpha < 255) { + mCirclePaint.setColor(mBackgroundColor); + mCirclePaint.setAlpha(255 - mAlpha); + c.drawCircle(bounds.exactCenterX(), bounds.exactCenterY(), bounds.width() / 2, + mCirclePaint); + } + } + + private void drawTriangle(Canvas c, float startAngle, float sweepAngle, Rect bounds) { + if (mShowArrow) { + if (mArrow == null) { + mArrow = new Path(); + mArrow.setFillType(Path.FillType.EVEN_ODD); + } else { + mArrow.reset(); + } + + // Adjust the position of the triangle so that it is inset as + // much as the arc, but also centered on the arc. + float inset = (int) mStrokeInset / 2 * mArrowScale; + float x = (float) (mRingCenterRadius * Math.cos(0) + bounds.exactCenterX()); + float y = (float) (mRingCenterRadius * Math.sin(0) + bounds.exactCenterY()); + + // 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(x - inset, y); + mArrow.close(); + // draw a triangle + mArrowPaint.setColor(mColors[mColorIndex]); + c.rotate(startAngle + sweepAngle - ARROW_OFFSET_ANGLE, bounds.exactCenterX(), + bounds.exactCenterY()); + c.drawPath(mArrow, mArrowPaint); + } + } + + /** + * Set the colors the progress spinner alternates between. + * + * @param colors Array of integers describing the colors. Must be non-null. + */ + public void setColors(@NonNull int[] colors) { + mColors = colors; + // if colors are reset, make sure to reset the color index as well + setColorIndex(0); + } + + /** + * @param index Index into the color array of the color to display in + * the progress spinner. + */ + public void setColorIndex(int index) { + mColorIndex = index; + } + + /** + * Proceed to the next available ring color. This will automatically + * wrap back to the beginning of colors. + */ + public void goToNextColor() { + mColorIndex = (mColorIndex + 1) % (mColors.length); + } + + public void setColorFilter(ColorFilter filter) { + mPaint.setColorFilter(filter); + invalidateSelf(); + } + + /** + * @param alpha Set the alpha of the progress spinner and associated arrowhead. + */ + public void setAlpha(int alpha) { + mAlpha = alpha; + } + + /** + * @return Current alpha of the progress spinner and arrowhead. + */ + public int getAlpha() { + return mAlpha; + } + + /** + * @param strokeWidth Set the stroke width of the progress spinner in pixels. + */ + public void setStrokeWidth(float strokeWidth) { + mStrokeWidth = strokeWidth; + mPaint.setStrokeWidth(strokeWidth); + invalidateSelf(); + } + + @SuppressWarnings("unused") + public float getStrokeWidth() { + return mStrokeWidth; + } + + @SuppressWarnings("unused") + public void setStartTrim(float startTrim) { + mStartTrim = startTrim; + invalidateSelf(); + } + + @SuppressWarnings("unused") + public float getStartTrim() { + return mStartTrim; + } + + public float getStartingStartTrim() { + return mStartingStartTrim; + } + + public float getStartingEndTrim() { + return mStartingEndTrim; + } + + @SuppressWarnings("unused") + public void setEndTrim(float endTrim) { + mEndTrim = endTrim; + invalidateSelf(); + } + + @SuppressWarnings("unused") + public float getEndTrim() { + return mEndTrim; + } + + @SuppressWarnings("unused") + public void setRotation(float rotation) { + mRotation = rotation; + invalidateSelf(); + } + + @SuppressWarnings("unused") + public float getRotation() { + return mRotation; + } + + public void setInsets(int width, int height) { + final float minEdge = (float) Math.min(width, height); + float insets; + if (mRingCenterRadius <= 0 || minEdge < 0) { + insets = (float) Math.ceil(mStrokeWidth / 2.0f); + } else { + insets = (float) (minEdge / 2.0f - mRingCenterRadius); + } + mStrokeInset = insets; + } + + @SuppressWarnings("unused") + public float getInsets() { + return mStrokeInset; + } + + /** + * @param centerRadius Inner radius in px of the circle the progress + * spinner arc traces. + */ + public void setCenterRadius(double centerRadius) { + mRingCenterRadius = centerRadius; + } + + public double getCenterRadius() { + return mRingCenterRadius; + } + + /** + * @param show Set to true to show the arrow head on the progress spinner. + */ + public void setShowArrow(boolean show) { + if (mShowArrow != show) { + mShowArrow = show; + invalidateSelf(); + } + } + + /** + * @param scale Set the scale of the arrowhead for the spinner. + */ + public void setArrowScale(float scale) { + if (scale != mArrowScale) { + mArrowScale = scale; + invalidateSelf(); + } + } + + /** + * @return The amount the progress spinner is currently rotated, between [0..1]. + */ + public float getStartingRotation() { + return mStartingRotation; + } + + /** + * If the start / end trim are offset to begin with, store them so that + * animation starts from that offset. + */ + public void storeOriginals() { + mStartingStartTrim = mStartTrim; + mStartingEndTrim = mEndTrim; + mStartingRotation = mRotation; + } + + /** + * Reset the progress spinner to default rotation, start and end angles. + */ + public void resetOriginals() { + mStartingStartTrim = 0; + mStartingEndTrim = 0; + mStartingRotation = 0; + setStartTrim(0); + setEndTrim(0); + setRotation(0); + } + + private void invalidateSelf() { + mCallback.invalidateDrawable(null); + } + } + + /** + * Squishes the interpolation curve into the second half of the animation. + */ + private static class EndCurveInterpolator extends AccelerateDecelerateInterpolator { + @Override + public float getInterpolation(float input) { + return super.getInterpolation(Math.max(0, (input - 0.5f) * 2.0f)); + } + } + + /** + * Squishes the interpolation curve into the first half of the animation. + */ + private static class StartCurveInterpolator extends AccelerateDecelerateInterpolator { + @Override + public float getInterpolation(float input) { + return super.getInterpolation(Math.min(1, input * 2.0f)); + } + } +} diff --git a/libs/SwipyRefreshLayout/src/main/java/com/orangegangsters/github/swipyrefreshlayout/library/SwipyRefreshLayout.java b/libs/SwipyRefreshLayout/src/main/java/com/orangegangsters/github/swipyrefreshlayout/library/SwipyRefreshLayout.java new file mode 100644 index 00000000..f193a0e1 --- /dev/null +++ b/libs/SwipyRefreshLayout/src/main/java/com/orangegangsters/github/swipyrefreshlayout/library/SwipyRefreshLayout.java @@ -0,0 +1,1152 @@ +/* + * Copyright (C) 2013 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 com.orangegangsters.github.swipyrefreshlayout.library; + + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.ViewCompat; +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; + +/** + * 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 SwipyRefreshLayout extends ViewGroup { + + public static final String TAG = "SwipyRefreshLayout"; + + private static final float MAX_SWIPE_DISTANCE_FACTOR = .6f; + private static final int REFRESH_TRIGGER_DISTANCE = 120; + + // Maps to ProgressBar.Large style + public static final int LARGE = MaterialProgressDrawable.LARGE; + // Maps to ProgressBar default style + public static final int DEFAULT = MaterialProgressDrawable.DEFAULT; + + private static final String LOG_TAG = SwipyRefreshLayout.class.getSimpleName(); + + private static final int MAX_ALPHA = 255; + private static final int STARTING_PROGRESS_ALPHA = (int) (.3f * MAX_ALPHA); + + private static final int CIRCLE_DIAMETER = 40; + private static final int CIRCLE_DIAMETER_LARGE = 56; + + 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 + private SwipyRefreshLayoutDirection mDirection; + private boolean mBothDirection; + private OnRefreshListener mListener; + private boolean mRefreshing = false; + private int mTouchSlop; + private float mTotalDragDistance = -1; + private int mMediumAnimationDuration; + private int mCurrentTargetOffsetTop; + // Whether or not the starting offset has been determined. + private boolean mOriginalOffsetCalculated = false; + + private float mInitialMotionY; + private float mInitialDownY; + private boolean mIsBeingDragged; + private int mActivePointerId = INVALID_POINTER; + // Whether this item is scaled up rather than clipped + private 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 + }; + + private CircleImageView mCircleView; + private int mCircleViewIndex = -1; + + protected int mFrom; + + private float mStartingScale; + + protected int mOriginalOffsetTop; + + private MaterialProgressDrawable mProgress; + + private Animation mScaleAnimation; + + private Animation mScaleDownAnimation; + + private Animation mAlphaStartAnimation; + + private Animation mAlphaMaxAnimation; + + private Animation mScaleDownToStartAnimation; + + private float mSpinnerFinalOffset; + + private boolean mNotify; + + private int mCircleWidth; + + private int mCircleHeight; + + // Whether the client has set a custom starting position; + private boolean mUsingCustomStart; + + 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(mDirection); + } + } + } else { + mProgress.stop(); + mCircleView.setVisibility(View.GONE); + setColorViewAlpha(MAX_ALPHA); + // Return the circle to its start position + if (mScale) { + setAnimationProgress(0 /* animation complete and view is hidden */); + } else { + setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCurrentTargetOffsetTop, + true /* requires update */); + } + } + mCurrentTargetOffsetTop = mCircleView.getTop(); + } + }; + + 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. + * + * @param scale Set to true if there is no view at a higher z-order than + * where the progress spinner is set to appear. + * @param start The offset in pixels from the top of this view at which the + * progress spinner should appear. + * @param end The offset in pixels from the top of this view at which the + * progress spinner should come to rest after a successful swipe + * gesture. + */ + /* + public void setProgressViewOffset(boolean scale, int start, int end) { + mScale = scale; + mCircleView.setVisibility(View.GONE); + mOriginalOffsetTop = mCurrentTargetOffsetTop = start; + mSpinnerFinalOffset = end; + mUsingCustomStart = true; + mCircleView.invalidate(); + }*/ + + /** + * The refresh indicator resting position is always positioned near the top + * of the refreshing content. This position is a consistent location, but + * can be adjusted in either direction based on whether or not there is a + * toolbar or actionbar present. + * + * @param scale Set to true if there is no view at a higher z-order than + * where the progress spinner is set to appear. + * @param end The offset in pixels from the top of this view at which the + * progress spinner should come to rest after a successful swipe + * gesture. + */ + /* + public void setProgressViewEndTarget(boolean scale, int end) { + mSpinnerFinalOffset = end; + mScale = scale; + mCircleView.invalidate(); + }*/ + + /** + * One of DEFAULT, or LARGE. + */ + public void setSize(int size) { + if (size != MaterialProgressDrawable.LARGE && size != MaterialProgressDrawable.DEFAULT) { + return; + } + final DisplayMetrics metrics = getResources().getDisplayMetrics(); + if (size == MaterialProgressDrawable.LARGE) { + mCircleHeight = mCircleWidth = (int) (CIRCLE_DIAMETER_LARGE * metrics.density); + } else { + mCircleHeight = mCircleWidth = (int) (CIRCLE_DIAMETER * metrics.density); + } + // force the bounds of the progress circle inside the circle view to + // update by setting it to null before updating its size and then + // re-setting it + mCircleView.setImageDrawable(null); + mProgress.updateSizes(size); + mCircleView.setImageDrawable(mProgress); + } + + /** + * Simple constructor to use when creating a SwipeRefreshLayout from code. + * + * @param context + */ + public SwipyRefreshLayout(Context context) { + this(context, null); + } + + /** + * Constructor that is called when inflating SwipeRefreshLayout from XML. + * + * @param context + * @param attrs + */ + public SwipyRefreshLayout(Context context, AttributeSet attrs) { + super(context, attrs); + + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + + mMediumAnimationDuration = getResources().getInteger( + android.R.integer.config_mediumAnimTime); + + setWillNotDraw(false); + mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR); + + final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); + setEnabled(a.getBoolean(0, true)); + a.recycle(); + + final TypedArray a2 = context.obtainStyledAttributes(attrs, R.styleable.SwipyRefreshLayout); + SwipyRefreshLayoutDirection direction + = SwipyRefreshLayoutDirection.getFromInt(a2.getInt(R.styleable.SwipyRefreshLayout_direction, 0)); + if (direction != SwipyRefreshLayoutDirection.BOTH) { + mDirection = direction; + mBothDirection = false; + } else { + mDirection = SwipyRefreshLayoutDirection.TOP; + mBothDirection = true; + } + a2.recycle(); + + final DisplayMetrics metrics = getResources().getDisplayMetrics(); + mCircleWidth = (int) (CIRCLE_DIAMETER * metrics.density); + mCircleHeight = (int) (CIRCLE_DIAMETER * metrics.density); + + createProgressView(); + ViewCompat.setChildrenDrawingOrderEnabled(this, true); + // the absolute offset has to take into account that the circle starts at an offset + mSpinnerFinalOffset = DEFAULT_CIRCLE_TARGET * metrics.density; + } + + 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, CIRCLE_DIAMETER / 2); + mProgress = new MaterialProgressDrawable(getContext(), this); + mProgress.setBackgroundColor(CIRCLE_BG_LIGHT); + 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(OnRefreshListener listener) { + mListener = listener; + } + + /** + * Pre API 11, alpha is used to make the progress circle appear instead of scale. + */ + private boolean isAlphaUsedForScale() { + return android.os.Build.VERSION.SDK_INT < 11; + } + + /** + * 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) { + switch (mDirection) { + case BOTTOM: + endTarget = getMeasuredHeight() - (int) (mSpinnerFinalOffset); + break; + case TOP: + default: + endTarget = (int) (mSpinnerFinalOffset - Math.abs(mOriginalOffsetTop)); + break; + } + } else { + endTarget = (int) mSpinnerFinalOffset; + } + setTargetOffsetTopAndBottom(endTarget - mCurrentTargetOffsetTop, + true /* requires update */); + mNotify = false; + startScaleUpAnimation(mRefreshListener); + } else { + setRefreshing(refreshing, false /* notify */); + } + } + + private void startScaleUpAnimation(AnimationListener listener) { + mCircleView.setVisibility(View.VISIBLE); + if (android.os.Build.VERSION.SDK_INT >= 11) { + // Pre API 11, alpha is used in place of scale up to show the + // progress circle appearing. + // Don't adjust the alpha during appearance otherwise. + mProgress.setAlpha(MAX_ALPHA); + } + mScaleAnimation = new Animation() { + @Override + public void applyTransformation(float interpolatedTime, Transformation t) { + setAnimationProgress(interpolatedTime); + } + }; + mScaleAnimation.setDuration(mMediumAnimationDuration); + if (listener != null) { + mCircleView.setAnimationListener(listener); + } + mCircleView.clearAnimation(); + mCircleView.startAnimation(mScaleAnimation); + } + + /** + * Pre API 11, this does an alpha animation. + * + * @param progress + */ + private void setAnimationProgress(float progress) { + if (isAlphaUsedForScale()) { + setColorViewAlpha((int) (progress * MAX_ALPHA)); + } else { + ViewCompat.setScaleX(mCircleView, progress); + ViewCompat.setScaleY(mCircleView, 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); + } + } + } + + private 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) { + // Pre API 11, alpha is used in place of scale. Don't also use it to + // show the trigger point. + if (mScale && isAlphaUsedForScale()) { + return null; + } + Animation alpha = new Animation() { + @Override + public void applyTransformation(float interpolatedTime, Transformation t) { + mProgress + .setAlpha((int) (startingAlpha + ((endingAlpha - startingAlpha) + * interpolatedTime))); + } + }; + alpha.setDuration(ALPHA_ANIMATION_DURATION); + // Clear out the previous animation listeners. + mCircleView.setAnimationListener(null); + mCircleView.clearAnimation(); + mCircleView.startAnimation(alpha); + return alpha; + } + + /** + * Set the background color of the progress spinner disc. + * + * @param colorRes Resource id of the color. + */ + public void setProgressBackgroundColor(int colorRes) { + mCircleView.setBackgroundColor(colorRes); + mProgress.setBackgroundColor(getResources().getColor(colorRes)); + } + + /** + * @deprecated Use {@link #setColorSchemeResources(int...)} + */ + @Deprecated + public void setColorScheme(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(int... colorResIds) { + final Resources res = getResources(); + int[] colorRes = new int[colorResIds.length]; + for (int i = 0; i < colorResIds.length; i++) { + colorRes[i] = res.getColor(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(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; + } + } + } + if (mTotalDragDistance == -1) { + if (getParent() != null && ((View) getParent()).getHeight() > 0) { + final DisplayMetrics metrics = getResources().getDisplayMetrics(); + mTotalDragDistance = (int) Math.min( + ((View) getParent()).getHeight() * MAX_SWIPE_DISTANCE_FACTOR, + REFRESH_TRIGGER_DISTANCE * metrics.density); + } + } + } + + /** + * 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(mCircleWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY)); + if (!mUsingCustomStart && !mOriginalOffsetCalculated) { + mOriginalOffsetCalculated = true; + + switch (mDirection) { + case BOTTOM: + mCurrentTargetOffsetTop = mOriginalOffsetTop = getMeasuredHeight(); + break; + case TOP: + default: + mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight(); + break; + } + } + mCircleViewIndex = -1; + // Get the index of the circleview. + for (int index = 0; index < getChildCount(); index++) { + if (getChildAt(index) == mCircleView) { + mCircleViewIndex = index; + break; + } + } + } + + /** + * @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 (android.os.Build.VERSION.SDK_INT < 14) { + if (mTarget instanceof AbsListView) { + final AbsListView absListView = (AbsListView) mTarget; + return absListView.getChildCount() > 0 + && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0) + .getTop() < absListView.getPaddingTop()); + } else { + return mTarget.getScrollY() > 0; + } + } else { + return ViewCompat.canScrollVertically(mTarget, -1); + } + } +// public boolean canChildScrollUp() { +// if (android.os.Build.VERSION.SDK_INT < 14) { +// if (mTarget instanceof AbsListView) { +// final AbsListView absListView = (AbsListView) mTarget; +// if (absListView.getLastVisiblePosition() + 1 == absListView.getCount()) { +// int lastIndex = absListView.getLastVisiblePosition() - absListView.getFirstVisiblePosition(); +// +// boolean res = absListView.getChildAt(lastIndex).getBottom() == absListView.getPaddingBottom(); +// +// return res; +// } +// return true; +// } else { +// return mTarget.getScrollY() > 0; +// } +// } else { +// return ViewCompat.canScrollVertically(mTarget, 1); +// } +// } + + + public boolean canChildScrollDown() { + if (android.os.Build.VERSION.SDK_INT < 14) { + if (mTarget instanceof AbsListView) { + final AbsListView absListView = (AbsListView) mTarget; + try { + if (absListView.getCount() > 0) { + if (absListView.getLastVisiblePosition() + 1 == absListView.getCount()) { + int lastIndex = absListView.getLastVisiblePosition() - absListView.getFirstVisiblePosition(); + return absListView.getChildAt(lastIndex).getBottom() == absListView.getPaddingBottom(); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return true; + } else { + return true; + } + } else { + return ViewCompat.canScrollVertically(mTarget, 1); + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + ensureTarget(); + + final int action = MotionEventCompat.getActionMasked(ev); + + if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { + mReturningToStart = false; + } + + switch (mDirection) { + case BOTTOM: + if (!isEnabled() || mReturningToStart || (!mBothDirection && canChildScrollDown()) || mRefreshing) { + // Fail fast if we're not in a state where a swipe is possible + return false; + } + break; + case TOP: + default: + if (!isEnabled() || mReturningToStart || (!mBothDirection && canChildScrollUp()) || mRefreshing) { + // Fail fast if we're not in a state where a swipe is possible + return false; + } + break; + } + + switch (action) { + case MotionEvent.ACTION_DOWN: + setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true); + mActivePointerId = MotionEventCompat.getPointerId(ev, 0); + mIsBeingDragged = false; + final float initialDownY = getMotionEventY(ev, mActivePointerId); + if (initialDownY == -1) { + return false; + } + mInitialDownY = initialDownY; + + case MotionEvent.ACTION_MOVE: + if (mActivePointerId == INVALID_POINTER) { + return false; + } + + final float y = getMotionEventY(ev, mActivePointerId); + if (y == -1) { + return false; + } + if (mBothDirection) { + if (y > mInitialDownY) { + setRawDirection(SwipyRefreshLayoutDirection.TOP); + } else if (y < mInitialDownY) { + setRawDirection(SwipyRefreshLayoutDirection.BOTTOM); + } + if ((mDirection == SwipyRefreshLayoutDirection.BOTTOM && canChildScrollDown()) + || (mDirection == SwipyRefreshLayoutDirection.TOP && canChildScrollUp())) { + mInitialDownY = y; + return false; + } + } + float yDiff; + switch (mDirection) { + case BOTTOM: + yDiff = mInitialDownY - y; + break; + case TOP: + default: + yDiff = y - mInitialDownY; + break; + } + if (yDiff > mTouchSlop && !mIsBeingDragged) { + switch (mDirection) { + case BOTTOM: + mInitialMotionY = mInitialDownY - mTouchSlop; + break; + case TOP: + default: + mInitialMotionY = mInitialDownY + mTouchSlop; + break; + } + mIsBeingDragged = true; + mProgress.setAlpha(STARTING_PROGRESS_ALPHA); + } + break; + + case MotionEventCompat.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mIsBeingDragged = false; + mActivePointerId = INVALID_POINTER; + break; + } + + return mIsBeingDragged; + } + + private float getMotionEventY(MotionEvent ev, int activePointerId) { + final int index = MotionEventCompat.findPointerIndex(ev, activePointerId); + if (index < 0) { + return -1; + } + return MotionEventCompat.getY(ev, index); + } + + @Override + public void requestDisallowInterceptTouchEvent(boolean b) { + // Nope. + } + + private boolean isAnimationRunning(Animation animation) { + return animation != null && animation.hasStarted() && !animation.hasEnded(); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + try { + final int action = MotionEventCompat.getActionMasked(ev); + + if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { + mReturningToStart = false; + } + + switch (mDirection) { + case BOTTOM: + if (!isEnabled() || mReturningToStart || canChildScrollDown() || mRefreshing) { + // Fail fast if we're not in a state where a swipe is possible + return false; + } + break; + case TOP: + default: + if (!isEnabled() || mReturningToStart || canChildScrollUp() || mRefreshing) { + // Fail fast if we're not in a state where a swipe is possible + return false; + } + break; + } + + switch (action) { + case MotionEvent.ACTION_DOWN: + mActivePointerId = MotionEventCompat.getPointerId(ev, 0); + mIsBeingDragged = false; + break; + + case MotionEvent.ACTION_MOVE: { + final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); + if (pointerIndex < 0) { + return false; + } + + final float y = MotionEventCompat.getY(ev, pointerIndex); + + float overscrollTop; + switch (mDirection) { + case BOTTOM: + overscrollTop = (mInitialMotionY - y) * DRAG_RATE; + break; + case TOP: + default: + overscrollTop = (y - mInitialMotionY) * DRAG_RATE; + break; + } + if (mIsBeingDragged) { + mProgress.showArrow(true); + float originalDragPercent = overscrollTop / mTotalDragDistance; + if (originalDragPercent < 0) { + return false; + } + float dragPercent = Math.min(1f, Math.abs(originalDragPercent)); + float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3; + float extraOS = Math.abs(overscrollTop) - mTotalDragDistance; + float slingshotDist = mUsingCustomStart ? mSpinnerFinalOffset + - mOriginalOffsetTop : mSpinnerFinalOffset; + float tensionSlingshotPercent = Math.max(0, + Math.min(extraOS, slingshotDist * 2) / slingshotDist); + float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow( + (tensionSlingshotPercent / 4), 2)) * 2f; + float extraMove = (slingshotDist) * tensionPercent * 2; + + // int targetY = mOriginalOffsetTop + (int) ((slingshotDist * dragPercent) + extraMove); + int targetY; + if (mDirection == SwipyRefreshLayoutDirection.TOP) { + targetY = mOriginalOffsetTop + (int) ((slingshotDist * dragPercent) + extraMove); + } else { + targetY = mOriginalOffsetTop - (int) ((slingshotDist * dragPercent) + extraMove); + } + // where 1.0f is a full circle + if (mCircleView.getVisibility() != View.VISIBLE) { + mCircleView.setVisibility(View.VISIBLE); + } + if (!mScale) { + ViewCompat.setScaleX(mCircleView, 1f); + ViewCompat.setScaleY(mCircleView, 1f); + } + if (overscrollTop < mTotalDragDistance) { + if (mScale) { + setAnimationProgress(overscrollTop / mTotalDragDistance); + } + if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA + && !isAnimationRunning(mAlphaStartAnimation)) { + // Animate the alpha + startProgressAlphaStartAnimation(); + } + float strokeStart = (float) (adjustedPercent * .8f); + mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart)); + mProgress.setArrowScale(Math.min(1f, adjustedPercent)); + } else { + if (mProgress.getAlpha() < MAX_ALPHA + && !isAnimationRunning(mAlphaMaxAnimation)) { + // Animate the alpha + startProgressAlphaMaxAnimation(); + } + } + float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f; + mProgress.setProgressRotation(rotation); + setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, + true /* requires update */); + } + break; + } + case MotionEventCompat.ACTION_POINTER_DOWN: { + final int index = MotionEventCompat.getActionIndex(ev); + mActivePointerId = MotionEventCompat.getPointerId(ev, index); + break; + } + + case MotionEventCompat.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: { + if (mActivePointerId == INVALID_POINTER) { + if (action == MotionEvent.ACTION_UP) { + } + return false; + } + final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); + final float y = MotionEventCompat.getY(ev, pointerIndex); + + float overscrollTop; + switch (mDirection) { + case BOTTOM: + overscrollTop = (mInitialMotionY - y) * DRAG_RATE; + break; + case TOP: + default: + overscrollTop = (y - mInitialMotionY) * DRAG_RATE; + break; + } + mIsBeingDragged = false; + 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.showArrow(false); + } + mActivePointerId = INVALID_POINTER; + return false; + } + } + } catch (Exception e) { + Log.e(TAG, "An exception occured during SwipyRefreshLayout onTouchEvent " + e.toString()); + } + + return true; + } + + 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) { + switch (mDirection) { + case BOTTOM: + endTarget = getMeasuredHeight() - (int) (mSpinnerFinalOffset); + break; + case TOP: + default: + endTarget = (int) (mSpinnerFinalOffset - Math.abs(mOriginalOffsetTop)); + break; + } + } else { + endTarget = (int) mSpinnerFinalOffset; + } + targetTop = (mFrom + (int) ((endTarget - mFrom) * interpolatedTime)); + int offset = targetTop - mCircleView.getTop(); + setTargetOffsetTopAndBottom(offset, false /* requires update */); + } + }; + + private void moveToStart(float interpolatedTime) { + int targetTop = 0; + targetTop = (mFrom + (int) ((mOriginalOffsetTop - mFrom) * interpolatedTime)); + int offset = targetTop - mCircleView.getTop(); + setTargetOffsetTopAndBottom(offset, false /* requires update */); + } + + 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; + if (isAlphaUsedForScale()) { + mStartingScale = mProgress.getAlpha(); + } else { + mStartingScale = ViewCompat.getScaleX(mCircleView); + } + mScaleDownToStartAnimation = new Animation() { + @Override + public void applyTransformation(float interpolatedTime, Transformation t) { + float targetScale = (mStartingScale + (-mStartingScale * interpolatedTime)); + setAnimationProgress(targetScale); + moveToStart(interpolatedTime); + } + }; + mScaleDownToStartAnimation.setDuration(SCALE_DOWN_DURATION); + if (listener != null) { + mCircleView.setAnimationListener(listener); + } + mCircleView.clearAnimation(); + mCircleView.startAnimation(mScaleDownToStartAnimation); + } + + private void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate) { + mCircleView.bringToFront(); + mCircleView.offsetTopAndBottom(offset); + +// switch (mDirection) { +// case BOTTOM: +// mCurrentTargetOffsetTop = getMeasuredHeight() - mCircleView.getMeasuredHeight(); +// break; +// case TOP: +// default: +// mCurrentTargetOffsetTop = mCircleView.getTop(); +// break; +// } + mCurrentTargetOffsetTop = mCircleView.getTop(); + if (requiresUpdate && android.os.Build.VERSION.SDK_INT < 11) { + invalidate(); + } + } + + private void onSecondaryPointerUp(MotionEvent ev) { + final int pointerIndex = MotionEventCompat.getActionIndex(ev); + final int pointerId = MotionEventCompat.getPointerId(ev, 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 = MotionEventCompat.getPointerId(ev, newPointerIndex); + } + } + + /** + * Classes that wish to be notified when the swipe gesture correctly + * triggers a refresh should implement this interface. + */ + public interface OnRefreshListener { + public void onRefresh(SwipyRefreshLayoutDirection direction); + } + + public SwipyRefreshLayoutDirection getDirection() { + return mBothDirection ? SwipyRefreshLayoutDirection.BOTH : mDirection; + } + + public void setDirection(SwipyRefreshLayoutDirection direction) { + if (direction == SwipyRefreshLayoutDirection.BOTH) { + mBothDirection = true; + } else { + mBothDirection = false; + mDirection = direction; + } + + switch (mDirection) { + case BOTTOM: + mCurrentTargetOffsetTop = mOriginalOffsetTop = getMeasuredHeight(); + break; + case TOP: + default: + mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight(); + break; + } + } + + // only TOP or Bottom + private void setRawDirection(SwipyRefreshLayoutDirection direction) { + if (mDirection == direction) { + return; + } + + mDirection = direction; + switch (mDirection) { + case BOTTOM: + mCurrentTargetOffsetTop = mOriginalOffsetTop = getMeasuredHeight(); + break; + case TOP: + default: + mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight(); + break; + } + } +} diff --git a/libs/SwipyRefreshLayout/src/main/java/com/orangegangsters/github/swipyrefreshlayout/library/SwipyRefreshLayoutDirection.java b/libs/SwipyRefreshLayout/src/main/java/com/orangegangsters/github/swipyrefreshlayout/library/SwipyRefreshLayoutDirection.java new file mode 100644 index 00000000..af5d6c2d --- /dev/null +++ b/libs/SwipyRefreshLayout/src/main/java/com/orangegangsters/github/swipyrefreshlayout/library/SwipyRefreshLayoutDirection.java @@ -0,0 +1,27 @@ +package com.orangegangsters.github.swipyrefreshlayout.library; + +/** + * Created by oliviergoutay on 1/23/15. + */ +public enum SwipyRefreshLayoutDirection { + + TOP(0), + BOTTOM(1), + BOTH(2); + + private int mValue; + + SwipyRefreshLayoutDirection(int value) { + this.mValue = value; + } + + public static SwipyRefreshLayoutDirection getFromInt(int value) { + for (SwipyRefreshLayoutDirection direction : SwipyRefreshLayoutDirection.values()) { + if (direction.mValue == value) { + return direction; + } + } + return BOTH; + } + +} diff --git a/libs/SwipyRefreshLayout/src/main/res/values/attrs.xml b/libs/SwipyRefreshLayout/src/main/res/values/attrs.xml new file mode 100644 index 00000000..db0c1d32 --- /dev/null +++ b/libs/SwipyRefreshLayout/src/main/res/values/attrs.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file -- cgit v1.2.3