diff --git a/Loadinglibrary/build.gradle b/Loadinglibrary/build.gradle
new file mode 100644
index 00000000..cb5850f3
--- /dev/null
+++ b/Loadinglibrary/build.gradle
@@ -0,0 +1,25 @@
+plugins {
+ alias(libs.plugins.android.library)
+}
+android {
+ compileSdk 35
+ namespace 'app.dinus.com.loadingdrawable'
+ defaultConfig {
+ minSdkVersion 24
+ targetSdkVersion 35
+ versionCode 1
+ versionName "1.0"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ implementation 'androidx.core:core:1.5.0' // or later
+ implementation 'androidx.annotation:annotation:1.6.0'
+ implementation libs.androidx.interpolator
+}
diff --git a/Loadinglibrary/proguard-rules.pro b/Loadinglibrary/proguard-rules.pro
new file mode 100644
index 00000000..290d4c09
--- /dev/null
+++ b/Loadinglibrary/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/dinus/workspace/android-sdk-macosx/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/Loadinglibrary/src/main/AndroidManifest.xml b/Loadinglibrary/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..3e848d82
--- /dev/null
+++ b/Loadinglibrary/src/main/AndroidManifest.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
diff --git a/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/DensityUtil.java b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/DensityUtil.java
new file mode 100644
index 00000000..c88e7895
--- /dev/null
+++ b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/DensityUtil.java
@@ -0,0 +1,11 @@
+package app.dinus.com.loadingdrawable;
+
+import android.content.Context;
+
+public class DensityUtil {
+
+ public static float dip2px(Context context, float dpValue) {
+ float scale = context.getResources().getDisplayMetrics().density;
+ return dpValue * scale;
+ }
+}
\ No newline at end of file
diff --git a/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/LoadingView.java b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/LoadingView.java
new file mode 100644
index 00000000..ba77e642
--- /dev/null
+++ b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/LoadingView.java
@@ -0,0 +1,77 @@
+package app.dinus.com.loadingdrawable;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageView;
+
+import app.dinus.com.loadingdrawable.render.LoadingDrawable;
+import app.dinus.com.loadingdrawable.render.LoadingRenderer;
+import app.dinus.com.loadingdrawable.render.LoadingRendererFactory;
+
+public class LoadingView extends ImageView {
+ private LoadingDrawable mLoadingDrawable;
+
+ public LoadingView(Context context) {
+ super(context);
+ }
+
+ public LoadingView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initAttrs(context, attrs);
+ }
+
+ private void initAttrs(Context context, AttributeSet attrs) {
+ try {
+ TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.LoadingView);
+ int loadingRendererId = ta.getInt(R.styleable.LoadingView_loading_renderer, 0);
+ LoadingRenderer loadingRenderer = LoadingRendererFactory.createLoadingRenderer(context, loadingRendererId);
+ setLoadingRenderer(loadingRenderer);
+ ta.recycle();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void setLoadingRenderer(LoadingRenderer loadingRenderer) {
+ mLoadingDrawable = new LoadingDrawable(loadingRenderer);
+ setImageDrawable(mLoadingDrawable);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ startAnimation();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ stopAnimation();
+ }
+
+ @Override
+ protected void onVisibilityChanged(View changedView, int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+
+ final boolean visible = visibility == VISIBLE && getVisibility() == VISIBLE;
+ if (visible) {
+ startAnimation();
+ } else {
+ stopAnimation();
+ }
+ }
+
+ private void startAnimation() {
+ if (mLoadingDrawable != null) {
+ mLoadingDrawable.start();
+ }
+ }
+
+ private void stopAnimation() {
+ if (mLoadingDrawable != null) {
+ mLoadingDrawable.stop();
+ }
+ }
+}
diff --git a/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/LoadingDrawable.java b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/LoadingDrawable.java
new file mode 100644
index 00000000..0c30ed64
--- /dev/null
+++ b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/LoadingDrawable.java
@@ -0,0 +1,89 @@
+package app.dinus.com.loadingdrawable.render;
+
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Animatable;
+import android.graphics.drawable.Drawable;
+
+import app.dinus.com.loadingdrawable.render.LoadingRenderer;
+
+public class LoadingDrawable extends Drawable implements Animatable {
+ private final LoadingRenderer mLoadingRender;
+
+ 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);
+ }
+ };
+
+ public LoadingDrawable(LoadingRenderer loadingRender) {
+ this.mLoadingRender = loadingRender;
+ this.mLoadingRender.setCallback(mCallback);
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ super.onBoundsChange(bounds);
+ this.mLoadingRender.setBounds(bounds);
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ if (!getBounds().isEmpty()) {
+ this.mLoadingRender.draw(canvas);
+ }
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ this.mLoadingRender.setAlpha(alpha);
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter cf) {
+ this.mLoadingRender.setColorFilter(cf);
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ @Override
+ public void start() {
+ this.mLoadingRender.start();
+ }
+
+ @Override
+ public void stop() {
+ this.mLoadingRender.stop();
+ }
+
+ @Override
+ public boolean isRunning() {
+ return this.mLoadingRender.isRunning();
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return (int) this.mLoadingRender.mHeight;
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return (int) this.mLoadingRender.mWidth;
+ }
+}
diff --git a/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/LoadingRenderer.java b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/LoadingRenderer.java
new file mode 100644
index 00000000..fe0282b5
--- /dev/null
+++ b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/LoadingRenderer.java
@@ -0,0 +1,124 @@
+package app.dinus.com.loadingdrawable.render;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.animation.Animation;
+import android.view.animation.LinearInterpolator;
+
+import app.dinus.com.loadingdrawable.DensityUtil;
+
+public abstract class LoadingRenderer {
+ private static final long ANIMATION_DURATION = 1333;
+ private static final float DEFAULT_SIZE = 56.0f;
+
+ private final ValueAnimator.AnimatorUpdateListener mAnimatorUpdateListener
+ = new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ computeRender((float) animation.getAnimatedValue());
+ invalidateSelf();
+ }
+ };
+
+ /**
+ * Whenever {@link LoadingDrawable} boundary changes mBounds will be updated.
+ * More details you can see {@link LoadingDrawable#onBoundsChange(Rect)}
+ */
+ protected final Rect mBounds = new Rect();
+
+ private Drawable.Callback mCallback;
+ private ValueAnimator mRenderAnimator;
+
+ protected long mDuration;
+
+ protected float mWidth;
+ protected float mHeight;
+
+ public LoadingRenderer(Context context) {
+ initParams(context);
+ setupAnimators();
+ }
+
+ @Deprecated
+ protected void draw(Canvas canvas, Rect bounds) {
+ }
+
+ protected void draw(Canvas canvas) {
+ draw(canvas, mBounds);
+ }
+
+ protected abstract void computeRender(float renderProgress);
+
+ protected abstract void setAlpha(int alpha);
+
+ protected abstract void setColorFilter(ColorFilter cf);
+
+ protected abstract void reset();
+
+ protected void addRenderListener(Animator.AnimatorListener animatorListener) {
+ mRenderAnimator.addListener(animatorListener);
+ }
+
+ void start() {
+ reset();
+ mRenderAnimator.addUpdateListener(mAnimatorUpdateListener);
+
+ mRenderAnimator.setRepeatCount(ValueAnimator.INFINITE);
+ mRenderAnimator.setDuration(mDuration);
+ mRenderAnimator.start();
+ }
+
+ void stop() {
+ // if I just call mRenderAnimator.end(),
+ // it will always call the method onAnimationUpdate(ValueAnimator animation)
+ // why ? if you know why please send email to me (dinus_developer@163.com)
+ mRenderAnimator.removeUpdateListener(mAnimatorUpdateListener);
+
+ mRenderAnimator.setRepeatCount(0);
+ mRenderAnimator.setDuration(0);
+ mRenderAnimator.end();
+ }
+
+ boolean isRunning() {
+ return mRenderAnimator.isRunning();
+ }
+
+ void setCallback(Drawable.Callback callback) {
+ this.mCallback = callback;
+ }
+
+ void setBounds(Rect bounds) {
+ mBounds.set(bounds);
+ }
+
+ private void initParams(Context context) {
+ mWidth = DensityUtil.dip2px(context, DEFAULT_SIZE);
+ mHeight = DensityUtil.dip2px(context, DEFAULT_SIZE);
+
+ mDuration = ANIMATION_DURATION;
+ }
+
+ private void setupAnimators() {
+ mRenderAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
+ mRenderAnimator.setRepeatCount(Animation.INFINITE);
+ mRenderAnimator.setRepeatMode(Animation.RESTART);
+ mRenderAnimator.setDuration(mDuration);
+ //fuck you! the default interpolator is AccelerateDecelerateInterpolator
+ mRenderAnimator.setInterpolator(new LinearInterpolator());
+ mRenderAnimator.addUpdateListener(mAnimatorUpdateListener);
+ }
+
+ private void invalidateSelf() {
+ mCallback.invalidateDrawable(null);
+ }
+
+}
diff --git a/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/LoadingRendererFactory.java b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/LoadingRendererFactory.java
new file mode 100644
index 00000000..59040b06
--- /dev/null
+++ b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/LoadingRendererFactory.java
@@ -0,0 +1,71 @@
+package app.dinus.com.loadingdrawable.render;
+
+import android.content.Context;
+import android.util.SparseArray;
+
+import java.lang.reflect.Constructor;
+
+import app.dinus.com.loadingdrawable.render.animal.FishLoadingRenderer;
+import app.dinus.com.loadingdrawable.render.animal.GhostsEyeLoadingRenderer;
+import app.dinus.com.loadingdrawable.render.circle.jump.CollisionLoadingRenderer;
+import app.dinus.com.loadingdrawable.render.circle.jump.DanceLoadingRenderer;
+import app.dinus.com.loadingdrawable.render.circle.jump.GuardLoadingRenderer;
+import app.dinus.com.loadingdrawable.render.circle.jump.SwapLoadingRenderer;
+import app.dinus.com.loadingdrawable.render.circle.rotate.GearLoadingRenderer;
+import app.dinus.com.loadingdrawable.render.circle.rotate.LevelLoadingRenderer;
+import app.dinus.com.loadingdrawable.render.circle.rotate.MaterialLoadingRenderer;
+import app.dinus.com.loadingdrawable.render.circle.rotate.WhorlLoadingRenderer;
+import app.dinus.com.loadingdrawable.render.goods.BalloonLoadingRenderer;
+import app.dinus.com.loadingdrawable.render.goods.WaterBottleLoadingRenderer;
+import app.dinus.com.loadingdrawable.render.scenery.DayNightLoadingRenderer;
+import app.dinus.com.loadingdrawable.render.scenery.ElectricFanLoadingRenderer;
+import app.dinus.com.loadingdrawable.render.shapechange.CircleBroodLoadingRenderer;
+import app.dinus.com.loadingdrawable.render.shapechange.CoolWaitLoadingRenderer;
+
+public final class LoadingRendererFactory {
+ private static final SparseArray> LOADING_RENDERERS = new SparseArray<>();
+
+ static {
+ //circle rotate
+ LOADING_RENDERERS.put(0, MaterialLoadingRenderer.class);
+ LOADING_RENDERERS.put(1, LevelLoadingRenderer.class);
+ LOADING_RENDERERS.put(2, WhorlLoadingRenderer.class);
+ LOADING_RENDERERS.put(3, GearLoadingRenderer.class);
+ //circle jump
+ LOADING_RENDERERS.put(4, SwapLoadingRenderer.class);
+ LOADING_RENDERERS.put(5, GuardLoadingRenderer.class);
+ LOADING_RENDERERS.put(6, DanceLoadingRenderer.class);
+ LOADING_RENDERERS.put(7, CollisionLoadingRenderer.class);
+ //scenery
+ LOADING_RENDERERS.put(8, DayNightLoadingRenderer.class);
+ LOADING_RENDERERS.put(9, ElectricFanLoadingRenderer.class);
+ //animal
+ LOADING_RENDERERS.put(10, FishLoadingRenderer.class);
+ LOADING_RENDERERS.put(11, GhostsEyeLoadingRenderer.class);
+ //goods
+ LOADING_RENDERERS.put(12, BalloonLoadingRenderer.class);
+ LOADING_RENDERERS.put(13, WaterBottleLoadingRenderer.class);
+ //shape change
+ LOADING_RENDERERS.put(14, CircleBroodLoadingRenderer.class);
+ LOADING_RENDERERS.put(15, CoolWaitLoadingRenderer.class);
+ }
+
+ private LoadingRendererFactory() {
+ }
+
+ public static LoadingRenderer createLoadingRenderer(Context context, int loadingRendererId) throws Exception {
+ Class> loadingRendererClazz = LOADING_RENDERERS.get(loadingRendererId);
+ Constructor>[] constructors = loadingRendererClazz.getDeclaredConstructors();
+ for (Constructor> constructor : constructors) {
+ Class>[] parameterTypes = constructor.getParameterTypes();
+ if (parameterTypes != null
+ && parameterTypes.length == 1
+ && parameterTypes[0].equals(Context.class)) {
+ constructor.setAccessible(true);
+ return (LoadingRenderer) constructor.newInstance(context);
+ }
+ }
+
+ throw new InstantiationException();
+ }
+}
diff --git a/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/animal/FishLoadingRenderer.java b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/animal/FishLoadingRenderer.java
new file mode 100644
index 00000000..2c63d48e
--- /dev/null
+++ b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/animal/FishLoadingRenderer.java
@@ -0,0 +1,262 @@
+package app.dinus.com.loadingdrawable.render.animal;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.DashPathEffect;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PathMeasure;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.util.DisplayMetrics;
+import android.view.animation.Interpolator;
+
+import app.dinus.com.loadingdrawable.DensityUtil;
+import app.dinus.com.loadingdrawable.render.LoadingRenderer;
+
+public class FishLoadingRenderer extends LoadingRenderer {
+ private Interpolator FISH_INTERPOLATOR = new FishInterpolator();
+
+ private static final float DEFAULT_PATH_FULL_LINE_SIZE = 7.0f;
+ private static final float DEFAULT_PATH_DOTTED_LINE_SIZE = DEFAULT_PATH_FULL_LINE_SIZE / 2.0f;
+ private static final float DEFAULT_RIVER_HEIGHT = DEFAULT_PATH_FULL_LINE_SIZE * 8.5f;
+ private static final float DEFAULT_RIVER_WIDTH = DEFAULT_PATH_FULL_LINE_SIZE * 5.5f;
+
+ private static final float DEFAULT_FISH_EYE_SIZE = DEFAULT_PATH_FULL_LINE_SIZE * 0.5f;
+ private static final float DEFAULT_FISH_WIDTH = DEFAULT_PATH_FULL_LINE_SIZE * 3.0f;
+ private static final float DEFAULT_FISH_HEIGHT = DEFAULT_PATH_FULL_LINE_SIZE * 4.5f;
+
+ private static final float DEFAULT_WIDTH = 200.0f;
+ private static final float DEFAULT_HEIGHT = 150.0f;
+ private static final float DEFAULT_RIVER_BANK_WIDTH = DEFAULT_PATH_FULL_LINE_SIZE;
+
+ private static final long ANIMATION_DURATION = 800;
+ private static final float DOTTED_LINE_WIDTH_COUNT = (8.5f + 5.5f - 2.0f) * 2.0f * 2.0f;
+ private static final float DOTTED_LINE_WIDTH_RATE = 1.0f / DOTTED_LINE_WIDTH_COUNT;
+
+ private final float[] FISH_MOVE_POINTS = new float[]{
+ DOTTED_LINE_WIDTH_RATE * 3.0f, DOTTED_LINE_WIDTH_RATE * 6.0f,
+ DOTTED_LINE_WIDTH_RATE * 15f, DOTTED_LINE_WIDTH_RATE * 18f,
+ DOTTED_LINE_WIDTH_RATE * 27.0f, DOTTED_LINE_WIDTH_RATE * 30.0f,
+ DOTTED_LINE_WIDTH_RATE * 39f, DOTTED_LINE_WIDTH_RATE * 42f,
+ };
+
+ private final float FISH_MOVE_POINTS_RATE = 1.0f / FISH_MOVE_POINTS.length;
+
+ private static final int DEFAULT_COLOR = Color.parseColor("#fffefed6");
+
+ private final Paint mPaint = new Paint();
+ private final RectF mTempBounds = new RectF();
+
+ private final float[] mFishHeadPos = new float[2];
+
+ private Path mRiverPath;
+ private PathMeasure mRiverMeasure;
+
+ private float mFishRotateDegrees;
+
+ private float mRiverBankWidth;
+ private float mRiverWidth;
+ private float mRiverHeight;
+ private float mFishWidth;
+ private float mFishHeight;
+ private float mFishEyeSize;
+ private float mPathFullLineSize;
+ private float mPathDottedLineSize;
+
+ private int mColor;
+
+ private FishLoadingRenderer(Context context) {
+ super(context);
+ init(context);
+ setupPaint();
+ }
+
+ private void init(Context context) {
+ mWidth = DensityUtil.dip2px(context, DEFAULT_WIDTH);
+ mHeight = DensityUtil.dip2px(context, DEFAULT_HEIGHT);
+ mRiverBankWidth = DensityUtil.dip2px(context, DEFAULT_RIVER_BANK_WIDTH);
+
+ mPathFullLineSize = DensityUtil.dip2px(context, DEFAULT_PATH_FULL_LINE_SIZE);
+ mPathDottedLineSize = DensityUtil.dip2px(context, DEFAULT_PATH_DOTTED_LINE_SIZE);
+ mFishWidth = DensityUtil.dip2px(context, DEFAULT_FISH_WIDTH);
+ mFishHeight = DensityUtil.dip2px(context, DEFAULT_FISH_HEIGHT);
+ mFishEyeSize = DensityUtil.dip2px(context, DEFAULT_FISH_EYE_SIZE);
+ mRiverWidth = DensityUtil.dip2px(context, DEFAULT_RIVER_WIDTH);
+ mRiverHeight = DensityUtil.dip2px(context, DEFAULT_RIVER_HEIGHT);
+
+ mColor = DEFAULT_COLOR;
+
+ mDuration = ANIMATION_DURATION;
+ }
+
+ private void setupPaint() {
+ mPaint.setAntiAlias(true);
+ mPaint.setStrokeWidth(mRiverBankWidth);
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setStrokeJoin(Paint.Join.MITER);
+ mPaint.setPathEffect(new DashPathEffect(new float[]{mPathFullLineSize, mPathDottedLineSize}, mPathDottedLineSize));
+ }
+
+ @Override
+ protected void draw(Canvas canvas, Rect bounds) {
+ int saveCount = canvas.save();
+ RectF arcBounds = mTempBounds;
+ arcBounds.set(bounds);
+
+ mPaint.setColor(mColor);
+
+ //calculate fish clip bounds
+ //clip the width of the fish need to increase mPathDottedLineSize * 1.2f
+ RectF fishRectF = new RectF(mFishHeadPos[0] - mFishWidth / 2.0f - mPathDottedLineSize * 1.2f, mFishHeadPos[1] - mFishHeight / 2.0f,
+ mFishHeadPos[0] + mFishWidth / 2.0f + mPathDottedLineSize * 1.2f, mFishHeadPos[1] + mFishHeight / 2.0f);
+ Matrix matrix = new Matrix();
+ matrix.postRotate(mFishRotateDegrees, fishRectF.centerX(), fishRectF.centerY());
+ matrix.mapRect(fishRectF);
+
+ //draw river
+ int riverSaveCount = canvas.save();
+ mPaint.setStyle(Paint.Style.STROKE);
+ canvas.clipRect(fishRectF, Region.Op.DIFFERENCE);
+ canvas.drawPath(createRiverPath(arcBounds), mPaint);
+ canvas.restoreToCount(riverSaveCount);
+
+ //draw fish
+ int fishSaveCount = canvas.save();
+ mPaint.setStyle(Paint.Style.FILL);
+ canvas.rotate(mFishRotateDegrees, mFishHeadPos[0], mFishHeadPos[1]);
+ canvas.clipPath(createFishEyePath(mFishHeadPos[0], mFishHeadPos[1] - mFishHeight * 0.06f), Region.Op.DIFFERENCE);
+ canvas.drawPath(createFishPath(mFishHeadPos[0], mFishHeadPos[1]), mPaint);
+ canvas.restoreToCount(fishSaveCount);
+
+ canvas.restoreToCount(saveCount);
+ }
+
+ private float calculateRotateDegrees(float fishProgress) {
+ if (fishProgress < FISH_MOVE_POINTS_RATE * 2) {
+ return 90;
+ }
+
+ if (fishProgress < FISH_MOVE_POINTS_RATE * 4) {
+ return 180;
+ }
+
+ if (fishProgress < FISH_MOVE_POINTS_RATE * 6) {
+ return 270;
+ }
+
+ return 0.0f;
+ }
+
+ @Override
+ protected void computeRender(float renderProgress) {
+ if (mRiverPath == null) {
+ return;
+ }
+
+ if (mRiverMeasure == null) {
+ mRiverMeasure = new PathMeasure(mRiverPath, false);
+ }
+
+ float fishProgress = FISH_INTERPOLATOR.getInterpolation(renderProgress);
+
+ mRiverMeasure.getPosTan(mRiverMeasure.getLength() * fishProgress, mFishHeadPos, null);
+ mFishRotateDegrees = calculateRotateDegrees(fishProgress);
+ }
+
+ @Override
+ protected void setAlpha(int alpha) {
+
+ }
+
+ @Override
+ protected void setColorFilter(ColorFilter cf) {
+
+ }
+
+ @Override
+ protected void reset() {
+ }
+
+ private Path createFishEyePath(float fishEyeCenterX, float fishEyeCenterY) {
+ Path path = new Path();
+ path.addCircle(fishEyeCenterX, fishEyeCenterY, mFishEyeSize, Path.Direction.CW);
+
+ return path;
+ }
+
+ private Path createFishPath(float fishCenterX, float fishCenterY) {
+ Path path = new Path();
+
+ float fishHeadX = fishCenterX;
+ float fishHeadY = fishCenterY - mFishHeight / 2.0f;
+
+ //the head of the fish
+ path.moveTo(fishHeadX, fishHeadY);
+ //the left body of the fish
+ path.quadTo(fishHeadX - mFishWidth * 0.333f, fishHeadY + mFishHeight * 0.222f, fishHeadX - mFishWidth * 0.333f, fishHeadY + mFishHeight * 0.444f);
+ path.lineTo(fishHeadX - mFishWidth * 0.333f, fishHeadY + mFishHeight * 0.666f);
+ path.lineTo(fishHeadX - mFishWidth * 0.5f, fishHeadY + mFishHeight * 0.8f);
+ path.lineTo(fishHeadX - mFishWidth * 0.5f, fishHeadY + mFishHeight);
+
+ //the tail of the fish
+ path.lineTo(fishHeadX, fishHeadY + mFishHeight * 0.9f);
+
+ //the right body of the fish
+ path.lineTo(fishHeadX + mFishWidth * 0.5f, fishHeadY + mFishHeight);
+ path.lineTo(fishHeadX + mFishWidth * 0.5f, fishHeadY + mFishHeight * 0.8f);
+ path.lineTo(fishHeadX + mFishWidth * 0.333f, fishHeadY + mFishHeight * 0.666f);
+ path.lineTo(fishHeadX + mFishWidth * 0.333f, fishHeadY + mFishHeight * 0.444f);
+ path.quadTo(fishHeadX + mFishWidth * 0.333f, fishHeadY + mFishHeight * 0.222f, fishHeadX, fishHeadY);
+
+ path.close();
+
+ return path;
+ }
+
+ private Path createRiverPath(RectF arcBounds) {
+ if (mRiverPath != null) {
+ return mRiverPath;
+ }
+
+ mRiverPath = new Path();
+
+ RectF rectF = new RectF(arcBounds.centerX() - mRiverWidth / 2.0f, arcBounds.centerY() - mRiverHeight / 2.0f,
+ arcBounds.centerX() + mRiverWidth / 2.0f, arcBounds.centerY() + mRiverHeight / 2.0f);
+
+ rectF.inset(mRiverBankWidth / 2.0f, mRiverBankWidth / 2.0f);
+
+ mRiverPath.addRect(rectF, Path.Direction.CW);
+
+ return mRiverPath;
+ }
+
+ private class FishInterpolator implements Interpolator {
+ @Override
+ public float getInterpolation(float input) {
+ int index = ((int) (input / FISH_MOVE_POINTS_RATE));
+ if (index >= FISH_MOVE_POINTS.length) {
+ index = FISH_MOVE_POINTS.length - 1;
+ }
+
+ return FISH_MOVE_POINTS[index];
+ }
+ }
+
+ public static class Builder {
+ private Context mContext;
+
+ public Builder(Context mContext) {
+ this.mContext = mContext;
+ }
+
+ public FishLoadingRenderer build() {
+ FishLoadingRenderer loadingRenderer = new FishLoadingRenderer(mContext);
+ return loadingRenderer;
+ }
+ }
+}
diff --git a/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/animal/GhostsEyeLoadingRenderer.java b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/animal/GhostsEyeLoadingRenderer.java
new file mode 100644
index 00000000..b13b2a86
--- /dev/null
+++ b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/animal/GhostsEyeLoadingRenderer.java
@@ -0,0 +1,261 @@
+package app.dinus.com.loadingdrawable.render.animal;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.DisplayMetrics;
+import android.view.animation.Interpolator;
+
+import app.dinus.com.loadingdrawable.DensityUtil;
+import app.dinus.com.loadingdrawable.render.LoadingRenderer;
+
+public class GhostsEyeLoadingRenderer extends LoadingRenderer {
+ private Interpolator EYE_BALL_INTERPOLATOR = new EyeBallInterpolator();
+ private Interpolator EYE_CIRCLE_INTERPOLATOR = new EyeCircleInterpolator();
+
+ private static final float DEFAULT_WIDTH = 200.0f;
+ private static final float DEFAULT_HEIGHT = 176.0f;
+ private static final float DEFAULT_EYE_EDGE_WIDTH = 5.0f;
+
+ private static final float DEFAULT_EYE_BALL_HEIGHT = 9.0f;
+ private static final float DEFAULT_EYE_BALL_WIDTH = 11.0f;
+
+ private static final float DEFAULT_EYE_CIRCLE_INTERVAL = 8.0f;
+ private static final float DEFAULT_EYE_BALL_OFFSET_Y = 2.0f;
+ private static final float DEFAULT_ABOVE_RADIAN_EYE_CIRCLE_OFFSET = 6.0f;
+ private static final float DEFAULT_EYE_CIRCLE_RADIUS = 21.0f;
+ private static final float DEFAULT_MAX_EYE_JUMP_DISTANCE = 11.0f;
+
+ private static final float LEFT_EYE_CIRCLE$BALL_START_JUMP_UP_OFFSET = 0.0f;
+ private static final float RIGHT_EYE_CIRCLE$BALL_START_JUMP_UP_OFFSET = 0.067f;
+
+ private static final float LEFT_EYE_BALL_END_JUMP_OFFSET = 0.4f;
+ private static final float LEFT_EYE_CIRCLE_END_JUMP_OFFSET = 0.533f;
+ private static final float RIGHT_EYE_BALL_END_JUMP_OFFSET = 0.467f;
+ private static final float RIGHT_EYE_CIRCLE_END_JUMP_OFFSET = 0.60f;
+
+ private static final int DEGREE_180 = 180;
+
+ private static final long ANIMATION_DURATION = 2333;
+
+ private static final int DEFAULT_COLOR = Color.parseColor("#ff484852");
+
+ private final Paint mPaint = new Paint();
+ private final RectF mTempBounds = new RectF();
+
+ private float mEyeInterval;
+ private float mEyeCircleRadius;
+ private float mMaxEyeJumptDistance;
+ private float mAboveRadianEyeOffsetX;
+ private float mEyeBallOffsetY;
+
+ private float mEyeEdgeWidth;
+ private float mEyeBallWidth;
+ private float mEyeBallHeight;
+
+ private float mLeftEyeCircleOffsetY;
+ private float mRightEyeCircleOffsetY;
+ private float mLeftEyeBallOffsetY;
+ private float mRightEyeBallOffsetY;
+
+ private int mColor;
+
+ private GhostsEyeLoadingRenderer(Context context) {
+ super(context);
+ init(context);
+ setupPaint();
+ }
+
+ private void init(Context context) {
+ mWidth = DensityUtil.dip2px(context, DEFAULT_WIDTH);
+ mHeight = DensityUtil.dip2px(context, DEFAULT_HEIGHT);
+ mEyeEdgeWidth = DensityUtil.dip2px(context, DEFAULT_EYE_EDGE_WIDTH);
+
+ mEyeInterval = DensityUtil.dip2px(context, DEFAULT_EYE_CIRCLE_INTERVAL);
+ mEyeBallOffsetY = DensityUtil.dip2px(context, DEFAULT_EYE_BALL_OFFSET_Y);
+ mEyeCircleRadius = DensityUtil.dip2px(context, DEFAULT_EYE_CIRCLE_RADIUS);
+ mMaxEyeJumptDistance = DensityUtil.dip2px(context, DEFAULT_MAX_EYE_JUMP_DISTANCE);
+ mAboveRadianEyeOffsetX = DensityUtil.dip2px(context, DEFAULT_ABOVE_RADIAN_EYE_CIRCLE_OFFSET);
+
+ mEyeBallWidth = DensityUtil.dip2px(context, DEFAULT_EYE_BALL_WIDTH);
+ mEyeBallHeight = DensityUtil.dip2px(context, DEFAULT_EYE_BALL_HEIGHT);
+
+ mColor = DEFAULT_COLOR;
+
+ mDuration = ANIMATION_DURATION;
+ }
+
+ private void setupPaint() {
+ mPaint.setAntiAlias(true);
+ mPaint.setStrokeWidth(mEyeEdgeWidth);
+ mPaint.setStrokeJoin(Paint.Join.ROUND);
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setStrokeCap(Paint.Cap.ROUND);
+ }
+
+ @Override
+ protected void draw(Canvas canvas, Rect bounds) {
+ int saveCount = canvas.save();
+ RectF arcBounds = mTempBounds;
+ arcBounds.set(bounds);
+
+ mPaint.setColor(mColor);
+
+ mPaint.setStyle(Paint.Style.STROKE);
+ canvas.drawPath(createLeftEyeCircle(arcBounds, mLeftEyeCircleOffsetY), mPaint);
+ canvas.drawPath(createRightEyeCircle(arcBounds, mRightEyeCircleOffsetY), mPaint);
+
+ mPaint.setStyle(Paint.Style.FILL);
+ //create left eye ball
+ canvas.drawOval(createLeftEyeBall(arcBounds, mLeftEyeBallOffsetY), mPaint);
+ //create right eye ball
+ canvas.drawOval(createRightEyeBall(arcBounds, mRightEyeBallOffsetY), mPaint);
+
+ canvas.restoreToCount(saveCount);
+ }
+
+ @Override
+ protected void computeRender(float renderProgress) {
+ if (renderProgress <= LEFT_EYE_BALL_END_JUMP_OFFSET && renderProgress >= LEFT_EYE_CIRCLE$BALL_START_JUMP_UP_OFFSET) {
+ float eyeCircle$BallJumpUpProgress = (renderProgress - LEFT_EYE_CIRCLE$BALL_START_JUMP_UP_OFFSET) / (LEFT_EYE_BALL_END_JUMP_OFFSET - LEFT_EYE_CIRCLE$BALL_START_JUMP_UP_OFFSET);
+ mLeftEyeBallOffsetY = -mMaxEyeJumptDistance * EYE_BALL_INTERPOLATOR.getInterpolation(eyeCircle$BallJumpUpProgress);
+ }
+
+ if (renderProgress <= LEFT_EYE_CIRCLE_END_JUMP_OFFSET && renderProgress >= LEFT_EYE_CIRCLE$BALL_START_JUMP_UP_OFFSET) {
+ float eyeCircle$BallJumpUpProgress = (renderProgress - LEFT_EYE_CIRCLE$BALL_START_JUMP_UP_OFFSET) / (LEFT_EYE_CIRCLE_END_JUMP_OFFSET - LEFT_EYE_CIRCLE$BALL_START_JUMP_UP_OFFSET);
+ mLeftEyeCircleOffsetY = -mMaxEyeJumptDistance * EYE_CIRCLE_INTERPOLATOR.getInterpolation(eyeCircle$BallJumpUpProgress);
+ }
+
+ if (renderProgress <= RIGHT_EYE_BALL_END_JUMP_OFFSET && renderProgress >= RIGHT_EYE_CIRCLE$BALL_START_JUMP_UP_OFFSET) {
+ float eyeCircle$BallJumpUpProgress = (renderProgress - RIGHT_EYE_CIRCLE$BALL_START_JUMP_UP_OFFSET) / (RIGHT_EYE_BALL_END_JUMP_OFFSET - RIGHT_EYE_CIRCLE$BALL_START_JUMP_UP_OFFSET);
+ mRightEyeBallOffsetY = -mMaxEyeJumptDistance * EYE_BALL_INTERPOLATOR.getInterpolation(eyeCircle$BallJumpUpProgress);
+ }
+
+ if (renderProgress <= RIGHT_EYE_CIRCLE_END_JUMP_OFFSET && renderProgress >= RIGHT_EYE_CIRCLE$BALL_START_JUMP_UP_OFFSET) {
+ float eyeCircle$BallJumpUpProgress = (renderProgress - RIGHT_EYE_CIRCLE$BALL_START_JUMP_UP_OFFSET) / (RIGHT_EYE_CIRCLE_END_JUMP_OFFSET - RIGHT_EYE_CIRCLE$BALL_START_JUMP_UP_OFFSET);
+ mRightEyeCircleOffsetY = -mMaxEyeJumptDistance * EYE_CIRCLE_INTERPOLATOR.getInterpolation(eyeCircle$BallJumpUpProgress);
+ }
+ }
+
+ @Override
+ protected void setAlpha(int alpha) {
+
+ }
+
+ @Override
+ protected void setColorFilter(ColorFilter cf) {
+
+ }
+
+ @Override
+ protected void reset() {
+ mLeftEyeBallOffsetY = 0.0f;
+ mRightEyeBallOffsetY = 0.0f;
+ mLeftEyeCircleOffsetY = 0.0f;
+ mRightEyeCircleOffsetY = 0.0f;
+ }
+
+ private RectF createLeftEyeBall(RectF arcBounds, float offsetY) {
+ //the center of the left eye
+ float leftEyeCenterX = arcBounds.centerX() - mEyeInterval / 2.0f - mEyeCircleRadius;
+ float leftEyeCenterY = arcBounds.centerY() - mEyeBallOffsetY + offsetY;
+
+ RectF rectF = new RectF(leftEyeCenterX - mEyeBallWidth / 2.0f, leftEyeCenterY - mEyeBallHeight / 2.0f,
+ leftEyeCenterX + mEyeBallWidth / 2.0f, leftEyeCenterY + mEyeBallHeight / 2.0f);
+
+ return rectF;
+ }
+
+ private RectF createRightEyeBall(RectF arcBounds, float offsetY) {
+ //the center of the right eye
+ float rightEyeCenterX = arcBounds.centerX() + mEyeInterval / 2.0f + mEyeCircleRadius;
+ float rightEyeCenterY = arcBounds.centerY() - mEyeBallOffsetY + offsetY;
+
+ RectF rectF = new RectF(rightEyeCenterX - mEyeBallWidth / 2.0f, rightEyeCenterY - mEyeBallHeight / 2.0f,
+ rightEyeCenterX + mEyeBallWidth / 2.0f, rightEyeCenterY + mEyeBallHeight / 2.0f);
+
+ return rectF;
+ }
+
+
+ private Path createLeftEyeCircle(RectF arcBounds, float offsetY) {
+ Path path = new Path();
+
+ //the center of the left eye
+ float leftEyeCenterX = arcBounds.centerX() - mEyeInterval / 2.0f - mEyeCircleRadius;
+ float leftEyeCenterY = arcBounds.centerY() + offsetY;
+ //the bounds of left eye
+ RectF leftEyeBounds = new RectF(leftEyeCenterX - mEyeCircleRadius, leftEyeCenterY - mEyeCircleRadius,
+ leftEyeCenterX + mEyeCircleRadius, leftEyeCenterY + mEyeCircleRadius);
+ path.addArc(leftEyeBounds, 0, DEGREE_180 + 15);
+ //the above radian of of the eye
+ path.quadTo(leftEyeBounds.left + mAboveRadianEyeOffsetX, leftEyeBounds.top + mEyeCircleRadius * 0.2f,
+ leftEyeBounds.left + mAboveRadianEyeOffsetX / 4.0f, leftEyeBounds.top - mEyeCircleRadius * 0.15f);
+
+ return path;
+ }
+
+ private Path createRightEyeCircle(RectF arcBounds, float offsetY) {
+ Path path = new Path();
+
+ //the center of the right eye
+ float rightEyeCenterX = arcBounds.centerX() + mEyeInterval / 2.0f + mEyeCircleRadius;
+ float rightEyeCenterY = arcBounds.centerY() + offsetY;
+ //the bounds of left eye
+ RectF leftEyeBounds = new RectF(rightEyeCenterX - mEyeCircleRadius, rightEyeCenterY - mEyeCircleRadius,
+ rightEyeCenterX + mEyeCircleRadius, rightEyeCenterY + mEyeCircleRadius);
+ path.addArc(leftEyeBounds, 180, -(DEGREE_180 + 15));
+ //the above radian of of the eye
+ path.quadTo(leftEyeBounds.right - mAboveRadianEyeOffsetX, leftEyeBounds.top + mEyeCircleRadius * 0.2f,
+ leftEyeBounds.right - mAboveRadianEyeOffsetX / 4.0f, leftEyeBounds.top - mEyeCircleRadius * 0.15f);
+
+ return path;
+ }
+
+ private class EyeCircleInterpolator implements Interpolator {
+
+ @Override
+ public float getInterpolation(float input) {
+ if (input < 0.25f) {
+ return input * 4.0f;
+ } else if (input < 0.5f) {
+ return 1.0f - (input - 0.25f) * 4.0f;
+ } else if (input < 0.75f) {
+ return (input - 0.5f) * 2.0f;
+ } else {
+ return 0.5f - (input - 0.75f) * 2.0f;
+ }
+
+ }
+ }
+
+ private class EyeBallInterpolator implements Interpolator {
+
+ @Override
+ public float getInterpolation(float input) {
+ if (input < 0.333333f) {
+ return input * 3.0f;
+ } else {
+ return 1.0f - (input - 0.333333f) * 1.5f;
+ }
+ }
+ }
+
+ public static class Builder {
+ private Context mContext;
+
+ public Builder(Context mContext) {
+ this.mContext = mContext;
+ }
+
+ public GhostsEyeLoadingRenderer build() {
+ GhostsEyeLoadingRenderer loadingRenderer = new GhostsEyeLoadingRenderer(mContext);
+ return loadingRenderer;
+ }
+ }
+}
diff --git a/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/circle/jump/CollisionLoadingRenderer.java b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/circle/jump/CollisionLoadingRenderer.java
new file mode 100644
index 00000000..9e49dcca
--- /dev/null
+++ b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/circle/jump/CollisionLoadingRenderer.java
@@ -0,0 +1,303 @@
+package app.dinus.com.loadingdrawable.render.circle.jump;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.graphics.Shader;
+import androidx.annotation.Size;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import app.dinus.com.loadingdrawable.DensityUtil;
+import app.dinus.com.loadingdrawable.render.LoadingRenderer;
+
+public class CollisionLoadingRenderer extends LoadingRenderer {
+ private static final Interpolator ACCELERATE_INTERPOLATOR = new AccelerateInterpolator();
+ private static final Interpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator();
+
+ private static final int MAX_ALPHA = 255;
+ private static final int OVAL_ALPHA = 64;
+
+ private static final int DEFAULT_BALL_COUNT = 7;
+
+ private static final float DEFAULT_OVAL_HEIGHT = 1.5f;
+ private static final float DEFAULT_BALL_RADIUS = 7.5f;
+ private static final float DEFAULT_WIDTH = 15.0f * 11;
+ private static final float DEFAULT_HEIGHT = 15.0f * 4;
+
+ private static final float START_LEFT_DURATION_OFFSET = 0.25f;
+ private static final float START_RIGHT_DURATION_OFFSET = 0.5f;
+ private static final float END_RIGHT_DURATION_OFFSET = 0.75f;
+ private static final float END_LEFT_DURATION_OFFSET = 1.0f;
+
+ private static final int[] DEFAULT_COLORS = new int[]{
+ Color.parseColor("#FF28435D"), Color.parseColor("#FFC32720")
+ };
+
+ private static final float[] DEFAULT_POSITIONS = new float[]{
+ 0.0f, 1.0f
+ };
+
+ private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final RectF mOvalRect = new RectF();
+
+ @Size(2)
+ private int[] mColors;
+ private float[] mPositions;
+
+ private float mOvalVerticalRadius;
+
+ private float mBallRadius;
+ private float mBallCenterY;
+ private float mBallSideOffsets;
+ private float mBallMoveXOffsets;
+ private float mBallQuadCoefficient;
+
+ private float mLeftBallMoveXOffsets;
+ private float mLeftBallMoveYOffsets;
+ private float mRightBallMoveXOffsets;
+ private float mRightBallMoveYOffsets;
+
+ private float mLeftOvalShapeRate;
+ private float mRightOvalShapeRate;
+
+ private int mBallCount;
+
+ private CollisionLoadingRenderer(Context context) {
+ super(context);
+ init(context);
+ adjustParams();
+ setupPaint();
+ }
+
+ private void init(Context context) {
+ mBallRadius = DensityUtil.dip2px(context, DEFAULT_BALL_RADIUS);
+ mWidth = DensityUtil.dip2px(context, DEFAULT_WIDTH);
+ mHeight = DensityUtil.dip2px(context, DEFAULT_HEIGHT);
+ mOvalVerticalRadius = DensityUtil.dip2px(context, DEFAULT_OVAL_HEIGHT);
+
+ mColors = DEFAULT_COLORS;
+ mPositions = DEFAULT_POSITIONS;
+
+ mBallCount = DEFAULT_BALL_COUNT;
+
+ //mBallMoveYOffsets = mBallQuadCoefficient * mBallMoveXOffsets ^ 2
+ // ==> if mBallMoveYOffsets == mBallMoveXOffsets
+ // ==> mBallQuadCoefficient = 1.0f / mBallMoveXOffsets;
+ mBallMoveXOffsets = 1.5f * (2 * mBallRadius);
+ mBallQuadCoefficient = 1.0f / mBallMoveXOffsets;
+ }
+
+ private void adjustParams() {
+ mBallCenterY = mHeight / 2.0f;
+ mBallSideOffsets = (mWidth - mBallRadius * 2.0f * (mBallCount - 2)) / 2;
+ }
+
+ private void setupPaint() {
+ mPaint.setStyle(Paint.Style.FILL);
+ mPaint.setShader(new LinearGradient(mBallSideOffsets, 0, mWidth - mBallSideOffsets, 0,
+ mColors, mPositions, Shader.TileMode.CLAMP));
+ }
+
+ @Override
+ protected void draw(Canvas canvas) {
+ int saveCount = canvas.save();
+
+ for (int i = 1; i < mBallCount - 1; i++) {
+ mPaint.setAlpha(MAX_ALPHA);
+ canvas.drawCircle(mBallRadius * (i * 2 - 1) + mBallSideOffsets, mBallCenterY, mBallRadius, mPaint);
+
+ mOvalRect.set(mBallRadius * (i * 2 - 2) + mBallSideOffsets, mHeight - mOvalVerticalRadius * 2,
+ mBallRadius * (i * 2) + mBallSideOffsets, mHeight);
+ mPaint.setAlpha(OVAL_ALPHA);
+ canvas.drawOval(mOvalRect, mPaint);
+ }
+
+ //draw the first ball
+ mPaint.setAlpha(MAX_ALPHA);
+ canvas.drawCircle(mBallSideOffsets - mBallRadius - mLeftBallMoveXOffsets,
+ mBallCenterY - mLeftBallMoveYOffsets, mBallRadius, mPaint);
+
+ mOvalRect.set(mBallSideOffsets - mBallRadius - mBallRadius * mLeftOvalShapeRate - mLeftBallMoveXOffsets,
+ mHeight - mOvalVerticalRadius - mOvalVerticalRadius * mLeftOvalShapeRate,
+ mBallSideOffsets - mBallRadius + mBallRadius * mLeftOvalShapeRate - mLeftBallMoveXOffsets,
+ mHeight - mOvalVerticalRadius + mOvalVerticalRadius * mLeftOvalShapeRate);
+ mPaint.setAlpha(OVAL_ALPHA);
+ canvas.drawOval(mOvalRect, mPaint);
+
+ //draw the last ball
+ mPaint.setAlpha(MAX_ALPHA);
+ canvas.drawCircle(mBallRadius * (mBallCount * 2 - 3) + mBallSideOffsets + mRightBallMoveXOffsets,
+ mBallCenterY - mRightBallMoveYOffsets, mBallRadius, mPaint);
+
+ mOvalRect.set(mBallRadius * (mBallCount * 2 - 3) - mBallRadius * mRightOvalShapeRate + mBallSideOffsets + mRightBallMoveXOffsets,
+ mHeight - mOvalVerticalRadius - mOvalVerticalRadius * mRightOvalShapeRate,
+ mBallRadius * (mBallCount * 2 - 3) + mBallRadius * mRightOvalShapeRate + mBallSideOffsets + mRightBallMoveXOffsets,
+ mHeight - mOvalVerticalRadius + mOvalVerticalRadius * mRightOvalShapeRate);
+ mPaint.setAlpha(OVAL_ALPHA);
+ canvas.drawOval(mOvalRect, mPaint);
+
+ canvas.restoreToCount(saveCount);
+ }
+
+ @Override
+ protected void computeRender(float renderProgress) {
+ // Moving the left ball to the left sides only occurs in the first 25% of a jump animation
+ if (renderProgress <= START_LEFT_DURATION_OFFSET) {
+ float startLeftOffsetProgress = renderProgress / START_LEFT_DURATION_OFFSET;
+ computeLeftBallMoveOffsets(DECELERATE_INTERPOLATOR.getInterpolation(startLeftOffsetProgress));
+ return;
+ }
+
+ // Moving the left ball to the origin location only occurs between 25% and 50% of a jump ring animation
+ if (renderProgress <= START_RIGHT_DURATION_OFFSET) {
+ float startRightOffsetProgress = (renderProgress - START_LEFT_DURATION_OFFSET) / (START_RIGHT_DURATION_OFFSET - START_LEFT_DURATION_OFFSET);
+ computeLeftBallMoveOffsets(ACCELERATE_INTERPOLATOR.getInterpolation(1.0f - startRightOffsetProgress));
+ return;
+ }
+
+ // Moving the right ball to the right sides only occurs between 50% and 75% of a jump animation
+ if (renderProgress <= END_RIGHT_DURATION_OFFSET) {
+ float endRightOffsetProgress = (renderProgress - START_RIGHT_DURATION_OFFSET) / (END_RIGHT_DURATION_OFFSET - START_RIGHT_DURATION_OFFSET);
+ computeRightBallMoveOffsets(DECELERATE_INTERPOLATOR.getInterpolation(endRightOffsetProgress));
+ return;
+ }
+
+ // Moving the right ball to the origin location only occurs after 75% of a jump animation
+ if (renderProgress <= END_LEFT_DURATION_OFFSET) {
+ float endRightOffsetProgress = (renderProgress - END_RIGHT_DURATION_OFFSET) / (END_LEFT_DURATION_OFFSET - END_RIGHT_DURATION_OFFSET);
+ computeRightBallMoveOffsets(ACCELERATE_INTERPOLATOR.getInterpolation(1 - endRightOffsetProgress));
+ return;
+ }
+
+ }
+
+ private void computeLeftBallMoveOffsets(float progress) {
+ mRightBallMoveXOffsets = 0.0f;
+ mRightBallMoveYOffsets = 0.0f;
+
+ mLeftOvalShapeRate = 1.0f - progress;
+ mLeftBallMoveXOffsets = mBallMoveXOffsets * progress;
+ mLeftBallMoveYOffsets = (float) (Math.pow(mLeftBallMoveXOffsets, 2) * mBallQuadCoefficient);
+ }
+
+ private void computeRightBallMoveOffsets(float progress) {
+ mLeftBallMoveXOffsets = 0.0f;
+ mLeftBallMoveYOffsets = 0.0f;
+
+ mRightOvalShapeRate = 1.0f - progress;
+ mRightBallMoveXOffsets = mBallMoveXOffsets * progress;
+ mRightBallMoveYOffsets = (float) (Math.pow(mRightBallMoveXOffsets, 2) * mBallQuadCoefficient);
+ }
+
+ @Override
+ protected void setAlpha(int alpha) {
+ mPaint.setAlpha(alpha);
+ }
+
+ @Override
+ protected void setColorFilter(ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+ }
+
+ @Override
+ protected void reset() {
+ }
+
+ private void apply(Builder builder) {
+ this.mWidth = builder.mWidth > 0 ? builder.mWidth : this.mWidth;
+ this.mHeight = builder.mHeight > 0 ? builder.mHeight : this.mHeight;
+
+ this.mOvalVerticalRadius = builder.mOvalVerticalRadius > 0 ? builder.mOvalVerticalRadius : this.mOvalVerticalRadius;
+ this.mBallRadius = builder.mBallRadius > 0 ? builder.mBallRadius : this.mBallRadius;
+ this.mBallMoveXOffsets = builder.mBallMoveXOffsets > 0 ? builder.mBallMoveXOffsets : this.mBallMoveXOffsets;
+ this.mBallQuadCoefficient = builder.mBallQuadCoefficient > 0 ? builder.mBallQuadCoefficient : this.mBallQuadCoefficient;
+ this.mBallCount = builder.mBallCount > 0 ? builder.mBallCount : this.mBallCount;
+
+ this.mDuration = builder.mDuration > 0 ? builder.mDuration : this.mDuration;
+
+ this.mColors = builder.mColors != null ? builder.mColors : this.mColors;
+
+ adjustParams();
+ setupPaint();
+ }
+
+ public static class Builder {
+ private Context mContext;
+
+ private int mWidth;
+ private int mHeight;
+
+ private float mOvalVerticalRadius;
+
+ private int mBallCount;
+ private float mBallRadius;
+ private float mBallMoveXOffsets;
+ private float mBallQuadCoefficient;
+
+ private int mDuration;
+
+ @Size(2)
+ private int[] mColors;
+
+ public Builder(Context mContext) {
+ this.mContext = mContext;
+ }
+
+ public Builder setWidth(int width) {
+ this.mWidth = width;
+ return this;
+ }
+
+ public Builder setHeight(int height) {
+ this.mHeight = height;
+ return this;
+ }
+
+ public Builder setOvalVerticalRadius(int ovalVerticalRadius) {
+ this.mOvalVerticalRadius = ovalVerticalRadius;
+ return this;
+ }
+
+ public Builder setBallRadius(int ballRadius) {
+ this.mBallRadius = ballRadius;
+ return this;
+ }
+
+ public Builder setBallMoveXOffsets(int ballMoveXOffsets) {
+ this.mBallMoveXOffsets = ballMoveXOffsets;
+ return this;
+ }
+
+ public Builder setBallQuadCoefficient(int ballQuadCoefficient) {
+ this.mBallQuadCoefficient = ballQuadCoefficient;
+ return this;
+ }
+
+ public Builder setBallCount(int ballCount) {
+ this.mBallCount = ballCount;
+ return this;
+ }
+
+ public Builder setColors(@Size(2) int[] colors) {
+ this.mColors = colors;
+ return this;
+ }
+
+ public Builder setDuration(int duration) {
+ this.mDuration = duration;
+ return this;
+ }
+
+ public CollisionLoadingRenderer build() {
+ CollisionLoadingRenderer loadingRenderer = new CollisionLoadingRenderer(mContext);
+ loadingRenderer.apply(this);
+ return loadingRenderer;
+ }
+ }
+}
diff --git a/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/circle/jump/DanceLoadingRenderer.java b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/circle/jump/DanceLoadingRenderer.java
new file mode 100644
index 00000000..aa5291d3
--- /dev/null
+++ b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/circle/jump/DanceLoadingRenderer.java
@@ -0,0 +1,339 @@
+package app.dinus.com.loadingdrawable.render.circle.jump;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
+
+import app.dinus.com.loadingdrawable.DensityUtil;
+import app.dinus.com.loadingdrawable.render.LoadingRenderer;
+
+public class DanceLoadingRenderer extends LoadingRenderer {
+ private static final Interpolator MATERIAL_INTERPOLATOR = new FastOutSlowInInterpolator();
+ private static final Interpolator ACCELERATE_INTERPOLATOR = new AccelerateInterpolator();
+ private static final Interpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator();
+
+ private static final long ANIMATION_DURATION = 1888;
+
+ private static final float DEFAULT_CENTER_RADIUS = 12.5f;
+ private static final float DEFAULT_STROKE_WIDTH = 1.5f;
+ private static final float DEFAULT_DANCE_BALL_RADIUS = 2.0f;
+
+ private static final int NUM_POINTS = 3;
+ private static final int DEGREE_360 = 360;
+ private static final int RING_START_ANGLE = -90;
+ private static final int DANCE_START_ANGLE = 0;
+ private static final int DANCE_INTERVAL_ANGLE = 60;
+
+ private static final int DEFAULT_COLOR = Color.WHITE;
+
+ //the center coordinate of the oval
+ private static final float[] POINT_X = new float[NUM_POINTS];
+ private static final float[] POINT_Y = new float[NUM_POINTS];
+ //1: the coordinate x from small to large; -1: the coordinate x from large to small
+ private static final int[] DIRECTION = new int[]{1, 1, -1};
+
+ private static final float BALL_FORWARD_START_ENTER_DURATION_OFFSET = 0f;
+ private static final float BALL_FORWARD_END_ENTER_DURATION_OFFSET = 0.125f;
+
+ private static final float RING_FORWARD_START_ROTATE_DURATION_OFFSET = 0.125f;
+ private static final float RING_FORWARD_END_ROTATE_DURATION_OFFSET = 0.375f;
+
+ private static final float CENTER_CIRCLE_FORWARD_START_SCALE_DURATION_OFFSET = 0.225f;
+ private static final float CENTER_CIRCLE_FORWARD_END_SCALE_DURATION_OFFSET = 0.475f;
+
+ private static final float BALL_FORWARD_START_EXIT_DURATION_OFFSET = 0.375f;
+ private static final float BALL_FORWARD_END_EXIT_DURATION_OFFSET = 0.54f;
+
+ private static final float RING_REVERSAL_START_ROTATE_DURATION_OFFSET = 0.5f;
+ private static final float RING_REVERSAL_END_ROTATE_DURATION_OFFSET = 0.75f;
+
+ private static final float BALL_REVERSAL_START_ENTER_DURATION_OFFSET = 0.6f;
+ private static final float BALL_REVERSAL_END_ENTER_DURATION_OFFSET = 0.725f;
+
+ private static final float CENTER_CIRCLE_REVERSAL_START_SCALE_DURATION_OFFSET = 0.675f;
+ private static final float CENTER_CIRCLE_REVERSAL_END_SCALE_DURATION_OFFSET = 0.875f;
+
+ private static final float BALL_REVERSAL_START_EXIT_DURATION_OFFSET = 0.875f;
+ private static final float BALL_REVERSAL_END_EXIT_DURATION_OFFSET = 1.0f;
+
+ private final Paint mPaint = new Paint();
+ private final RectF mTempBounds = new RectF();
+ private final RectF mCurrentBounds = new RectF();
+
+ private float mScale;
+ private float mRotation;
+ private float mStrokeInset;
+
+ private float mCenterRadius;
+ private float mStrokeWidth;
+ private float mDanceBallRadius;
+ private float mShapeChangeWidth;
+ private float mShapeChangeHeight;
+
+ private int mColor;
+ private int mArcColor;
+
+ private DanceLoadingRenderer(Context context) {
+ super(context);
+ init(context);
+ setupPaint();
+ }
+
+ private void init(Context context) {
+ mStrokeWidth = DensityUtil.dip2px(context, DEFAULT_STROKE_WIDTH);
+ mCenterRadius = DensityUtil.dip2px(context, DEFAULT_CENTER_RADIUS);
+ mDanceBallRadius = DensityUtil.dip2px(context, DEFAULT_DANCE_BALL_RADIUS);
+
+ setColor(DEFAULT_COLOR);
+ setInsets((int) mWidth, (int) mHeight);
+ mDuration = ANIMATION_DURATION;
+ }
+
+ private void setupPaint() {
+ mPaint.setAntiAlias(true);
+ mPaint.setStrokeWidth(mStrokeWidth);
+ mPaint.setStyle(Paint.Style.STROKE);
+ }
+
+ @Override
+ protected void draw(Canvas canvas, Rect bounds) {
+ int saveCount = canvas.save();
+
+ mTempBounds.set(bounds);
+ mTempBounds.inset(mStrokeInset, mStrokeInset);
+ mCurrentBounds.set(mTempBounds);
+
+ float outerCircleRadius = Math.min(mTempBounds.height(), mTempBounds.width()) / 2.0f;
+ float interCircleRadius = outerCircleRadius / 2.0f;
+ float centerRingWidth = interCircleRadius - mStrokeWidth / 2;
+
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setColor(mColor);
+ mPaint.setStrokeWidth(mStrokeWidth);
+ canvas.drawCircle(mTempBounds.centerX(), mTempBounds.centerY(), outerCircleRadius, mPaint);
+ mPaint.setStyle(Paint.Style.FILL);
+ canvas.drawCircle(mTempBounds.centerX(), mTempBounds.centerY(), interCircleRadius * mScale, mPaint);
+
+ if (mRotation != 0) {
+ mPaint.setColor(mArcColor);
+ mPaint.setStyle(Paint.Style.STROKE);
+ //strokeWidth / 2.0f + mStrokeWidth / 2.0f is the center of the inter circle width
+ mTempBounds.inset(centerRingWidth / 2.0f + mStrokeWidth / 2.0f, centerRingWidth / 2.0f + mStrokeWidth / 2.0f);
+ mPaint.setStrokeWidth(centerRingWidth);
+ canvas.drawArc(mTempBounds, RING_START_ANGLE, mRotation, false, mPaint);
+ }
+
+ mPaint.setColor(mColor);
+ mPaint.setStyle(Paint.Style.FILL);
+ for (int i = 0; i < NUM_POINTS; i++) {
+ canvas.rotate(i * DANCE_INTERVAL_ANGLE, POINT_X[i], POINT_Y[i]);
+ RectF rectF = new RectF(POINT_X[i] - mDanceBallRadius - mShapeChangeWidth / 2.0f,
+ POINT_Y[i] - mDanceBallRadius - mShapeChangeHeight / 2.0f,
+ POINT_X[i] + mDanceBallRadius + mShapeChangeWidth / 2.0f,
+ POINT_Y[i] + mDanceBallRadius + mShapeChangeHeight / 2.0f);
+ canvas.drawOval(rectF, mPaint);
+ canvas.rotate(-i * DANCE_INTERVAL_ANGLE, POINT_X[i], POINT_Y[i]);
+ }
+
+ canvas.restoreToCount(saveCount);
+ }
+
+ @Override
+ protected void computeRender(float renderProgress) {
+ float radius = Math.min(mCurrentBounds.height(), mCurrentBounds.width()) / 2.0f;
+ //the origin coordinate is the centerLeft of the field mCurrentBounds
+ float originCoordinateX = mCurrentBounds.left;
+ float originCoordinateY = mCurrentBounds.top + radius;
+
+ if (renderProgress <= BALL_FORWARD_END_ENTER_DURATION_OFFSET && renderProgress > BALL_FORWARD_START_ENTER_DURATION_OFFSET) {
+ final float ballForwardEnterProgress = (renderProgress - BALL_FORWARD_START_ENTER_DURATION_OFFSET) / (BALL_FORWARD_END_ENTER_DURATION_OFFSET - BALL_FORWARD_START_ENTER_DURATION_OFFSET);
+
+ mShapeChangeHeight = (0.5f - ballForwardEnterProgress) * mDanceBallRadius / 2.0f;
+ mShapeChangeWidth = -mShapeChangeHeight;
+ //y = k(x - r)--> k = tan(angle)
+ //(x - r)^2 + y^2 = r^2
+ // compute crossover point --> (k(x -r)) ^ 2 + (x - )^2 = r^2
+ // so x --> [r + r / sqrt(k ^ 2 + 1), r - r / sqrt(k ^ 2 + 1)]
+ for (int i = 0; i < NUM_POINTS; i++) {
+ float k = (float) Math.tan((DANCE_START_ANGLE + DANCE_INTERVAL_ANGLE * i) / 360.0f * (2.0f * Math.PI));
+ // progress[-1, 1]
+ float progress = (ACCELERATE_INTERPOLATOR.getInterpolation(ballForwardEnterProgress) / 2.0f - 0.5f) * 2.0f * DIRECTION[i];
+ POINT_X[i] = (float) (radius + progress * (radius / Math.sqrt(Math.pow(k, 2.0f) + 1.0f)));
+ POINT_Y[i] = k * (POINT_X[i] - radius);
+
+ POINT_X[i] += originCoordinateX;
+ POINT_Y[i] += originCoordinateY;
+ }
+ }
+
+ if (renderProgress <= RING_FORWARD_END_ROTATE_DURATION_OFFSET && renderProgress > RING_FORWARD_START_ROTATE_DURATION_OFFSET) {
+ final float forwardRotateProgress = (renderProgress - RING_FORWARD_START_ROTATE_DURATION_OFFSET) / (RING_FORWARD_END_ROTATE_DURATION_OFFSET - RING_FORWARD_START_ROTATE_DURATION_OFFSET);
+ mRotation = DEGREE_360 * MATERIAL_INTERPOLATOR.getInterpolation(forwardRotateProgress);
+ }
+
+ if (renderProgress <= CENTER_CIRCLE_FORWARD_END_SCALE_DURATION_OFFSET && renderProgress > CENTER_CIRCLE_FORWARD_START_SCALE_DURATION_OFFSET) {
+ final float centerCircleScaleProgress = (renderProgress - CENTER_CIRCLE_FORWARD_START_SCALE_DURATION_OFFSET) / (CENTER_CIRCLE_FORWARD_END_SCALE_DURATION_OFFSET - CENTER_CIRCLE_FORWARD_START_SCALE_DURATION_OFFSET);
+
+ if (centerCircleScaleProgress <= 0.5f) {
+ mScale = 1.0f + DECELERATE_INTERPOLATOR.getInterpolation(centerCircleScaleProgress * 2.0f) * 0.2f;
+ } else {
+ mScale = 1.2f - ACCELERATE_INTERPOLATOR.getInterpolation((centerCircleScaleProgress - 0.5f) * 2.0f) * 0.2f;
+ }
+
+ }
+
+ if (renderProgress <= BALL_FORWARD_END_EXIT_DURATION_OFFSET && renderProgress > BALL_FORWARD_START_EXIT_DURATION_OFFSET) {
+ final float ballForwardExitProgress = (renderProgress - BALL_FORWARD_START_EXIT_DURATION_OFFSET) / (BALL_FORWARD_END_EXIT_DURATION_OFFSET - BALL_FORWARD_START_EXIT_DURATION_OFFSET);
+ mShapeChangeHeight = (ballForwardExitProgress - 0.5f) * mDanceBallRadius / 2.0f;
+ mShapeChangeWidth = -mShapeChangeHeight;
+ for (int i = 0; i < NUM_POINTS; i++) {
+ float k = (float) Math.tan((DANCE_START_ANGLE + DANCE_INTERVAL_ANGLE * i) / 360.0f * (2.0f * Math.PI));
+ float progress = (DECELERATE_INTERPOLATOR.getInterpolation(ballForwardExitProgress) / 2.0f) * 2.0f * DIRECTION[i];
+ POINT_X[i] = (float) (radius + progress * (radius / Math.sqrt(Math.pow(k, 2.0f) + 1.0f)));
+ POINT_Y[i] = k * (POINT_X[i] - radius);
+
+ POINT_X[i] += originCoordinateX;
+ POINT_Y[i] += originCoordinateY;
+ }
+ }
+
+ if (renderProgress <= RING_REVERSAL_END_ROTATE_DURATION_OFFSET && renderProgress > RING_REVERSAL_START_ROTATE_DURATION_OFFSET) {
+ float scaledTime = (renderProgress - RING_REVERSAL_START_ROTATE_DURATION_OFFSET) / (RING_REVERSAL_END_ROTATE_DURATION_OFFSET - RING_REVERSAL_START_ROTATE_DURATION_OFFSET);
+ mRotation = DEGREE_360 * MATERIAL_INTERPOLATOR.getInterpolation(scaledTime) - 360;
+ } else if (renderProgress > RING_REVERSAL_END_ROTATE_DURATION_OFFSET) {
+ mRotation = 0.0f;
+ }
+
+ if (renderProgress <= BALL_REVERSAL_END_ENTER_DURATION_OFFSET && renderProgress > BALL_REVERSAL_START_ENTER_DURATION_OFFSET) {
+ final float ballReversalEnterProgress = (renderProgress - BALL_REVERSAL_START_ENTER_DURATION_OFFSET) / (BALL_REVERSAL_END_ENTER_DURATION_OFFSET - BALL_REVERSAL_START_ENTER_DURATION_OFFSET);
+ mShapeChangeHeight = (0.5f - ballReversalEnterProgress) * mDanceBallRadius / 2.0f;
+ mShapeChangeWidth = -mShapeChangeHeight;
+
+ for (int i = 0; i < NUM_POINTS; i++) {
+ float k = (float) Math.tan((DANCE_START_ANGLE + DANCE_INTERVAL_ANGLE * i) / 360.0f * (2.0f * Math.PI));
+ float progress = (0.5f - ACCELERATE_INTERPOLATOR.getInterpolation(ballReversalEnterProgress) / 2.0f) * 2.0f * DIRECTION[i];
+ POINT_X[i] = (float) (radius + progress * (radius / Math.sqrt(Math.pow(k, 2.0f) + 1.0f)));
+ POINT_Y[i] = k * (POINT_X[i] - radius);
+
+ POINT_X[i] += originCoordinateX;
+ POINT_Y[i] += originCoordinateY;
+ }
+ }
+
+ if (renderProgress <= CENTER_CIRCLE_REVERSAL_END_SCALE_DURATION_OFFSET && renderProgress > CENTER_CIRCLE_REVERSAL_START_SCALE_DURATION_OFFSET) {
+ final float centerCircleScaleProgress = (renderProgress - CENTER_CIRCLE_REVERSAL_START_SCALE_DURATION_OFFSET) / (CENTER_CIRCLE_REVERSAL_END_SCALE_DURATION_OFFSET - CENTER_CIRCLE_REVERSAL_START_SCALE_DURATION_OFFSET);
+
+ if (centerCircleScaleProgress <= 0.5f) {
+ mScale = 1.0f + DECELERATE_INTERPOLATOR.getInterpolation(centerCircleScaleProgress * 2.0f) * 0.2f;
+ } else {
+ mScale = 1.2f - ACCELERATE_INTERPOLATOR.getInterpolation((centerCircleScaleProgress - 0.5f) * 2.0f) * 0.2f;
+ }
+
+ }
+
+ if (renderProgress <= BALL_REVERSAL_END_EXIT_DURATION_OFFSET && renderProgress > BALL_REVERSAL_START_EXIT_DURATION_OFFSET) {
+ final float ballReversalExitProgress = (renderProgress - BALL_REVERSAL_START_EXIT_DURATION_OFFSET) / (BALL_REVERSAL_END_EXIT_DURATION_OFFSET - BALL_REVERSAL_START_EXIT_DURATION_OFFSET);
+ mShapeChangeHeight = (ballReversalExitProgress - 0.5f) * mDanceBallRadius / 2.0f;
+ mShapeChangeWidth = -mShapeChangeHeight;
+
+ for (int i = 0; i < NUM_POINTS; i++) {
+ float k = (float) Math.tan((DANCE_START_ANGLE + DANCE_INTERVAL_ANGLE * i) / 360.0f * (2.0f * Math.PI));
+ float progress = (0.0f - DECELERATE_INTERPOLATOR.getInterpolation(ballReversalExitProgress) / 2.0f) * 2.0f * DIRECTION[i];
+ POINT_X[i] = (float) (radius + progress * (radius / Math.sqrt(Math.pow(k, 2.0f) + 1.0f)));
+ POINT_Y[i] = k * (POINT_X[i] - radius);
+
+ POINT_X[i] += originCoordinateX;
+ POINT_Y[i] += originCoordinateY;
+ }
+ }
+ }
+
+ @Override
+ protected void setAlpha(int alpha) {
+ mPaint.setAlpha(alpha);
+
+ }
+
+ @Override
+ protected void setColorFilter(ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+
+ }
+
+ @Override
+ protected void reset() {
+ mScale = 1.0f;
+ mRotation = 0;
+ }
+
+ private void setColor(int color) {
+ mColor = color;
+ mArcColor = halfAlphaColor(mColor);
+ }
+
+ private void setRotation(float rotation) {
+ mRotation = rotation;
+
+ }
+
+ private void setDanceBallRadius(float danceBallRadius) {
+ this.mDanceBallRadius = danceBallRadius;
+
+ }
+
+ private float getDanceBallRadius() {
+ return mDanceBallRadius;
+ }
+
+ private float getRotation() {
+ return mRotation;
+ }
+
+ private void setInsets(int width, int height) {
+ final float minEdge = (float) Math.min(width, height);
+ float insets;
+ if (mCenterRadius <= 0 || minEdge < 0) {
+ insets = (float) Math.ceil(mStrokeWidth / 2.0f);
+ } else {
+ insets = minEdge / 2.0f - mCenterRadius;
+ }
+ mStrokeInset = insets;
+ }
+
+ private int halfAlphaColor(int colorValue) {
+ int startA = (colorValue >> 24) & 0xff;
+ int startR = (colorValue >> 16) & 0xff;
+ int startG = (colorValue >> 8) & 0xff;
+ int startB = colorValue & 0xff;
+
+ return ((startA / 2) << 24)
+ | (startR << 16)
+ | (startG << 8)
+ | startB;
+ }
+
+ public static class Builder {
+ private Context mContext;
+
+ public Builder(Context mContext) {
+ this.mContext = mContext;
+ }
+
+ public DanceLoadingRenderer build() {
+ DanceLoadingRenderer loadingRenderer = new DanceLoadingRenderer(mContext);
+ return loadingRenderer;
+ }
+ }
+}
diff --git a/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/circle/jump/GuardLoadingRenderer.java b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/circle/jump/GuardLoadingRenderer.java
new file mode 100644
index 00000000..32cac092
--- /dev/null
+++ b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/circle/jump/GuardLoadingRenderer.java
@@ -0,0 +1,260 @@
+package app.dinus.com.loadingdrawable.render.circle.jump;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PathMeasure;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.DisplayMetrics;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
+
+import app.dinus.com.loadingdrawable.DensityUtil;
+import app.dinus.com.loadingdrawable.render.LoadingRenderer;
+
+public class GuardLoadingRenderer extends LoadingRenderer {
+ private static final Interpolator MATERIAL_INTERPOLATOR = new FastOutSlowInInterpolator();
+ private static final Interpolator ACCELERATE_INTERPOLATOR = new AccelerateInterpolator();
+ private static final Interpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator();
+
+ private static final long ANIMATION_DURATION = 5000;
+
+ private static final float DEFAULT_STROKE_WIDTH = 1.0f;
+ private static final float DEFAULT_CENTER_RADIUS = 12.5f;
+ private static final float DEFAULT_SKIP_BALL_RADIUS = 1.0f;
+
+ private static final float START_TRIM_INIT_ROTATION = -0.5f;
+ private static final float START_TRIM_MAX_ROTATION = -0.25f;
+ private static final float END_TRIM_INIT_ROTATION = 0.25f;
+ private static final float END_TRIM_MAX_ROTATION = 0.75f;
+
+ private static final float START_TRIM_DURATION_OFFSET = 0.23f;
+ private static final float WAVE_DURATION_OFFSET = 0.36f;
+ private static final float BALL_SKIP_DURATION_OFFSET = 0.74f;
+ private static final float BALL_SCALE_DURATION_OFFSET = 0.82f;
+ private static final float END_TRIM_DURATION_OFFSET = 1.0f;
+
+ private static final int DEFAULT_COLOR = Color.WHITE;
+ private static final int DEFAULT_BALL_COLOR = Color.RED;
+
+ private final Paint mPaint = new Paint();
+ private final RectF mTempBounds = new RectF();
+ private final RectF mCurrentBounds = new RectF();
+ private final float[] mCurrentPosition = new float[2];
+
+ private float mStrokeInset;
+ private float mSkipBallSize;
+
+ private float mScale;
+ private float mEndTrim;
+ private float mRotation;
+ private float mStartTrim;
+ private float mWaveProgress;
+
+ private float mStrokeWidth;
+ private float mCenterRadius;
+
+ private int mColor;
+ private int mBallColor;
+
+ private PathMeasure mPathMeasure;
+
+ private GuardLoadingRenderer(Context context) {
+ super(context);
+
+ mDuration = ANIMATION_DURATION;
+ init(context);
+ setupPaint();
+ }
+
+ private void init(Context context) {
+ mStrokeWidth = DensityUtil.dip2px(context, DEFAULT_STROKE_WIDTH);
+ mCenterRadius = DensityUtil.dip2px(context, DEFAULT_CENTER_RADIUS);
+ mSkipBallSize = DensityUtil.dip2px(context, DEFAULT_SKIP_BALL_RADIUS);
+
+ mColor = DEFAULT_COLOR;
+ mBallColor = DEFAULT_BALL_COLOR;
+ }
+
+ private void setupPaint() {
+ mPaint.setAntiAlias(true);
+ mPaint.setStrokeWidth(mStrokeWidth);
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setStrokeCap(Paint.Cap.ROUND);
+
+ setInsets((int) mWidth, (int) mHeight);
+ }
+
+ @Override
+ protected void draw(Canvas canvas, Rect bounds) {
+ RectF arcBounds = mTempBounds;
+ arcBounds.set(bounds);
+ arcBounds.inset(mStrokeInset, mStrokeInset);
+ mCurrentBounds.set(arcBounds);
+
+ int saveCount = canvas.save();
+
+ //draw circle trim
+ float startAngle = (mStartTrim + mRotation) * 360;
+ float endAngle = (mEndTrim + mRotation) * 360;
+ float sweepAngle = endAngle - startAngle;
+ if (sweepAngle != 0) {
+ mPaint.setColor(mColor);
+ mPaint.setStyle(Paint.Style.STROKE);
+ canvas.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint);
+ }
+
+ //draw water wave
+ if (mWaveProgress < 1.0f) {
+ mPaint.setColor(Color.argb((int) (Color.alpha(mColor) * (1.0f - mWaveProgress)),
+ Color.red(mColor), Color.green(mColor), Color.blue(mColor)));
+ mPaint.setStyle(Paint.Style.STROKE);
+ float radius = Math.min(arcBounds.width(), arcBounds.height()) / 2.0f;
+ canvas.drawCircle(arcBounds.centerX(), arcBounds.centerY(), radius * (1.0f + mWaveProgress), mPaint);
+ }
+ //draw ball bounce
+ if (mPathMeasure != null) {
+ mPaint.setColor(mBallColor);
+ mPaint.setStyle(Paint.Style.FILL);
+ canvas.drawCircle(mCurrentPosition[0], mCurrentPosition[1], mSkipBallSize * mScale, mPaint);
+ }
+
+ canvas.restoreToCount(saveCount);
+ }
+
+ @Override
+ protected void computeRender(float renderProgress) {
+ if (renderProgress <= START_TRIM_DURATION_OFFSET) {
+ final float startTrimProgress = (renderProgress) / START_TRIM_DURATION_OFFSET;
+ mEndTrim = -MATERIAL_INTERPOLATOR.getInterpolation(startTrimProgress);
+ mRotation = START_TRIM_INIT_ROTATION + START_TRIM_MAX_ROTATION
+ * MATERIAL_INTERPOLATOR.getInterpolation(startTrimProgress);
+ }
+
+ if (renderProgress <= WAVE_DURATION_OFFSET && renderProgress > START_TRIM_DURATION_OFFSET) {
+ final float waveProgress = (renderProgress - START_TRIM_DURATION_OFFSET)
+ / (WAVE_DURATION_OFFSET - START_TRIM_DURATION_OFFSET);
+ mWaveProgress = ACCELERATE_INTERPOLATOR.getInterpolation(waveProgress);
+ }
+
+ if (renderProgress <= BALL_SKIP_DURATION_OFFSET && renderProgress > WAVE_DURATION_OFFSET) {
+ if (mPathMeasure == null) {
+ mPathMeasure = new PathMeasure(createSkipBallPath(), false);
+ }
+
+ final float ballSkipProgress = (renderProgress - WAVE_DURATION_OFFSET)
+ / (BALL_SKIP_DURATION_OFFSET - WAVE_DURATION_OFFSET);
+ mPathMeasure.getPosTan(ballSkipProgress * mPathMeasure.getLength(), mCurrentPosition, null);
+
+ mWaveProgress = 1.0f;
+ }
+
+ if (renderProgress <= BALL_SCALE_DURATION_OFFSET && renderProgress > BALL_SKIP_DURATION_OFFSET) {
+ final float ballScaleProgress =
+ (renderProgress - BALL_SKIP_DURATION_OFFSET)
+ / (BALL_SCALE_DURATION_OFFSET - BALL_SKIP_DURATION_OFFSET);
+ if (ballScaleProgress < 0.5f) {
+ mScale = 1.0f + DECELERATE_INTERPOLATOR.getInterpolation(ballScaleProgress * 2.0f);
+ } else {
+ mScale = 2.0f - ACCELERATE_INTERPOLATOR.getInterpolation((ballScaleProgress - 0.5f) * 2.0f) * 2.0f;
+ }
+ }
+
+ if (renderProgress >= BALL_SCALE_DURATION_OFFSET) {
+ final float endTrimProgress =
+ (renderProgress - BALL_SKIP_DURATION_OFFSET)
+ / (END_TRIM_DURATION_OFFSET - BALL_SKIP_DURATION_OFFSET);
+ mEndTrim = -1 + MATERIAL_INTERPOLATOR.getInterpolation(endTrimProgress);
+ mRotation = END_TRIM_INIT_ROTATION + END_TRIM_MAX_ROTATION
+ * MATERIAL_INTERPOLATOR.getInterpolation(endTrimProgress);
+
+ mScale = 1.0f;
+ mPathMeasure = null;
+ }
+ }
+
+ @Override
+ protected void setAlpha(int alpha) {
+ mPaint.setAlpha(alpha);
+
+ }
+
+ @Override
+ protected void setColorFilter(ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+
+ }
+
+ @Override
+ protected void reset() {
+ mScale = 1.0f;
+ mEndTrim = 0.0f;
+ mRotation = 0.0f;
+ mStartTrim = 0.0f;
+ mWaveProgress = 1.0f;
+ }
+
+ private Path createSkipBallPath() {
+ float radius = Math.min(mCurrentBounds.width(), mCurrentBounds.height()) / 2.0f;
+ float radiusPow2 = (float) Math.pow(radius, 2.0f);
+ float originCoordinateX = mCurrentBounds.centerX();
+ float originCoordinateY = mCurrentBounds.centerY();
+
+ float[] coordinateX = new float[]{0.0f, 0.0f, -0.8f * radius, 0.75f * radius,
+ -0.45f * radius, 0.9f * radius, -0.5f * radius};
+ float[] sign = new float[]{1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f};
+
+ Path path = new Path();
+ for (int i = 0; i < coordinateX.length; i++) {
+ // x^2 + y^2 = radius^2 --> y = sqrt(radius^2 - x^2)
+ if (i == 0) {
+ path.moveTo(
+ originCoordinateX + coordinateX[i],
+ originCoordinateY + sign[i]
+ * (float) Math.sqrt(radiusPow2 - Math.pow(coordinateX[i], 2.0f)));
+ continue;
+ }
+
+ path.lineTo(
+ originCoordinateX + coordinateX[i],
+ originCoordinateY + sign[i]
+ * (float) Math.sqrt(radiusPow2 - Math.pow(coordinateX[i], 2.0f)));
+
+ if (i == coordinateX.length - 1) {
+ path.lineTo(originCoordinateX, originCoordinateY);
+ }
+ }
+ return path;
+ }
+
+ private void setInsets(int width, int height) {
+ final float minEdge = (float) Math.min(width, height);
+ float insets;
+ if (mCenterRadius <= 0 || minEdge < 0) {
+ insets = (float) Math.ceil(mStrokeWidth / 2.0f);
+ } else {
+ insets = minEdge / 2.0f - mCenterRadius;
+ }
+ mStrokeInset = insets;
+ }
+
+ public static class Builder {
+ private Context mContext;
+
+ public Builder(Context mContext) {
+ this.mContext = mContext;
+ }
+
+ public GuardLoadingRenderer build() {
+ GuardLoadingRenderer loadingRenderer = new GuardLoadingRenderer(mContext);
+ return loadingRenderer;
+ }
+ }
+}
diff --git a/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/circle/jump/SwapLoadingRenderer.java b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/circle/jump/SwapLoadingRenderer.java
new file mode 100644
index 00000000..c759723d
--- /dev/null
+++ b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/circle/jump/SwapLoadingRenderer.java
@@ -0,0 +1,227 @@
+package app.dinus.com.loadingdrawable.render.circle.jump;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import app.dinus.com.loadingdrawable.DensityUtil;
+import app.dinus.com.loadingdrawable.render.LoadingRenderer;
+
+public class SwapLoadingRenderer extends LoadingRenderer {
+ private static final Interpolator ACCELERATE_DECELERATE_INTERPOLATOR = new AccelerateDecelerateInterpolator();
+
+ private static final long ANIMATION_DURATION = 2500;
+
+ private static final int DEFAULT_CIRCLE_COUNT = 5;
+
+ private static final float DEFAULT_BALL_RADIUS = 7.5f;
+ private static final float DEFAULT_WIDTH = 15.0f * 11;
+ private static final float DEFAULT_HEIGHT = 15.0f * 5;
+ private static final float DEFAULT_STROKE_WIDTH = 1.5f;
+
+ private static final int DEFAULT_COLOR = Color.WHITE;
+
+ private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ private int mColor;
+
+ private int mSwapIndex;
+ private int mBallCount;
+
+ private float mBallSideOffsets;
+ private float mBallCenterY;
+ private float mBallRadius;
+ private float mBallInterval;
+ private float mSwapBallOffsetX;
+ private float mSwapBallOffsetY;
+ private float mASwapThreshold;
+
+ private float mStrokeWidth;
+
+ private SwapLoadingRenderer(Context context) {
+ super(context);
+
+ init(context);
+ adjustParams();
+ setupPaint();
+ }
+
+ private void init(Context context) {
+ mWidth = DensityUtil.dip2px(context, DEFAULT_WIDTH);
+ mHeight = DensityUtil.dip2px(context, DEFAULT_HEIGHT);
+ mBallRadius = DensityUtil.dip2px(context, DEFAULT_BALL_RADIUS);
+ mStrokeWidth = DensityUtil.dip2px(context, DEFAULT_STROKE_WIDTH);
+
+ mColor = DEFAULT_COLOR;
+ mDuration = ANIMATION_DURATION;
+ mBallCount = DEFAULT_CIRCLE_COUNT;
+
+ mBallInterval = mBallRadius;
+ }
+
+ private void adjustParams() {
+ mBallCenterY = mHeight / 2.0f;
+ mBallSideOffsets = (mWidth - mBallRadius * 2 * mBallCount - mBallInterval * (mBallCount - 1)) / 2.0f;
+
+ mASwapThreshold = 1.0f / mBallCount;
+ }
+
+ private void setupPaint() {
+ mPaint.setColor(mColor);
+ mPaint.setStrokeWidth(mStrokeWidth);
+ }
+
+ @Override
+ protected void draw(Canvas canvas) {
+ int saveCount = canvas.save();
+
+ for (int i = 0; i < mBallCount; i++) {
+ if (i == mSwapIndex) {
+ mPaint.setStyle(Paint.Style.FILL);
+ canvas.drawCircle(mBallSideOffsets + mBallRadius * (i * 2 + 1) + i * mBallInterval + mSwapBallOffsetX
+ , mBallCenterY - mSwapBallOffsetY, mBallRadius, mPaint);
+ } else if (i == (mSwapIndex + 1) % mBallCount) {
+ mPaint.setStyle(Paint.Style.STROKE);
+ canvas.drawCircle(mBallSideOffsets + mBallRadius * (i * 2 + 1) + i * mBallInterval - mSwapBallOffsetX
+ , mBallCenterY + mSwapBallOffsetY, mBallRadius - mStrokeWidth / 2, mPaint);
+ } else {
+ mPaint.setStyle(Paint.Style.STROKE);
+ canvas.drawCircle(mBallSideOffsets + mBallRadius * (i * 2 + 1) + i * mBallInterval, mBallCenterY
+ , mBallRadius - mStrokeWidth / 2, mPaint);
+ }
+ }
+
+ canvas.restoreToCount(saveCount);
+ }
+
+ @Override
+ protected void computeRender(float renderProgress) {
+ mSwapIndex = (int) (renderProgress / mASwapThreshold);
+
+ // Swap trace : x^2 + y^2 = r ^ 2
+ float swapTraceProgress = ACCELERATE_DECELERATE_INTERPOLATOR.getInterpolation(
+ (renderProgress - mSwapIndex * mASwapThreshold) / mASwapThreshold);
+
+ float swapTraceRadius = mSwapIndex == mBallCount - 1
+ ? (mBallRadius * 2 * (mBallCount - 1) + mBallInterval * (mBallCount - 1)) / 2
+ : (mBallRadius * 2 + mBallInterval) / 2;
+
+ // Calculate the X offset of the swap ball
+ mSwapBallOffsetX = mSwapIndex == mBallCount - 1
+ ? -swapTraceProgress * swapTraceRadius * 2
+ : swapTraceProgress * swapTraceRadius * 2;
+
+ // if mSwapIndex == mBallCount - 1 then (swapTraceRadius, swapTraceRadius) as the origin of coordinates
+ // else (-swapTraceRadius, -swapTraceRadius) as the origin of coordinates
+ float xCoordinate = mSwapIndex == mBallCount - 1
+ ? mSwapBallOffsetX + swapTraceRadius
+ : mSwapBallOffsetX - swapTraceRadius;
+
+ // Calculate the Y offset of the swap ball
+ mSwapBallOffsetY = (float) (mSwapIndex % 2 == 0 && mSwapIndex != mBallCount - 1
+ ? Math.sqrt(Math.pow(swapTraceRadius, 2.0f) - Math.pow(xCoordinate, 2.0f))
+ : -Math.sqrt(Math.pow(swapTraceRadius, 2.0f) - Math.pow(xCoordinate, 2.0f)));
+
+ }
+
+ @Override
+ protected void setAlpha(int alpha) {
+ mPaint.setAlpha(alpha);
+ }
+
+ @Override
+ protected void setColorFilter(ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+ }
+
+ @Override
+ protected void reset() {
+ }
+
+ private void apply(Builder builder) {
+ this.mWidth = builder.mWidth > 0 ? builder.mWidth : this.mWidth;
+ this.mHeight = builder.mHeight > 0 ? builder.mHeight : this.mHeight;
+ this.mStrokeWidth = builder.mStrokeWidth > 0 ? builder.mStrokeWidth : this.mStrokeWidth;
+
+ this.mBallRadius = builder.mBallRadius > 0 ? builder.mBallRadius : this.mBallRadius;
+ this.mBallInterval = builder.mBallInterval > 0 ? builder.mBallInterval : this.mBallInterval;
+ this.mBallCount = builder.mBallCount > 0 ? builder.mBallCount : this.mBallCount;
+
+ this.mColor = builder.mColor != 0 ? builder.mColor : this.mColor;
+
+ this.mDuration = builder.mDuration > 0 ? builder.mDuration : this.mDuration;
+
+ adjustParams();
+ setupPaint();
+ }
+
+ public static class Builder {
+ private Context mContext;
+
+ private int mWidth;
+ private int mHeight;
+ private int mStrokeWidth;
+
+ private int mBallCount;
+ private int mBallRadius;
+ private int mBallInterval;
+
+ private int mDuration;
+
+ private int mColor;
+
+ public Builder(Context mContext) {
+ this.mContext = mContext;
+ }
+
+ public Builder setWidth(int width) {
+ this.mWidth = width;
+ return this;
+ }
+
+ public Builder setHeight(int height) {
+ this.mHeight = height;
+ return this;
+ }
+
+ public Builder setStrokeWidth(int strokeWidth) {
+ this.mStrokeWidth = strokeWidth;
+ return this;
+ }
+
+ public Builder setBallRadius(int ballRadius) {
+ this.mBallRadius = ballRadius;
+ return this;
+ }
+
+ public Builder setBallInterval(int ballInterval) {
+ this.mBallInterval = ballInterval;
+ return this;
+ }
+
+ public Builder setBallCount(int ballCount) {
+ this.mBallCount = ballCount;
+ return this;
+ }
+
+ public Builder setColor(int color) {
+ this.mColor = color;
+ return this;
+ }
+
+ public Builder setDuration(int duration) {
+ this.mDuration = duration;
+ return this;
+ }
+
+ public SwapLoadingRenderer build() {
+ SwapLoadingRenderer loadingRenderer = new SwapLoadingRenderer(mContext);
+ loadingRenderer.apply(this);
+ return loadingRenderer;
+ }
+ }
+}
diff --git a/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/circle/rotate/GearLoadingRenderer.java b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/circle/rotate/GearLoadingRenderer.java
new file mode 100644
index 00000000..58274f54
--- /dev/null
+++ b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/circle/rotate/GearLoadingRenderer.java
@@ -0,0 +1,288 @@
+package app.dinus.com.loadingdrawable.render.circle.rotate;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import androidx.annotation.IntRange;
+
+import app.dinus.com.loadingdrawable.DensityUtil;
+import app.dinus.com.loadingdrawable.render.LoadingRenderer;
+
+public class GearLoadingRenderer extends LoadingRenderer {
+ private static final Interpolator ACCELERATE_INTERPOLATOR = new AccelerateInterpolator();
+ private static final Interpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator();
+
+ private static final int GEAR_COUNT = 4;
+ private static final int NUM_POINTS = 3;
+ private static final int MAX_ALPHA = 255;
+ private static final int DEGREE_360 = 360;
+
+ private static final int DEFAULT_GEAR_SWIPE_DEGREES = 60;
+
+ private static final float FULL_GROUP_ROTATION = 3.0f * DEGREE_360;
+
+ private static final float START_SCALE_DURATION_OFFSET = 0.3f;
+ private static final float START_TRIM_DURATION_OFFSET = 0.5f;
+ private static final float END_TRIM_DURATION_OFFSET = 0.7f;
+ private static final float END_SCALE_DURATION_OFFSET = 1.0f;
+
+ private static final float DEFAULT_CENTER_RADIUS = 12.5f;
+ private static final float DEFAULT_STROKE_WIDTH = 2.5f;
+
+ private static final int DEFAULT_COLOR = Color.WHITE;
+
+ private final Paint mPaint = new Paint();
+ private final RectF mTempBounds = new RectF();
+
+ private final Animator.AnimatorListener mAnimatorListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationRepeat(Animator animator) {
+ super.onAnimationRepeat(animator);
+ storeOriginals();
+
+ mStartDegrees = mEndDegrees;
+ mRotationCount = (mRotationCount + 1) % NUM_POINTS;
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ super.onAnimationStart(animation);
+ mRotationCount = 0;
+ }
+ };
+
+ private int mColor;
+
+ private int mGearCount;
+ private int mGearSwipeDegrees;
+
+ private float mStrokeInset;
+
+ private float mRotationCount;
+ private float mGroupRotation;
+
+ private float mScale;
+ private float mEndDegrees;
+ private float mStartDegrees;
+ private float mSwipeDegrees;
+ private float mOriginEndDegrees;
+ private float mOriginStartDegrees;
+
+ private float mStrokeWidth;
+ private float mCenterRadius;
+
+ private GearLoadingRenderer(Context context) {
+ super(context);
+
+ init(context);
+ setupPaint();
+ addRenderListener(mAnimatorListener);
+ }
+
+ private void init(Context context) {
+ mStrokeWidth = DensityUtil.dip2px(context, DEFAULT_STROKE_WIDTH);
+ mCenterRadius = DensityUtil.dip2px(context, DEFAULT_CENTER_RADIUS);
+
+ mColor = DEFAULT_COLOR;
+
+ mGearCount = GEAR_COUNT;
+ mGearSwipeDegrees = DEFAULT_GEAR_SWIPE_DEGREES;
+ }
+
+ private void setupPaint() {
+ mPaint.setAntiAlias(true);
+ mPaint.setStrokeWidth(mStrokeWidth);
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setStrokeCap(Paint.Cap.ROUND);
+
+ initStrokeInset(mWidth, mHeight);
+ }
+
+ @Override
+ protected void draw(Canvas canvas) {
+ int saveCount = canvas.save();
+
+ mTempBounds.set(mBounds);
+ mTempBounds.inset(mStrokeInset, mStrokeInset);
+ mTempBounds.inset(mTempBounds.width() * (1.0f - mScale) / 2.0f, mTempBounds.width() * (1.0f - mScale) / 2.0f);
+
+ canvas.rotate(mGroupRotation, mTempBounds.centerX(), mTempBounds.centerY());
+
+ mPaint.setColor(mColor);
+ mPaint.setAlpha((int) (MAX_ALPHA * mScale));
+ mPaint.setStrokeWidth(mStrokeWidth * mScale);
+
+ if (mSwipeDegrees != 0) {
+ for (int i = 0; i < mGearCount; i++) {
+ canvas.drawArc(mTempBounds, mStartDegrees + DEGREE_360 / mGearCount * i, mSwipeDegrees, false, mPaint);
+ }
+ }
+
+ canvas.restoreToCount(saveCount);
+ }
+
+ @Override
+ protected void computeRender(float renderProgress) {
+ // Scaling up the start size only occurs in the first 20% of a single ring animation
+ if (renderProgress <= START_SCALE_DURATION_OFFSET) {
+ float startScaleProgress = (renderProgress) / START_SCALE_DURATION_OFFSET;
+ mScale = DECELERATE_INTERPOLATOR.getInterpolation(startScaleProgress);
+ }
+
+ // Moving the start trim only occurs between 20% to 50% of a single ring animation
+ if (renderProgress <= START_TRIM_DURATION_OFFSET && renderProgress > START_SCALE_DURATION_OFFSET) {
+ float startTrimProgress = (renderProgress - START_SCALE_DURATION_OFFSET) / (START_TRIM_DURATION_OFFSET - START_SCALE_DURATION_OFFSET);
+ mStartDegrees = mOriginStartDegrees + mGearSwipeDegrees * startTrimProgress;
+ }
+
+ // Moving the end trim starts between 50% to 80% of a single ring animation
+ if (renderProgress <= END_TRIM_DURATION_OFFSET && renderProgress > START_TRIM_DURATION_OFFSET) {
+ float endTrimProgress = (renderProgress - START_TRIM_DURATION_OFFSET) / (END_TRIM_DURATION_OFFSET - START_TRIM_DURATION_OFFSET);
+ mEndDegrees = mOriginEndDegrees + mGearSwipeDegrees * endTrimProgress;
+ }
+
+ // Scaling down the end size starts after 80% of a single ring animation
+ if (renderProgress > END_TRIM_DURATION_OFFSET) {
+ float endScaleProgress = (renderProgress - END_TRIM_DURATION_OFFSET) / (END_SCALE_DURATION_OFFSET - END_TRIM_DURATION_OFFSET);
+ mScale = 1.0f - ACCELERATE_INTERPOLATOR.getInterpolation(endScaleProgress);
+ }
+
+ if (renderProgress <= END_TRIM_DURATION_OFFSET && renderProgress > START_SCALE_DURATION_OFFSET) {
+ float rotateProgress = (renderProgress - START_SCALE_DURATION_OFFSET) / (END_TRIM_DURATION_OFFSET - START_SCALE_DURATION_OFFSET);
+ mGroupRotation = ((FULL_GROUP_ROTATION / NUM_POINTS) * rotateProgress) + (FULL_GROUP_ROTATION * (mRotationCount / NUM_POINTS));
+ }
+
+ if (Math.abs(mEndDegrees - mStartDegrees) > 0) {
+ mSwipeDegrees = mEndDegrees - mStartDegrees;
+ }
+ }
+
+ @Override
+ protected void setAlpha(int alpha) {
+ mPaint.setAlpha(alpha);
+ }
+
+ @Override
+ protected void setColorFilter(ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+ }
+
+ @Override
+ protected void reset() {
+ resetOriginals();
+ }
+
+ private void initStrokeInset(float width, float height) {
+ float minSize = Math.min(width, height);
+ float strokeInset = minSize / 2.0f - mCenterRadius;
+ float minStrokeInset = (float) Math.ceil(mStrokeWidth / 2.0f);
+ mStrokeInset = strokeInset < minStrokeInset ? minStrokeInset : strokeInset;
+ }
+
+ private void storeOriginals() {
+ mOriginEndDegrees = mEndDegrees;
+ mOriginStartDegrees = mEndDegrees;
+ }
+
+ private void resetOriginals() {
+ mOriginEndDegrees = 0;
+ mOriginStartDegrees = 0;
+
+ mEndDegrees = 0;
+ mStartDegrees = 0;
+
+ mSwipeDegrees = 1;
+ }
+
+ private void apply(Builder builder) {
+ this.mWidth = builder.mWidth > 0 ? builder.mWidth : this.mWidth;
+ this.mHeight = builder.mHeight > 0 ? builder.mHeight : this.mHeight;
+ this.mStrokeWidth = builder.mStrokeWidth > 0 ? builder.mStrokeWidth : this.mStrokeWidth;
+ this.mCenterRadius = builder.mCenterRadius > 0 ? builder.mCenterRadius : this.mCenterRadius;
+
+ this.mDuration = builder.mDuration > 0 ? builder.mDuration : this.mDuration;
+
+ this.mColor = builder.mColor != 0 ? builder.mColor : this.mColor;
+
+ this.mGearCount = builder.mGearCount > 0 ? builder.mGearCount : this.mGearCount;
+ this.mGearSwipeDegrees = builder.mGearSwipeDegrees > 0 ? builder.mGearSwipeDegrees : this.mGearSwipeDegrees;
+
+ setupPaint();
+ initStrokeInset(this.mWidth, this.mHeight);
+ }
+
+ public static class Builder {
+ private Context mContext;
+
+ private int mWidth;
+ private int mHeight;
+ private int mStrokeWidth;
+ private int mCenterRadius;
+
+ private int mDuration;
+
+ private int mColor;
+
+ private int mGearCount;
+ private int mGearSwipeDegrees;
+
+ public Builder(Context mContext) {
+ this.mContext = mContext;
+ }
+
+ public Builder setWidth(int width) {
+ this.mWidth = width;
+ return this;
+ }
+
+ public Builder setHeight(int height) {
+ this.mHeight = height;
+ return this;
+ }
+
+ public Builder setStrokeWidth(int strokeWidth) {
+ this.mStrokeWidth = strokeWidth;
+ return this;
+ }
+
+ public Builder setCenterRadius(int centerRadius) {
+ this.mCenterRadius = centerRadius;
+ return this;
+ }
+
+ public Builder setDuration(int duration) {
+ this.mDuration = duration;
+ return this;
+ }
+
+ public Builder setColor(int color) {
+ this.mColor = color;
+ return this;
+ }
+
+ public Builder setGearCount(int gearCount) {
+ this.mGearCount = gearCount;
+ return this;
+ }
+
+ public Builder setGearSwipeDegrees(@IntRange(from = 0, to = 360) int gearSwipeDegrees) {
+ this.mGearSwipeDegrees = gearSwipeDegrees;
+ return this;
+ }
+
+ public GearLoadingRenderer build() {
+ GearLoadingRenderer loadingRenderer = new GearLoadingRenderer(mContext);
+ loadingRenderer.apply(this);
+ return loadingRenderer;
+ }
+ }
+}
diff --git a/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/circle/rotate/LevelLoadingRenderer.java b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/circle/rotate/LevelLoadingRenderer.java
new file mode 100644
index 00000000..2e740073
--- /dev/null
+++ b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/circle/rotate/LevelLoadingRenderer.java
@@ -0,0 +1,300 @@
+package app.dinus.com.loadingdrawable.render.circle.rotate;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+
+import androidx.annotation.Size;
+import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
+
+import app.dinus.com.loadingdrawable.DensityUtil;
+import app.dinus.com.loadingdrawable.render.LoadingRenderer;
+
+public class LevelLoadingRenderer extends LoadingRenderer {
+ private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
+ private static final Interpolator MATERIAL_INTERPOLATOR = new FastOutSlowInInterpolator();
+ private static final Interpolator ACCELERATE_INTERPOLATOR = new AccelerateInterpolator();
+ private static final Interpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator();
+
+ private static final int NUM_POINTS = 5;
+ private static final int DEGREE_360 = 360;
+
+ private static final float MAX_SWIPE_DEGREES = 0.8f * DEGREE_360;
+ private static final float FULL_GROUP_ROTATION = 3.0f * DEGREE_360;
+
+ private static final float[] LEVEL_SWEEP_ANGLE_OFFSETS = new float[]{1.0f, 7.0f / 8.0f, 5.0f / 8.0f};
+
+ private static final float START_TRIM_DURATION_OFFSET = 0.5f;
+ private static final float END_TRIM_DURATION_OFFSET = 1.0f;
+
+ private static final float DEFAULT_CENTER_RADIUS = 12.5f;
+ private static final float DEFAULT_STROKE_WIDTH = 2.5f;
+
+ private static final int[] DEFAULT_LEVEL_COLORS = new int[]{Color.parseColor("#55ffffff"),
+ Color.parseColor("#b1ffffff"), Color.parseColor("#ffffffff")};
+
+ private final Paint mPaint = new Paint();
+ private final RectF mTempBounds = new RectF();
+
+ private final Animator.AnimatorListener mAnimatorListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationRepeat(Animator animator) {
+ super.onAnimationRepeat(animator);
+ storeOriginals();
+
+ mStartDegrees = mEndDegrees;
+ mRotationCount = (mRotationCount + 1) % (NUM_POINTS);
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ super.onAnimationStart(animation);
+ mRotationCount = 0;
+ }
+ };
+
+ @Size(3)
+ private int[] mLevelColors;
+ @Size(3)
+ private float[] mLevelSwipeDegrees;
+
+ private float mStrokeInset;
+
+ private float mRotationCount;
+ private float mGroupRotation;
+
+ private float mEndDegrees;
+ private float mStartDegrees;
+ private float mOriginEndDegrees;
+ private float mOriginStartDegrees;
+
+ private float mStrokeWidth;
+ private float mCenterRadius;
+
+ private LevelLoadingRenderer(Context context) {
+ super(context);
+ init(context);
+ setupPaint();
+ addRenderListener(mAnimatorListener);
+ }
+
+ private void init(Context context) {
+ mStrokeWidth = DensityUtil.dip2px(context, DEFAULT_STROKE_WIDTH);
+ mCenterRadius = DensityUtil.dip2px(context, DEFAULT_CENTER_RADIUS);
+
+ mLevelSwipeDegrees = new float[3];
+ mLevelColors = DEFAULT_LEVEL_COLORS;
+ }
+
+ private void setupPaint() {
+
+ mPaint.setAntiAlias(true);
+ mPaint.setStrokeWidth(mStrokeWidth);
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setStrokeCap(Paint.Cap.ROUND);
+
+ initStrokeInset((int) mWidth, (int) mHeight);
+ }
+
+ @Override
+ protected void draw(Canvas canvas) {
+ int saveCount = canvas.save();
+
+ mTempBounds.set(mBounds);
+ mTempBounds.inset(mStrokeInset, mStrokeInset);
+ canvas.rotate(mGroupRotation, mTempBounds.centerX(), mTempBounds.centerY());
+
+ for (int i = 0; i < 3; i++) {
+ if (mLevelSwipeDegrees[i] != 0) {
+ mPaint.setColor(mLevelColors[i]);
+ canvas.drawArc(mTempBounds, mEndDegrees, mLevelSwipeDegrees[i], false, mPaint);
+ }
+ }
+
+ canvas.restoreToCount(saveCount);
+ }
+
+ @Override
+ protected void computeRender(float renderProgress) {
+ // Moving the start trim only occurs in the first 50% of a single ring animation
+ if (renderProgress <= START_TRIM_DURATION_OFFSET) {
+ float startTrimProgress = (renderProgress) / START_TRIM_DURATION_OFFSET;
+ mStartDegrees = mOriginStartDegrees + MAX_SWIPE_DEGREES * MATERIAL_INTERPOLATOR.getInterpolation(startTrimProgress);
+
+ float mSwipeDegrees = mEndDegrees - mStartDegrees;
+ float levelSwipeDegreesProgress = Math.abs(mSwipeDegrees) / MAX_SWIPE_DEGREES;
+
+ float level1Increment = DECELERATE_INTERPOLATOR.getInterpolation(levelSwipeDegreesProgress) - LINEAR_INTERPOLATOR.getInterpolation(levelSwipeDegreesProgress);
+ float level3Increment = ACCELERATE_INTERPOLATOR.getInterpolation(levelSwipeDegreesProgress) - LINEAR_INTERPOLATOR.getInterpolation(levelSwipeDegreesProgress);
+
+ mLevelSwipeDegrees[0] = -mSwipeDegrees * LEVEL_SWEEP_ANGLE_OFFSETS[0] * (1.0f + level1Increment);
+ mLevelSwipeDegrees[1] = -mSwipeDegrees * LEVEL_SWEEP_ANGLE_OFFSETS[1] * 1.0f;
+ mLevelSwipeDegrees[2] = -mSwipeDegrees * LEVEL_SWEEP_ANGLE_OFFSETS[2] * (1.0f + level3Increment);
+ }
+
+ // Moving the end trim starts after 50% of a single ring animation
+ if (renderProgress > START_TRIM_DURATION_OFFSET) {
+ float endTrimProgress = (renderProgress - START_TRIM_DURATION_OFFSET) / (END_TRIM_DURATION_OFFSET - START_TRIM_DURATION_OFFSET);
+ mEndDegrees = mOriginEndDegrees + MAX_SWIPE_DEGREES * MATERIAL_INTERPOLATOR.getInterpolation(endTrimProgress);
+
+ float mSwipeDegrees = mEndDegrees - mStartDegrees;
+ float levelSwipeDegreesProgress = Math.abs(mSwipeDegrees) / MAX_SWIPE_DEGREES;
+
+ if (levelSwipeDegreesProgress > LEVEL_SWEEP_ANGLE_OFFSETS[1]) {
+ mLevelSwipeDegrees[0] = -mSwipeDegrees;
+ mLevelSwipeDegrees[1] = MAX_SWIPE_DEGREES * LEVEL_SWEEP_ANGLE_OFFSETS[1];
+ mLevelSwipeDegrees[2] = MAX_SWIPE_DEGREES * LEVEL_SWEEP_ANGLE_OFFSETS[2];
+ } else if (levelSwipeDegreesProgress > LEVEL_SWEEP_ANGLE_OFFSETS[2]) {
+ mLevelSwipeDegrees[0] = 0;
+ mLevelSwipeDegrees[1] = -mSwipeDegrees;
+ mLevelSwipeDegrees[2] = MAX_SWIPE_DEGREES * LEVEL_SWEEP_ANGLE_OFFSETS[2];
+ } else {
+ mLevelSwipeDegrees[0] = 0;
+ mLevelSwipeDegrees[1] = 0;
+ mLevelSwipeDegrees[2] = -mSwipeDegrees;
+ }
+ }
+
+ mGroupRotation = ((FULL_GROUP_ROTATION / NUM_POINTS) * renderProgress) + (FULL_GROUP_ROTATION * (mRotationCount / NUM_POINTS));
+ }
+
+ @Override
+ protected void setAlpha(int alpha) {
+ mPaint.setAlpha(alpha);
+ }
+
+ @Override
+ protected void setColorFilter(ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+ }
+
+ @Override
+ protected void reset() {
+ resetOriginals();
+ }
+
+ private void initStrokeInset(float width, float height) {
+ float minSize = Math.min(width, height);
+ float strokeInset = minSize / 2.0f - mCenterRadius;
+ float minStrokeInset = (float) Math.ceil(mStrokeWidth / 2.0f);
+ mStrokeInset = strokeInset < minStrokeInset ? minStrokeInset : strokeInset;
+ }
+
+ private void storeOriginals() {
+ mOriginEndDegrees = mEndDegrees;
+ mOriginStartDegrees = mEndDegrees;
+ }
+
+ private void resetOriginals() {
+ mOriginEndDegrees = 0;
+ mOriginStartDegrees = 0;
+
+ mEndDegrees = 0;
+ mStartDegrees = 0;
+
+ mLevelSwipeDegrees[0] = 0;
+ mLevelSwipeDegrees[1] = 0;
+ mLevelSwipeDegrees[2] = 0;
+ }
+
+ private void apply(Builder builder) {
+ this.mWidth = builder.mWidth > 0 ? builder.mWidth : this.mWidth;
+ this.mHeight = builder.mHeight > 0 ? builder.mHeight : this.mHeight;
+ this.mStrokeWidth = builder.mStrokeWidth > 0 ? builder.mStrokeWidth : this.mStrokeWidth;
+ this.mCenterRadius = builder.mCenterRadius > 0 ? builder.mCenterRadius : this.mCenterRadius;
+
+ this.mDuration = builder.mDuration > 0 ? builder.mDuration : this.mDuration;
+
+ this.mLevelColors = builder.mLevelColors != null ? builder.mLevelColors : this.mLevelColors;
+
+ setupPaint();
+ initStrokeInset(this.mWidth, this.mHeight);
+ }
+
+ public static class Builder {
+ private Context mContext;
+
+ private int mWidth;
+ private int mHeight;
+ private int mStrokeWidth;
+ private int mCenterRadius;
+
+ private int mDuration;
+
+ @Size(3)
+ private int[] mLevelColors;
+
+ public Builder(Context mContext) {
+ this.mContext = mContext;
+ }
+
+ public Builder setWidth(int width) {
+ this.mWidth = width;
+ return this;
+ }
+
+ public Builder setHeight(int height) {
+ this.mHeight = height;
+ return this;
+ }
+
+ public Builder setStrokeWidth(int strokeWidth) {
+ this.mStrokeWidth = strokeWidth;
+ return this;
+ }
+
+ public Builder setCenterRadius(int centerRadius) {
+ this.mCenterRadius = centerRadius;
+ return this;
+ }
+
+ public Builder setDuration(int duration) {
+ this.mDuration = duration;
+ return this;
+ }
+
+ public Builder setLevelColors(@Size(3) int[] colors) {
+ this.mLevelColors = colors;
+ return this;
+ }
+
+ public Builder setLevelColor(int color) {
+ return setLevelColors(new int[]{oneThirdAlphaColor(color), twoThirdAlphaColor(color), color});
+ }
+
+ public LevelLoadingRenderer build() {
+ LevelLoadingRenderer loadingRenderer = new LevelLoadingRenderer(mContext);
+ loadingRenderer.apply(this);
+ return loadingRenderer;
+ }
+
+ private int oneThirdAlphaColor(int colorValue) {
+ int startA = (colorValue >> 24) & 0xff;
+ int startR = (colorValue >> 16) & 0xff;
+ int startG = (colorValue >> 8) & 0xff;
+ int startB = colorValue & 0xff;
+
+ return (startA / 3 << 24) | (startR << 16) | (startG << 8) | startB;
+ }
+
+ private int twoThirdAlphaColor(int colorValue) {
+ int startA = (colorValue >> 24) & 0xff;
+ int startR = (colorValue >> 16) & 0xff;
+ int startG = (colorValue >> 8) & 0xff;
+ int startB = colorValue & 0xff;
+
+ return (startA * 2 / 3 << 24) | (startR << 16) | (startG << 8) | startB;
+ }
+ }
+
+}
diff --git a/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/circle/rotate/MaterialLoadingRenderer.java b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/circle/rotate/MaterialLoadingRenderer.java
new file mode 100644
index 00000000..700bab9c
--- /dev/null
+++ b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/circle/rotate/MaterialLoadingRenderer.java
@@ -0,0 +1,292 @@
+package app.dinus.com.loadingdrawable.render.circle.rotate;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.view.animation.Interpolator;
+
+import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
+
+import app.dinus.com.loadingdrawable.DensityUtil;
+import app.dinus.com.loadingdrawable.render.LoadingRenderer;
+
+public class MaterialLoadingRenderer extends LoadingRenderer {
+ private static final Interpolator MATERIAL_INTERPOLATOR = new FastOutSlowInInterpolator();
+
+ private static final int DEGREE_360 = 360;
+ private static final int NUM_POINTS = 5;
+
+ private static final float MAX_SWIPE_DEGREES = 0.8f * DEGREE_360;
+ private static final float FULL_GROUP_ROTATION = 3.0f * DEGREE_360;
+
+ private static final float COLOR_START_DELAY_OFFSET = 0.8f;
+ private static final float END_TRIM_DURATION_OFFSET = 1.0f;
+ private static final float START_TRIM_DURATION_OFFSET = 0.5f;
+
+ private static final float DEFAULT_CENTER_RADIUS = 12.5f;
+ private static final float DEFAULT_STROKE_WIDTH = 2.5f;
+
+ private static final int[] DEFAULT_COLORS = new int[]{
+ Color.RED, Color.GREEN, Color.BLUE
+ };
+
+ private final Paint mPaint = new Paint();
+ private final RectF mTempBounds = new RectF();
+
+ private final Animator.AnimatorListener mAnimatorListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationRepeat(Animator animator) {
+ super.onAnimationRepeat(animator);
+ storeOriginals();
+ goToNextColor();
+
+ mStartDegrees = mEndDegrees;
+ mRotationCount = (mRotationCount + 1) % (NUM_POINTS);
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ super.onAnimationStart(animation);
+ mRotationCount = 0;
+ }
+ };
+
+ private int[] mColors;
+ private int mColorIndex;
+ private int mCurrentColor;
+
+ private float mStrokeInset;
+
+ private float mRotationCount;
+ private float mGroupRotation;
+
+ private float mEndDegrees;
+ private float mStartDegrees;
+ private float mSwipeDegrees;
+ private float mOriginEndDegrees;
+ private float mOriginStartDegrees;
+
+ private float mStrokeWidth;
+ private float mCenterRadius;
+
+ private MaterialLoadingRenderer(Context context) {
+ super(context);
+ init(context);
+ setupPaint();
+ addRenderListener(mAnimatorListener);
+ }
+
+ private void init(Context context) {
+ mStrokeWidth = DensityUtil.dip2px(context, DEFAULT_STROKE_WIDTH);
+ mCenterRadius = DensityUtil.dip2px(context, DEFAULT_CENTER_RADIUS);
+
+ mColors = DEFAULT_COLORS;
+
+ setColorIndex(0);
+ initStrokeInset(mWidth, mHeight);
+ }
+
+ private void setupPaint() {
+ mPaint.setAntiAlias(true);
+ mPaint.setStrokeWidth(mStrokeWidth);
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setStrokeCap(Paint.Cap.ROUND);
+ }
+
+ @Override
+ protected void draw(Canvas canvas) {
+ int saveCount = canvas.save();
+
+ mTempBounds.set(mBounds);
+ mTempBounds.inset(mStrokeInset, mStrokeInset);
+
+ canvas.rotate(mGroupRotation, mTempBounds.centerX(), mTempBounds.centerY());
+
+ if (mSwipeDegrees != 0) {
+ mPaint.setColor(mCurrentColor);
+ canvas.drawArc(mTempBounds, mStartDegrees, mSwipeDegrees, false, mPaint);
+ }
+
+ canvas.restoreToCount(saveCount);
+ }
+
+ @Override
+ protected void computeRender(float renderProgress) {
+ updateRingColor(renderProgress);
+
+ // Moving the start trim only occurs in the first 50% of a single ring animation
+ if (renderProgress <= START_TRIM_DURATION_OFFSET) {
+ float startTrimProgress = renderProgress / START_TRIM_DURATION_OFFSET;
+ mStartDegrees = mOriginStartDegrees + MAX_SWIPE_DEGREES
+ * MATERIAL_INTERPOLATOR.getInterpolation(startTrimProgress);
+ }
+
+ // Moving the end trim starts after 50% of a single ring animation completes
+ if (renderProgress > START_TRIM_DURATION_OFFSET) {
+ float endTrimProgress = (renderProgress - START_TRIM_DURATION_OFFSET)
+ / (END_TRIM_DURATION_OFFSET - START_TRIM_DURATION_OFFSET);
+ mEndDegrees = mOriginEndDegrees + MAX_SWIPE_DEGREES
+ * MATERIAL_INTERPOLATOR.getInterpolation(endTrimProgress);
+ }
+
+ if (Math.abs(mEndDegrees - mStartDegrees) > 0) {
+ mSwipeDegrees = mEndDegrees - mStartDegrees;
+ }
+
+ mGroupRotation = ((FULL_GROUP_ROTATION / NUM_POINTS) * renderProgress)
+ + (FULL_GROUP_ROTATION * (mRotationCount / NUM_POINTS));
+ }
+
+ @Override
+ protected void setAlpha(int alpha) {
+ mPaint.setAlpha(alpha);
+ }
+
+ @Override
+ protected void setColorFilter(ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+ }
+
+ @Override
+ protected void reset() {
+ resetOriginals();
+ }
+
+ private void setColorIndex(int index) {
+ mColorIndex = index;
+ mCurrentColor = mColors[mColorIndex];
+ }
+
+ private int getNextColor() {
+ return mColors[getNextColorIndex()];
+ }
+
+ private int getNextColorIndex() {
+ return (mColorIndex + 1) % (mColors.length);
+ }
+
+ private void goToNextColor() {
+ setColorIndex(getNextColorIndex());
+ }
+
+ private void initStrokeInset(float width, float height) {
+ float minSize = Math.min(width, height);
+ float strokeInset = minSize / 2.0f - mCenterRadius;
+ float minStrokeInset = (float) Math.ceil(mStrokeWidth / 2.0f);
+ mStrokeInset = strokeInset < minStrokeInset ? minStrokeInset : strokeInset;
+ }
+
+ private void storeOriginals() {
+ mOriginEndDegrees = mEndDegrees;
+ mOriginStartDegrees = mEndDegrees;
+ }
+
+ private void resetOriginals() {
+ mOriginEndDegrees = 0;
+ mOriginStartDegrees = 0;
+
+ mEndDegrees = 0;
+ mStartDegrees = 0;
+ }
+
+ private int getStartingColor() {
+ return mColors[mColorIndex];
+ }
+
+ private void updateRingColor(float interpolatedTime) {
+ if (interpolatedTime > COLOR_START_DELAY_OFFSET) {
+ mCurrentColor = evaluateColorChange((interpolatedTime - COLOR_START_DELAY_OFFSET)
+ / (1.0f - COLOR_START_DELAY_OFFSET), getStartingColor(), getNextColor());
+ }
+ }
+
+ 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))));
+ }
+
+ private void apply(Builder builder) {
+ this.mWidth = builder.mWidth > 0 ? builder.mWidth : this.mWidth;
+ this.mHeight = builder.mHeight > 0 ? builder.mHeight : this.mHeight;
+ this.mStrokeWidth = builder.mStrokeWidth > 0 ? builder.mStrokeWidth : this.mStrokeWidth;
+ this.mCenterRadius = builder.mCenterRadius > 0 ? builder.mCenterRadius : this.mCenterRadius;
+
+ this.mDuration = builder.mDuration > 0 ? builder.mDuration : this.mDuration;
+
+ this.mColors = builder.mColors != null && builder.mColors.length > 0 ? builder.mColors : this.mColors;
+
+ setColorIndex(0);
+ setupPaint();
+ initStrokeInset(this.mWidth, this.mHeight);
+ }
+
+ public static class Builder {
+ private Context mContext;
+
+ private int mWidth;
+ private int mHeight;
+ private int mStrokeWidth;
+ private int mCenterRadius;
+
+ private int mDuration;
+
+ private int[] mColors;
+
+ public Builder(Context mContext) {
+ this.mContext = mContext;
+ }
+
+ public Builder setWidth(int width) {
+ this.mWidth = width;
+ return this;
+ }
+
+ public Builder setHeight(int height) {
+ this.mHeight = height;
+ return this;
+ }
+
+ public Builder setStrokeWidth(int strokeWidth) {
+ this.mStrokeWidth = strokeWidth;
+ return this;
+ }
+
+ public Builder setCenterRadius(int centerRadius) {
+ this.mCenterRadius = centerRadius;
+ return this;
+ }
+
+ public Builder setDuration(int duration) {
+ this.mDuration = duration;
+ return this;
+ }
+
+ public Builder setColors(int[] colors) {
+ this.mColors = colors;
+ return this;
+ }
+
+ public MaterialLoadingRenderer build() {
+ MaterialLoadingRenderer loadingRenderer = new MaterialLoadingRenderer(mContext);
+ loadingRenderer.apply(this);
+ return loadingRenderer;
+ }
+ }
+}
diff --git a/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/circle/rotate/WhorlLoadingRenderer.java b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/circle/rotate/WhorlLoadingRenderer.java
new file mode 100644
index 00000000..49c1d667
--- /dev/null
+++ b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/circle/rotate/WhorlLoadingRenderer.java
@@ -0,0 +1,261 @@
+package app.dinus.com.loadingdrawable.render.circle.rotate;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.view.animation.Interpolator;
+
+import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
+
+import app.dinus.com.loadingdrawable.DensityUtil;
+import app.dinus.com.loadingdrawable.render.LoadingRenderer;
+
+public class WhorlLoadingRenderer extends LoadingRenderer {
+ private static final Interpolator MATERIAL_INTERPOLATOR = new FastOutSlowInInterpolator();
+
+ private static final int DEGREE_180 = 180;
+ private static final int DEGREE_360 = 360;
+ private static final int NUM_POINTS = 5;
+
+ private static final float MAX_SWIPE_DEGREES = 0.6f * DEGREE_360;
+ private static final float FULL_GROUP_ROTATION = 3.0f * DEGREE_360;
+
+ private static final float START_TRIM_DURATION_OFFSET = 0.5f;
+ private static final float END_TRIM_DURATION_OFFSET = 1.0f;
+
+ private static final float DEFAULT_CENTER_RADIUS = 12.5f;
+ private static final float DEFAULT_STROKE_WIDTH = 2.5f;
+
+ private static final int[] DEFAULT_COLORS = new int[]{
+ Color.RED, Color.GREEN, Color.BLUE
+ };
+
+ private final Paint mPaint = new Paint();
+ private final RectF mTempBounds = new RectF();
+ private final RectF mTempArcBounds = new RectF();
+
+ private final Animator.AnimatorListener mAnimatorListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationRepeat(Animator animator) {
+ super.onAnimationRepeat(animator);
+ storeOriginals();
+
+ mStartDegrees = mEndDegrees;
+ mRotationCount = (mRotationCount + 1) % (NUM_POINTS);
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ super.onAnimationStart(animation);
+ mRotationCount = 0;
+ }
+ };
+
+ private int[] mColors;
+
+ private float mStrokeInset;
+
+ private float mRotationCount;
+ private float mGroupRotation;
+
+ private float mEndDegrees;
+ private float mStartDegrees;
+ private float mSwipeDegrees;
+ private float mOriginEndDegrees;
+ private float mOriginStartDegrees;
+
+ private float mStrokeWidth;
+ private float mCenterRadius;
+
+ private WhorlLoadingRenderer(Context context) {
+ super(context);
+ init(context);
+ setupPaint();
+ addRenderListener(mAnimatorListener);
+ }
+
+ private void init(Context context) {
+ mColors = DEFAULT_COLORS;
+ mStrokeWidth = DensityUtil.dip2px(context, DEFAULT_STROKE_WIDTH);
+ mCenterRadius = DensityUtil.dip2px(context, DEFAULT_CENTER_RADIUS);
+
+ initStrokeInset(mWidth, mHeight);
+ }
+
+ private void setupPaint() {
+ mPaint.setAntiAlias(true);
+ mPaint.setStrokeWidth(mStrokeWidth);
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setStrokeCap(Paint.Cap.ROUND);
+ }
+
+ @Override
+ protected void draw(Canvas canvas) {
+ int saveCount = canvas.save();
+
+ mTempBounds.set(mBounds);
+ mTempBounds.inset(mStrokeInset, mStrokeInset);
+
+ canvas.rotate(mGroupRotation, mTempBounds.centerX(), mTempBounds.centerY());
+
+ if (mSwipeDegrees != 0) {
+ for (int i = 0; i < mColors.length; i++) {
+ mPaint.setStrokeWidth(mStrokeWidth / (i + 1));
+ mPaint.setColor(mColors[i]);
+ canvas.drawArc(createArcBounds(mTempBounds, i), mStartDegrees + DEGREE_180 * (i % 2),
+ mSwipeDegrees, false, mPaint);
+ }
+ }
+
+ canvas.restoreToCount(saveCount);
+ }
+
+ private RectF createArcBounds(RectF sourceArcBounds, int index) {
+ int intervalWidth = 0;
+
+ for (int i = 0; i < index; i++) {
+ intervalWidth += mStrokeWidth / (i + 1.0f) * 1.5f;
+ }
+
+ int arcBoundsLeft = (int) (sourceArcBounds.left + intervalWidth);
+ int arcBoundsTop = (int) (sourceArcBounds.top + intervalWidth);
+ int arcBoundsRight = (int) (sourceArcBounds.right - intervalWidth);
+ int arcBoundsBottom = (int) (sourceArcBounds.bottom - intervalWidth);
+ mTempArcBounds.set(arcBoundsLeft, arcBoundsTop, arcBoundsRight, arcBoundsBottom);
+
+ return mTempArcBounds;
+ }
+
+ @Override
+ protected void computeRender(float renderProgress) {
+ // Moving the start trim only occurs in the first 50% of a single ring animation
+ if (renderProgress <= START_TRIM_DURATION_OFFSET) {
+ float startTrimProgress = (renderProgress) / (1.0f - START_TRIM_DURATION_OFFSET);
+ mStartDegrees = mOriginStartDegrees + MAX_SWIPE_DEGREES * MATERIAL_INTERPOLATOR.getInterpolation(startTrimProgress);
+ }
+
+ // Moving the end trim starts after 50% of a single ring animation
+ if (renderProgress > START_TRIM_DURATION_OFFSET) {
+ float endTrimProgress = (renderProgress - START_TRIM_DURATION_OFFSET) / (END_TRIM_DURATION_OFFSET - START_TRIM_DURATION_OFFSET);
+ mEndDegrees = mOriginEndDegrees + MAX_SWIPE_DEGREES * MATERIAL_INTERPOLATOR.getInterpolation(endTrimProgress);
+ }
+
+ if (Math.abs(mEndDegrees - mStartDegrees) > 0) {
+ mSwipeDegrees = mEndDegrees - mStartDegrees;
+ }
+
+ mGroupRotation = ((FULL_GROUP_ROTATION / NUM_POINTS) * renderProgress) + (FULL_GROUP_ROTATION * (mRotationCount / NUM_POINTS));
+ }
+
+ @Override
+ protected void setAlpha(int alpha) {
+ mPaint.setAlpha(alpha);
+
+ }
+
+ @Override
+ protected void setColorFilter(ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+
+ }
+
+ @Override
+ protected void reset() {
+ resetOriginals();
+ }
+
+ private void initStrokeInset(float width, float height) {
+ float minSize = Math.min(width, height);
+ float strokeInset = minSize / 2.0f - mCenterRadius;
+ float minStrokeInset = (float) Math.ceil(mStrokeWidth / 2.0f);
+ mStrokeInset = strokeInset < minStrokeInset ? minStrokeInset : strokeInset;
+ }
+
+ private void storeOriginals() {
+ mOriginEndDegrees = mEndDegrees;
+ mOriginStartDegrees = mEndDegrees;
+ }
+
+ private void resetOriginals() {
+ mOriginEndDegrees = 0;
+ mOriginStartDegrees = 0;
+
+ mEndDegrees = 0;
+ mStartDegrees = 0;
+
+ mSwipeDegrees = 0;
+ }
+
+ private void apply(Builder builder) {
+ this.mWidth = builder.mWidth > 0 ? builder.mWidth : this.mWidth;
+ this.mHeight = builder.mHeight > 0 ? builder.mHeight : this.mHeight;
+ this.mStrokeWidth = builder.mStrokeWidth > 0 ? builder.mStrokeWidth : this.mStrokeWidth;
+ this.mCenterRadius = builder.mCenterRadius > 0 ? builder.mCenterRadius : this.mCenterRadius;
+
+ this.mDuration = builder.mDuration > 0 ? builder.mDuration : this.mDuration;
+
+ this.mColors = builder.mColors != null && builder.mColors.length > 0 ? builder.mColors : this.mColors;
+
+ setupPaint();
+ initStrokeInset(this.mWidth, this.mHeight);
+ }
+
+ public static class Builder {
+ private Context mContext;
+
+ private int mWidth;
+ private int mHeight;
+ private int mStrokeWidth;
+ private int mCenterRadius;
+
+ private int mDuration;
+
+ private int[] mColors;
+
+ public Builder(Context mContext) {
+ this.mContext = mContext;
+ }
+
+ public Builder setWidth(int width) {
+ this.mWidth = width;
+ return this;
+ }
+
+ public Builder setHeight(int height) {
+ this.mHeight = height;
+ return this;
+ }
+
+ public Builder setStrokeWidth(int strokeWidth) {
+ this.mStrokeWidth = strokeWidth;
+ return this;
+ }
+
+ public Builder setCenterRadius(int centerRadius) {
+ this.mCenterRadius = centerRadius;
+ return this;
+ }
+
+ public Builder setDuration(int duration) {
+ this.mDuration = duration;
+ return this;
+ }
+
+ public Builder setColors(int[] colors) {
+ this.mColors = colors;
+ return this;
+ }
+
+ public WhorlLoadingRenderer build() {
+ WhorlLoadingRenderer loadingRenderer = new WhorlLoadingRenderer(mContext);
+ loadingRenderer.apply(this);
+ return loadingRenderer;
+ }
+ }
+}
diff --git a/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/goods/BalloonLoadingRenderer.java b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/goods/BalloonLoadingRenderer.java
new file mode 100644
index 00000000..269ecacf
--- /dev/null
+++ b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/goods/BalloonLoadingRenderer.java
@@ -0,0 +1,311 @@
+package app.dinus.com.loadingdrawable.render.goods;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.DisplayMetrics;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import app.dinus.com.loadingdrawable.DensityUtil;
+import app.dinus.com.loadingdrawable.render.LoadingRenderer;
+
+public class BalloonLoadingRenderer extends LoadingRenderer {
+ private static final String PERCENT_SIGN = "%";
+
+ private static final Interpolator ACCELERATE_INTERPOLATOR = new AccelerateInterpolator();
+
+ private static final float START_INHALE_DURATION_OFFSET = 0.4f;
+
+ private static final float DEFAULT_WIDTH = 200.0f;
+ private static final float DEFAULT_HEIGHT = 150.0f;
+ private static final float DEFAULT_STROKE_WIDTH = 2.0f;
+ private static final float DEFAULT_GAS_TUBE_WIDTH = 48;
+ private static final float DEFAULT_GAS_TUBE_HEIGHT = 20;
+ private static final float DEFAULT_CANNULA_WIDTH = 13;
+ private static final float DEFAULT_CANNULA_HEIGHT = 37;
+ private static final float DEFAULT_CANNULA_OFFSET_Y = 3;
+ private static final float DEFAULT_CANNULA_MAX_OFFSET_Y = 15;
+ private static final float DEFAULT_PIPE_BODY_WIDTH = 16;
+ private static final float DEFAULT_PIPE_BODY_HEIGHT = 36;
+ private static final float DEFAULT_BALLOON_WIDTH = 38;
+ private static final float DEFAULT_BALLOON_HEIGHT = 48;
+ private static final float DEFAULT_RECT_CORNER_RADIUS = 2;
+
+ private static final int DEFAULT_BALLOON_COLOR = Color.parseColor("#ffF3C211");
+ private static final int DEFAULT_GAS_TUBE_COLOR = Color.parseColor("#ff174469");
+ private static final int DEFAULT_PIPE_BODY_COLOR = Color.parseColor("#aa2369B1");
+ private static final int DEFAULT_CANNULA_COLOR = Color.parseColor("#ff174469");
+
+ private static final float DEFAULT_TEXT_SIZE = 7.0f;
+
+ private static final long ANIMATION_DURATION = 3333;
+
+ private final Paint mPaint = new Paint();
+ private final RectF mCurrentBounds = new RectF();
+ private final RectF mGasTubeBounds = new RectF();
+ private final RectF mPipeBodyBounds = new RectF();
+ private final RectF mCannulaBounds = new RectF();
+ private final RectF mBalloonBounds = new RectF();
+
+ private final Rect mProgressBounds = new Rect();
+
+ private float mTextSize;
+ private float mProgress;
+
+ private String mProgressText;
+
+ private float mGasTubeWidth;
+ private float mGasTubeHeight;
+ private float mCannulaWidth;
+ private float mCannulaHeight;
+ private float mCannulaMaxOffsetY;
+ private float mCannulaOffsetY;
+ private float mPipeBodyWidth;
+ private float mPipeBodyHeight;
+ private float mBalloonWidth;
+ private float mBalloonHeight;
+ private float mRectCornerRadius;
+ private float mStrokeWidth;
+
+ private int mBalloonColor;
+ private int mGasTubeColor;
+ private int mCannulaColor;
+ private int mPipeBodyColor;
+
+ private BalloonLoadingRenderer(Context context) {
+ super(context);
+ init(context);
+ setupPaint();
+ }
+
+ private void init(Context context) {
+ mTextSize = DensityUtil.dip2px(context, DEFAULT_TEXT_SIZE);
+
+ mWidth = DensityUtil.dip2px(context, DEFAULT_WIDTH);
+ mHeight = DensityUtil.dip2px(context, DEFAULT_HEIGHT);
+ mStrokeWidth = DensityUtil.dip2px(context, DEFAULT_STROKE_WIDTH);
+
+ mGasTubeWidth = DensityUtil.dip2px(context, DEFAULT_GAS_TUBE_WIDTH);
+ mGasTubeHeight = DensityUtil.dip2px(context, DEFAULT_GAS_TUBE_HEIGHT);
+ mCannulaWidth = DensityUtil.dip2px(context, DEFAULT_CANNULA_WIDTH);
+ mCannulaHeight = DensityUtil.dip2px(context, DEFAULT_CANNULA_HEIGHT);
+ mCannulaOffsetY = DensityUtil.dip2px(context, DEFAULT_CANNULA_OFFSET_Y);
+ mCannulaMaxOffsetY = DensityUtil.dip2px(context, DEFAULT_CANNULA_MAX_OFFSET_Y);
+ mPipeBodyWidth = DensityUtil.dip2px(context, DEFAULT_PIPE_BODY_WIDTH);
+ mPipeBodyHeight = DensityUtil.dip2px(context, DEFAULT_PIPE_BODY_HEIGHT);
+ mBalloonWidth = DensityUtil.dip2px(context, DEFAULT_BALLOON_WIDTH);
+ mBalloonHeight = DensityUtil.dip2px(context, DEFAULT_BALLOON_HEIGHT);
+ mRectCornerRadius = DensityUtil.dip2px(context, DEFAULT_RECT_CORNER_RADIUS);
+
+ mBalloonColor = DEFAULT_BALLOON_COLOR;
+ mGasTubeColor = DEFAULT_GAS_TUBE_COLOR;
+ mCannulaColor = DEFAULT_CANNULA_COLOR;
+ mPipeBodyColor = DEFAULT_PIPE_BODY_COLOR;
+
+ mProgressText = 10 + PERCENT_SIGN;
+
+ mDuration = ANIMATION_DURATION;
+ }
+
+ private void setupPaint() {
+ mPaint.setAntiAlias(true);
+ mPaint.setStrokeWidth(mStrokeWidth);
+ }
+
+ @Override
+ protected void draw(Canvas canvas, Rect bounds) {
+ int saveCount = canvas.save();
+
+ RectF arcBounds = mCurrentBounds;
+ arcBounds.set(bounds);
+
+ //draw draw gas tube
+ mPaint.setColor(mGasTubeColor);
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setStrokeWidth(mStrokeWidth);
+ canvas.drawPath(createGasTubePath(mGasTubeBounds), mPaint);
+
+ //draw balloon
+ mPaint.setColor(mBalloonColor);
+ mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
+ canvas.drawPath(createBalloonPath(mBalloonBounds, mProgress), mPaint);
+
+ //draw progress
+ mPaint.setColor(mGasTubeColor);
+ mPaint.setTextSize(mTextSize);
+ mPaint.setStrokeWidth(mStrokeWidth / 5.0f);
+ canvas.drawText(mProgressText, arcBounds.centerX() - mProgressBounds.width() / 2.0f,
+ mGasTubeBounds.centerY() + mProgressBounds.height() / 2.0f, mPaint);
+
+ //draw cannula
+ mPaint.setColor(mCannulaColor);
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setStrokeWidth(mStrokeWidth);
+ canvas.drawPath(createCannulaHeadPath(mCannulaBounds), mPaint);
+ mPaint.setStyle(Paint.Style.FILL);
+ canvas.drawPath(createCannulaBottomPath(mCannulaBounds), mPaint);
+
+ //draw pipe body
+ mPaint.setColor(mPipeBodyColor);
+ mPaint.setStyle(Paint.Style.FILL);
+ canvas.drawRoundRect(mPipeBodyBounds, mRectCornerRadius, mRectCornerRadius, mPaint);
+
+ canvas.restoreToCount(saveCount);
+ }
+
+ @Override
+ protected void computeRender(float renderProgress) {
+ RectF arcBounds = mCurrentBounds;
+ //compute gas tube bounds
+ mGasTubeBounds.set(arcBounds.centerX() - mGasTubeWidth / 2.0f, arcBounds.centerY(),
+ arcBounds.centerX() + mGasTubeWidth / 2.0f, arcBounds.centerY() + mGasTubeHeight);
+ //compute pipe body bounds
+ mPipeBodyBounds.set(arcBounds.centerX() + mGasTubeWidth / 2.0f - mPipeBodyWidth / 2.0f, arcBounds.centerY() - mPipeBodyHeight,
+ arcBounds.centerX() + mGasTubeWidth / 2.0f + mPipeBodyWidth / 2.0f, arcBounds.centerY());
+ //compute cannula bounds
+ mCannulaBounds.set(arcBounds.centerX() + mGasTubeWidth / 2.0f - mCannulaWidth / 2.0f, arcBounds.centerY() - mCannulaHeight - mCannulaOffsetY,
+ arcBounds.centerX() + mGasTubeWidth / 2.0f + mCannulaWidth / 2.0f, arcBounds.centerY() - mCannulaOffsetY);
+ //compute balloon bounds
+ float insetX = mBalloonWidth * 0.333f * (1 - mProgress);
+ float insetY = mBalloonHeight * 0.667f * (1 - mProgress);
+ mBalloonBounds.set(arcBounds.centerX() - mGasTubeWidth / 2.0f - mBalloonWidth / 2.0f + insetX, arcBounds.centerY() - mBalloonHeight + insetY,
+ arcBounds.centerX() - mGasTubeWidth / 2.0f + mBalloonWidth / 2.0f - insetX, arcBounds.centerY());
+
+ if (renderProgress <= START_INHALE_DURATION_OFFSET) {
+ mCannulaBounds.offset(0, -mCannulaMaxOffsetY * renderProgress / START_INHALE_DURATION_OFFSET);
+
+ mProgress = 0.0f;
+ mProgressText = 10 + PERCENT_SIGN;
+
+ mPaint.setTextSize(mTextSize);
+ mPaint.getTextBounds(mProgressText, 0, mProgressText.length(), mProgressBounds);
+ } else {
+ float exhaleProgress = ACCELERATE_INTERPOLATOR.getInterpolation(1.0f - (renderProgress - START_INHALE_DURATION_OFFSET) / (1.0f - START_INHALE_DURATION_OFFSET));
+ mCannulaBounds.offset(0, -mCannulaMaxOffsetY * exhaleProgress);
+
+ mProgress = 1.0f - exhaleProgress;
+ mProgressText = adjustProgress((int) (exhaleProgress * 100.0f)) + PERCENT_SIGN;
+
+ mPaint.setTextSize(mTextSize);
+ mPaint.getTextBounds(mProgressText, 0, mProgressText.length(), mProgressBounds);
+ }
+ }
+
+ private int adjustProgress(int progress) {
+ progress = progress / 10 * 10;
+ progress = 100 - progress + 10;
+ if (progress > 100) {
+ progress = 100;
+ }
+
+ return progress;
+ }
+
+ private Path createGasTubePath(RectF gasTubeRect) {
+ Path path = new Path();
+ path.moveTo(gasTubeRect.left, gasTubeRect.top);
+ path.lineTo(gasTubeRect.left, gasTubeRect.bottom);
+ path.lineTo(gasTubeRect.right, gasTubeRect.bottom);
+ path.lineTo(gasTubeRect.right, gasTubeRect.top);
+ return path;
+ }
+
+ private Path createCannulaHeadPath(RectF cannulaRect) {
+ Path path = new Path();
+ path.moveTo(cannulaRect.left, cannulaRect.top);
+ path.lineTo(cannulaRect.right, cannulaRect.top);
+ path.moveTo(cannulaRect.centerX(), cannulaRect.top);
+ path.lineTo(cannulaRect.centerX(), cannulaRect.bottom - 0.833f * cannulaRect.width());
+ return path;
+ }
+
+ private Path createCannulaBottomPath(RectF cannulaRect) {
+ RectF cannulaHeadRect = new RectF(cannulaRect.left, cannulaRect.bottom - 0.833f * cannulaRect.width(),
+ cannulaRect.right, cannulaRect.bottom);
+
+ Path path = new Path();
+ path.addRoundRect(cannulaHeadRect, mRectCornerRadius, mRectCornerRadius, Path.Direction.CCW);
+ return path;
+ }
+
+ /**
+ * Coordinates are approximate, you have better cooperate with the designer's design draft
+ */
+ private Path createBalloonPath(RectF balloonRect, float progress) {
+
+ Path path = new Path();
+ path.moveTo(balloonRect.centerX(), balloonRect.bottom);
+
+ float progressWidth = balloonRect.width() * progress;
+ float progressHeight = balloonRect.height() * progress;
+ //draw left half
+ float leftIncrementX1 = progressWidth * -0.48f;
+ float leftIncrementY1 = progressHeight * 0.75f;
+ float leftIncrementX2 = progressWidth * -0.03f;
+ float leftIncrementY2 = progressHeight * -1.6f;
+ float leftIncrementX3 = progressWidth * 0.9f;
+ float leftIncrementY3 = progressHeight * -1.0f;
+
+ path.cubicTo(balloonRect.left + balloonRect.width() * 0.25f + leftIncrementX1, balloonRect.centerY() - balloonRect.height() * 0.4f + leftIncrementY1,
+ balloonRect.left - balloonRect.width() * 0.20f + leftIncrementX2, balloonRect.centerY() + balloonRect.height() * 1.15f + leftIncrementY2,
+ balloonRect.left - balloonRect.width() * 0.4f + leftIncrementX3, balloonRect.bottom + leftIncrementY3);
+
+// the results of the left final transformation
+// path.cubicTo(balloonRect.left - balloonRect.width() * 0.13f, balloonRect.centerY() + balloonRect.height() * 0.35f,
+// balloonRect.left - balloonRect.width() * 0.23f, balloonRect.centerY() - balloonRect.height() * 0.45f,
+// balloonRect.left + balloonRect.width() * 0.5f, balloonRect.bottom - balloonRect.height());
+
+ //draw right half
+ float rightIncrementX1 = progressWidth * 1.51f;
+ float rightIncrementY1 = progressHeight * -0.05f;
+ float rightIncrementX2 = progressWidth * 0.03f;
+ float rightIncrementY2 = progressHeight * 0.5f;
+ float rightIncrementX3 = 0.0f;
+ float rightIncrementY3 = 0.0f;
+
+ path.cubicTo(balloonRect.left - balloonRect.width() * 0.38f + rightIncrementX1, balloonRect.centerY() - balloonRect.height() * 0.4f + rightIncrementY1,
+ balloonRect.left + balloonRect.width() * 1.1f + rightIncrementX2, balloonRect.centerY() - balloonRect.height() * 0.15f + rightIncrementY2,
+ balloonRect.left + balloonRect.width() * 0.5f + rightIncrementX3, balloonRect.bottom + rightIncrementY3);
+
+// the results of the right final transformation
+// path.cubicTo(balloonRect.left + balloonRect.width() * 1.23f, balloonRect.centerY() - balloonRect.height() * 0.45f,
+// balloonRect.left + balloonRect.width() * 1.13f, balloonRect.centerY() + balloonRect.height() * 0.35f,
+// balloonRect.left + balloonRect.width() * 0.5f, balloonRect.bottom);
+
+ return path;
+ }
+
+ @Override
+ protected void setAlpha(int alpha) {
+ mPaint.setAlpha(alpha);
+
+ }
+
+ @Override
+ protected void setColorFilter(ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+ }
+
+ @Override
+ protected void reset() {
+ }
+
+ public static class Builder {
+ private Context mContext;
+
+ public Builder(Context mContext) {
+ this.mContext = mContext;
+ }
+
+ public BalloonLoadingRenderer build() {
+ BalloonLoadingRenderer loadingRenderer = new BalloonLoadingRenderer(mContext);
+ return loadingRenderer;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/goods/WaterBottleLoadingRenderer.java b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/goods/WaterBottleLoadingRenderer.java
new file mode 100644
index 00000000..ba5a7b59
--- /dev/null
+++ b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/goods/WaterBottleLoadingRenderer.java
@@ -0,0 +1,323 @@
+package app.dinus.com.loadingdrawable.render.goods;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.DisplayMetrics;
+import android.view.animation.Interpolator;
+
+import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+import app.dinus.com.loadingdrawable.DensityUtil;
+import app.dinus.com.loadingdrawable.render.LoadingRenderer;
+
+public class WaterBottleLoadingRenderer extends LoadingRenderer {
+ private static final Interpolator MATERIAL_INTERPOLATOR = new FastOutSlowInInterpolator();
+
+ private static final float DEFAULT_WIDTH = 200.0f;
+ private static final float DEFAULT_HEIGHT = 150.0f;
+ private static final float DEFAULT_STROKE_WIDTH = 1.5f;
+ private static final float DEFAULT_BOTTLE_WIDTH = 30;
+ private static final float DEFAULT_BOTTLE_HEIGHT = 43;
+ private static final float WATER_LOWEST_POINT_TO_BOTTLENECK_DISTANCE = 30;
+
+ private static final int DEFAULT_WAVE_COUNT = 5;
+ private static final int DEFAULT_WATER_DROP_COUNT = 25;
+
+ private static final int MAX_WATER_DROP_RADIUS = 5;
+ private static final int MIN_WATER_DROP_RADIUS = 1;
+
+ private static final int DEFAULT_BOTTLE_COLOR = Color.parseColor("#FFDAEBEB");
+ private static final int DEFAULT_WATER_COLOR = Color.parseColor("#FF29E3F2");
+
+ private static final float DEFAULT_TEXT_SIZE = 7.0f;
+
+ private static final String LOADING_TEXT = "loading";
+
+ private static final long ANIMATION_DURATION = 11111;
+
+ private final Random mRandom = new Random();
+
+ private final Paint mPaint = new Paint();
+ private final RectF mCurrentBounds = new RectF();
+ private final RectF mBottleBounds = new RectF();
+ private final RectF mWaterBounds = new RectF();
+ private final Rect mLoadingBounds = new Rect();
+ private final List mWaterDropHolders = new ArrayList<>();
+
+ private float mTextSize;
+ private float mProgress;
+
+ private float mBottleWidth;
+ private float mBottleHeight;
+ private float mStrokeWidth;
+ private float mWaterLowestPointToBottleneckDistance;
+
+ private int mBottleColor;
+ private int mWaterColor;
+
+ private int mWaveCount;
+
+ private WaterBottleLoadingRenderer(Context context) {
+ super(context);
+ init(context);
+ setupPaint();
+ }
+
+ private void init(Context context) {
+ mTextSize = DensityUtil.dip2px(context, DEFAULT_TEXT_SIZE);
+
+ mWidth = DensityUtil.dip2px(context, DEFAULT_WIDTH);
+ mHeight = DensityUtil.dip2px(context, DEFAULT_HEIGHT);
+ mStrokeWidth = DensityUtil.dip2px(context, DEFAULT_STROKE_WIDTH);
+
+ mBottleWidth = DensityUtil.dip2px(context, DEFAULT_BOTTLE_WIDTH);
+ mBottleHeight = DensityUtil.dip2px(context, DEFAULT_BOTTLE_HEIGHT);
+ mWaterLowestPointToBottleneckDistance = DensityUtil.dip2px(context, WATER_LOWEST_POINT_TO_BOTTLENECK_DISTANCE);
+
+ mBottleColor = DEFAULT_BOTTLE_COLOR;
+ mWaterColor = DEFAULT_WATER_COLOR;
+
+ mWaveCount = DEFAULT_WAVE_COUNT;
+
+ mDuration = ANIMATION_DURATION;
+ }
+
+ private void setupPaint() {
+ mPaint.setAntiAlias(true);
+ mPaint.setStrokeWidth(mStrokeWidth);
+ mPaint.setStrokeJoin(Paint.Join.ROUND);
+ }
+
+ @Override
+ protected void draw(Canvas canvas, Rect bounds) {
+ int saveCount = canvas.save();
+
+ RectF arcBounds = mCurrentBounds;
+ arcBounds.set(bounds);
+ //draw bottle
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setColor(mBottleColor);
+ canvas.drawPath(createBottlePath(mBottleBounds), mPaint);
+
+ //draw water
+ mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
+ mPaint.setColor(mWaterColor);
+ canvas.drawPath(createWaterPath(mWaterBounds, mProgress), mPaint);
+
+ //draw water drop
+ mPaint.setStyle(Paint.Style.FILL);
+ mPaint.setColor(mWaterColor);
+ for (WaterDropHolder waterDropHolder : mWaterDropHolders) {
+ if (waterDropHolder.mNeedDraw) {
+ canvas.drawCircle(waterDropHolder.mInitX, waterDropHolder.mCurrentY, waterDropHolder.mRadius, mPaint);
+ }
+ }
+
+ //draw loading text
+ mPaint.setColor(mBottleColor);
+ canvas.drawText(LOADING_TEXT, mBottleBounds.centerX() - mLoadingBounds.width() / 2.0f,
+ mBottleBounds.bottom + mBottleBounds.height() * 0.2f, mPaint);
+ canvas.restoreToCount(saveCount);
+ }
+
+ @Override
+ protected void computeRender(float renderProgress) {
+ if (mCurrentBounds.width() <= 0) {
+ return;
+ }
+
+ RectF arcBounds = mCurrentBounds;
+ //compute gas tube bounds
+ mBottleBounds.set(arcBounds.centerX() - mBottleWidth / 2.0f, arcBounds.centerY() - mBottleHeight / 2.0f,
+ arcBounds.centerX() + mBottleWidth / 2.0f, arcBounds.centerY() + mBottleHeight / 2.0f);
+ //compute pipe body bounds
+ mWaterBounds.set(mBottleBounds.left + mStrokeWidth * 1.5f, mBottleBounds.top + mWaterLowestPointToBottleneckDistance,
+ mBottleBounds.right - mStrokeWidth * 1.5f, mBottleBounds.bottom - mStrokeWidth * 1.5f);
+
+ //compute wave progress
+ float totalWaveProgress = renderProgress * mWaveCount;
+ float currentWaveProgress = totalWaveProgress - ((int) totalWaveProgress);
+
+ if (currentWaveProgress > 0.5f) {
+ mProgress = 1.0f - MATERIAL_INTERPOLATOR.getInterpolation((currentWaveProgress - 0.5f) * 2.0f);
+ } else {
+ mProgress = MATERIAL_INTERPOLATOR.getInterpolation(currentWaveProgress * 2.0f);
+ }
+
+ //init water drop holders
+ if (mWaterDropHolders.isEmpty()) {
+ initWaterDropHolders(mBottleBounds, mWaterBounds);
+ }
+
+ //compute the location of these water drops
+ for (WaterDropHolder waterDropHolder : mWaterDropHolders) {
+ if (waterDropHolder.mDelayDuration < renderProgress
+ && waterDropHolder.mDelayDuration + waterDropHolder.mDuration > renderProgress) {
+ float riseProgress = (renderProgress - waterDropHolder.mDelayDuration) / waterDropHolder.mDuration;
+ riseProgress = riseProgress < 0.5f ? riseProgress * 2.0f : 1.0f - (riseProgress - 0.5f) * 2.0f;
+ waterDropHolder.mCurrentY = waterDropHolder.mInitY -
+ MATERIAL_INTERPOLATOR.getInterpolation(riseProgress) * waterDropHolder.mRiseHeight;
+ waterDropHolder.mNeedDraw = true;
+ } else {
+ waterDropHolder.mNeedDraw = false;
+ }
+ }
+
+ //measure loading text
+ mPaint.setTextSize(mTextSize);
+ mPaint.getTextBounds(LOADING_TEXT, 0, LOADING_TEXT.length(), mLoadingBounds);
+ }
+
+ private Path createBottlePath(RectF bottleRect) {
+ float bottleneckWidth = bottleRect.width() * 0.3f;
+ float bottleneckHeight = bottleRect.height() * 0.415f;
+ float bottleneckDecorationWidth = bottleneckWidth * 1.1f;
+ float bottleneckDecorationHeight = bottleneckHeight * 0.167f;
+
+ Path path = new Path();
+ //draw the left side of the bottleneck decoration
+ path.moveTo(bottleRect.centerX() - bottleneckDecorationWidth * 0.5f, bottleRect.top);
+ path.quadTo(bottleRect.centerX() - bottleneckDecorationWidth * 0.5f - bottleneckWidth * 0.15f, bottleRect.top + bottleneckDecorationHeight * 0.5f,
+ bottleRect.centerX() - bottleneckWidth * 0.5f, bottleRect.top + bottleneckDecorationHeight);
+ path.lineTo(bottleRect.centerX() - bottleneckWidth * 0.5f, bottleRect.top + bottleneckHeight);
+
+ //draw the left side of the bottle's body
+ float radius = (bottleRect.width() - mStrokeWidth) / 2.0f;
+ float centerY = bottleRect.bottom - 0.86f * radius;
+ RectF bodyRect = new RectF(bottleRect.left, centerY - radius, bottleRect.right, centerY + radius);
+ path.addArc(bodyRect, 255, -135);
+
+ //draw the bottom of the bottle
+ float bottleBottomWidth = bottleRect.width() / 2.0f;
+ path.lineTo(bottleRect.centerX() - bottleBottomWidth / 2.0f, bottleRect.bottom);
+ path.lineTo(bottleRect.centerX() + bottleBottomWidth / 2.0f, bottleRect.bottom);
+
+ //draw the right side of the bottle's body
+ path.addArc(bodyRect, 60, -135);
+
+ //draw the right side of the bottleneck decoration
+ path.lineTo(bottleRect.centerX() + bottleneckWidth * 0.5f, bottleRect.top + bottleneckDecorationHeight);
+ path.quadTo(bottleRect.centerX() + bottleneckDecorationWidth * 0.5f + bottleneckWidth * 0.15f, bottleRect.top + bottleneckDecorationHeight * 0.5f,
+ bottleRect.centerX() + bottleneckDecorationWidth * 0.5f, bottleRect.top);
+
+ return path;
+ }
+
+ private Path createWaterPath(RectF waterRect, float progress) {
+ Path path = new Path();
+
+ path.moveTo(waterRect.left, waterRect.top);
+
+ //Similar to the way draw the bottle's bottom sides
+ float radius = (waterRect.width() - mStrokeWidth) / 2.0f;
+ float centerY = waterRect.bottom - 0.86f * radius;
+ float bottleBottomWidth = waterRect.width() / 2.0f;
+ RectF bodyRect = new RectF(waterRect.left, centerY - radius, waterRect.right, centerY + radius);
+
+ path.addArc(bodyRect, 187.5f, -67.5f);
+ path.lineTo(waterRect.centerX() - bottleBottomWidth / 2.0f, waterRect.bottom);
+ path.lineTo(waterRect.centerX() + bottleBottomWidth / 2.0f, waterRect.bottom);
+ path.addArc(bodyRect, 60, -67.5f);
+
+ //draw the water waves
+ float cubicXChangeSize = waterRect.width() * 0.35f * progress;
+ float cubicYChangeSize = waterRect.height() * 1.2f * progress;
+
+ path.cubicTo(waterRect.left + waterRect.width() * 0.80f - cubicXChangeSize, waterRect.top - waterRect.height() * 1.2f + cubicYChangeSize,
+ waterRect.left + waterRect.width() * 0.55f - cubicXChangeSize, waterRect.top - cubicYChangeSize,
+ waterRect.left, waterRect.top - mStrokeWidth / 2.0f);
+
+ path.lineTo(waterRect.left, waterRect.top);
+
+ return path;
+ }
+
+ private void initWaterDropHolders(RectF bottleRect, RectF waterRect) {
+ float bottleRadius = bottleRect.width() / 2.0f;
+ float lowestWaterPointY = waterRect.top;
+ float twoSidesInterval = 0.2f * bottleRect.width();
+ float atLeastDelayDuration = 0.1f;
+
+ float unitDuration = 0.1f;
+ float delayDurationRange = 0.6f;
+ int radiusRandomRange = MAX_WATER_DROP_RADIUS - MIN_WATER_DROP_RADIUS;
+ float currentXRandomRange = bottleRect.width() * 0.6f;
+
+ for (int i = 0; i < DEFAULT_WATER_DROP_COUNT; i++) {
+ WaterDropHolder waterDropHolder = new WaterDropHolder();
+ waterDropHolder.mRadius = MIN_WATER_DROP_RADIUS + mRandom.nextInt(radiusRandomRange);
+ waterDropHolder.mInitX = bottleRect.left + twoSidesInterval + mRandom.nextFloat() * currentXRandomRange;
+ waterDropHolder.mInitY = lowestWaterPointY + waterDropHolder.mRadius / 2.0f;
+ waterDropHolder.mRiseHeight = getMaxRiseHeight(bottleRadius, waterDropHolder.mRadius, waterDropHolder.mInitX - bottleRect.left)
+ * (0.2f + 0.8f * mRandom.nextFloat());
+ waterDropHolder.mDelayDuration = atLeastDelayDuration + mRandom.nextFloat() * delayDurationRange;
+ waterDropHolder.mDuration = waterDropHolder.mRiseHeight / bottleRadius * unitDuration;
+
+ mWaterDropHolders.add(waterDropHolder);
+ }
+ }
+
+ private float getMaxRiseHeight(float bottleRadius, float waterDropRadius, float currentX) {
+ float coordinateX = currentX - bottleRadius;
+ float bottleneckRadius = bottleRadius * 0.3f;
+ if (coordinateX - waterDropRadius > -bottleneckRadius
+ && coordinateX + waterDropRadius < bottleneckRadius) {
+ return bottleRadius * 2.0f;
+ }
+
+ return (float) (Math.sqrt(Math.pow(bottleRadius, 2.0f) - Math.pow(coordinateX, 2.0f)) - waterDropRadius);
+ }
+
+ @Override
+ protected void setAlpha(int alpha) {
+ mPaint.setAlpha(alpha);
+
+ }
+
+ @Override
+ protected void setColorFilter(ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+
+ }
+
+ @Override
+ protected void reset() {
+ }
+
+ private class WaterDropHolder {
+ public float mCurrentY;
+
+ public float mInitX;
+ public float mInitY;
+ public float mDelayDuration;
+ public float mRiseHeight;
+
+ public float mRadius;
+ public float mDuration;
+
+ public boolean mNeedDraw;
+ }
+
+ public static class Builder {
+ private Context mContext;
+
+ public Builder(Context mContext) {
+ this.mContext = mContext;
+ }
+
+ public WaterBottleLoadingRenderer build() {
+ WaterBottleLoadingRenderer loadingRenderer = new WaterBottleLoadingRenderer(mContext);
+ return loadingRenderer;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/scenery/DayNightLoadingRenderer.java b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/scenery/DayNightLoadingRenderer.java
new file mode 100644
index 00000000..9d57c8f0
--- /dev/null
+++ b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/scenery/DayNightLoadingRenderer.java
@@ -0,0 +1,361 @@
+package app.dinus.com.loadingdrawable.render.scenery;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.DisplayMetrics;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+
+import androidx.interpolator.view.animation.FastOutLinearInInterpolator;
+import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+import app.dinus.com.loadingdrawable.DensityUtil;
+import app.dinus.com.loadingdrawable.render.LoadingRenderer;
+
+public class DayNightLoadingRenderer extends LoadingRenderer {
+ private static final Interpolator MATERIAL_INTERPOLATOR = new FastOutSlowInInterpolator();
+ private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
+ private static final Interpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator();
+ private static final Interpolator ACCELERATE_INTERPOLATOR = new AccelerateInterpolator();
+ private static final Interpolator FASTOUTLINEARIN_INTERPOLATOR = new FastOutLinearInInterpolator();
+
+ private static final Interpolator[] INTERPOLATORS = new Interpolator[]{LINEAR_INTERPOLATOR,
+ DECELERATE_INTERPOLATOR, ACCELERATE_INTERPOLATOR, FASTOUTLINEARIN_INTERPOLATOR, MATERIAL_INTERPOLATOR};
+
+ private static final int MAX_ALPHA = 255;
+ private static final int DEGREE_360 = 360;
+ private static final int MAX_SUN_RAY_COUNT = 12;
+
+ private static final float DEFAULT_WIDTH = 200.0f;
+ private static final float DEFAULT_HEIGHT = 150.0f;
+ private static final float DEFAULT_STROKE_WIDTH = 2.5f;
+ private static final float DEFAULT_SUN$MOON_RADIUS = 12.0f;
+ private static final float DEFAULT_STAR_RADIUS = 2.5f;
+ private static final float DEFAULT_SUN_RAY_LENGTH = 10.0f;
+ private static final float DEFAULT_SUN_RAY_OFFSET = 3.0f;
+
+ public static final float STAR_RISE_PROGRESS_OFFSET = 0.2f;
+ public static final float STAR_DECREASE_PROGRESS_OFFSET = 0.8f;
+ public static final float STAR_FLASH_PROGRESS_PERCENTAGE = 0.2f;
+
+ private static final float MAX_SUN_ROTATE_DEGREE = DEGREE_360 / 3.0f;
+ private static final float MAX_MOON_ROTATE_DEGREE = DEGREE_360 / 6.0f;
+ private static final float SUN_RAY_INTERVAL_DEGREE = DEGREE_360 / 3.0f / 55;
+
+ private static final float SUN_RISE_DURATION_OFFSET = 0.143f;
+ private static final float SUN_ROTATE_DURATION_OFFSET = 0.492f;
+ private static final float SUN_DECREASE_DURATION_OFFSET = 0.570f;
+ private static final float MOON_RISE_DURATION_OFFSET = 0.713f;
+ private static final float MOON_DECREASE_START_DURATION_OFFSET = 0.935f;
+ private static final float MOON_DECREASE_END_DURATION_OFFSET = 1.0f;
+ private static final float STAR_RISE_START_DURATION_OFFSET = 0.684f;
+ private static final float STAR_DECREASE_START_DURATION_OFFSET = 1.0f;
+
+ private static final int DEFAULT_COLOR = Color.parseColor("#ff21fd8e");
+
+ private static final long ANIMATION_DURATION = 5111;
+
+ private final Random mRandom = new Random();
+ private final List mStarHolders = new ArrayList<>();
+
+ private final Paint mPaint = new Paint();
+ private final RectF mTempBounds = new RectF();
+
+ private final Animator.AnimatorListener mAnimatorListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationRepeat(Animator animator) {
+ super.onAnimationRepeat(animator);
+ }
+ };
+
+ private int mCurrentColor;
+
+ private float mMaxStarOffsets;
+
+ private float mStrokeWidth;
+ private float mStarRadius;
+ private float mSun$MoonRadius;
+ private float mSunCoordinateY;
+ private float mMoonCoordinateY;
+ //the y-coordinate of the end point of the sun ray
+ private float mSunRayEndCoordinateY;
+ //the y-coordinate of the start point of the sun ray
+ private float mSunRayStartCoordinateY;
+ //the y-coordinate of the start point of the sun
+ private float mInitSun$MoonCoordinateY;
+ //the distance from the outside to the center of the drawable
+ private float mMaxSun$MoonRiseDistance;
+
+ private float mSunRayRotation;
+ private float mMoonRotation;
+
+ //the number of sun's rays is increasing
+ private boolean mIsExpandSunRay;
+ private boolean mShowStar;
+
+ private int mSunRayCount;
+
+ private DayNightLoadingRenderer(Context context) {
+ super(context);
+ init(context);
+ setupPaint();
+ addRenderListener(mAnimatorListener);
+ }
+
+ private void init(Context context) {
+ mWidth = DensityUtil.dip2px(context, DEFAULT_WIDTH);
+ mHeight = DensityUtil.dip2px(context, DEFAULT_HEIGHT);
+ mStrokeWidth = DensityUtil.dip2px(context, DEFAULT_STROKE_WIDTH);
+
+ mStarRadius = DensityUtil.dip2px(context, DEFAULT_STAR_RADIUS);
+ mSun$MoonRadius = DensityUtil.dip2px(context, DEFAULT_SUN$MOON_RADIUS);
+ mInitSun$MoonCoordinateY = mHeight + mSun$MoonRadius + mStrokeWidth * 2.0f;
+ mMaxSun$MoonRiseDistance = mHeight / 2.0f + mSun$MoonRadius;
+
+ mSunRayStartCoordinateY = mInitSun$MoonCoordinateY - mMaxSun$MoonRiseDistance //the center
+ - mSun$MoonRadius //sub the radius
+ - mStrokeWidth // sub the with the sun circle
+ - DensityUtil.dip2px(context, DEFAULT_SUN_RAY_OFFSET); //sub the interval between the sun and the sun ray
+
+ //add strokeWidth * 2.0f because the stroke cap is Paint.Cap.ROUND
+ mSunRayEndCoordinateY = mSunRayStartCoordinateY - DensityUtil.dip2px(context, DEFAULT_SUN_RAY_LENGTH)
+ + mStrokeWidth;
+
+ mSunCoordinateY = mInitSun$MoonCoordinateY;
+ mMoonCoordinateY = mInitSun$MoonCoordinateY;
+
+ mCurrentColor = DEFAULT_COLOR;
+
+ mDuration = ANIMATION_DURATION;
+ }
+
+ private void setupPaint() {
+ mPaint.setAntiAlias(true);
+ mPaint.setStrokeWidth(mStrokeWidth);
+ mPaint.setStrokeJoin(Paint.Join.ROUND);
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setStrokeCap(Paint.Cap.ROUND);
+ }
+
+ @Override
+ protected void draw(Canvas canvas, Rect bounds) {
+ int saveCount = canvas.save();
+
+ RectF arcBounds = mTempBounds;
+ arcBounds.set(bounds);
+
+ mPaint.setAlpha(MAX_ALPHA);
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setColor(mCurrentColor);
+
+ if (mSunCoordinateY < mInitSun$MoonCoordinateY) {
+ canvas.drawCircle(arcBounds.centerX(), mSunCoordinateY, mSun$MoonRadius, mPaint);
+ }
+
+ if (mMoonCoordinateY < mInitSun$MoonCoordinateY) {
+ int moonSaveCount = canvas.save();
+ canvas.rotate(mMoonRotation, arcBounds.centerX(), mMoonCoordinateY);
+ canvas.drawPath(createMoonPath(arcBounds.centerX(), mMoonCoordinateY), mPaint);
+ canvas.restoreToCount(moonSaveCount);
+ }
+
+ for (int i = 0; i < mSunRayCount; i++) {
+ int sunRaySaveCount = canvas.save();
+ //rotate 45 degrees can change the direction of 0 degrees to 1:30 clock
+ //-mSunRayRotation means reverse rotation
+ canvas.rotate(45 - mSunRayRotation
+ + (mIsExpandSunRay ? i : MAX_SUN_RAY_COUNT - i) * DEGREE_360 / MAX_SUN_RAY_COUNT,
+ arcBounds.centerX(), mSunCoordinateY);
+
+ canvas.drawLine(arcBounds.centerX(), mSunRayStartCoordinateY, arcBounds.centerX(), mSunRayEndCoordinateY, mPaint);
+ canvas.restoreToCount(sunRaySaveCount);
+ }
+
+ if (mShowStar) {
+ if (mStarHolders.isEmpty()) {
+ initStarHolders(arcBounds);
+ }
+
+ for (int i = 0; i < mStarHolders.size(); i++) {
+ mPaint.setStyle(Paint.Style.FILL);
+ mPaint.setAlpha(mStarHolders.get(i).mAlpha);
+ canvas.drawCircle(mStarHolders.get(i).mCurrentPoint.x, mStarHolders.get(i).mCurrentPoint.y, mStarRadius, mPaint);
+ }
+ }
+
+ canvas.restoreToCount(saveCount);
+ }
+
+ @Override
+ protected void computeRender(float renderProgress) {
+ if (renderProgress <= SUN_RISE_DURATION_OFFSET) {
+ float sunRiseProgress = renderProgress / SUN_RISE_DURATION_OFFSET;
+ mSunCoordinateY = mInitSun$MoonCoordinateY - mMaxSun$MoonRiseDistance * MATERIAL_INTERPOLATOR.getInterpolation(sunRiseProgress);
+ mMoonCoordinateY = mInitSun$MoonCoordinateY;
+ mShowStar = false;
+ }
+
+ if (renderProgress <= SUN_ROTATE_DURATION_OFFSET && renderProgress > SUN_RISE_DURATION_OFFSET) {
+ float sunRotateProgress = (renderProgress - SUN_RISE_DURATION_OFFSET) / (SUN_ROTATE_DURATION_OFFSET - SUN_RISE_DURATION_OFFSET);
+ mSunRayRotation = sunRotateProgress * MAX_SUN_ROTATE_DEGREE;
+
+ if ((int) (mSunRayRotation / SUN_RAY_INTERVAL_DEGREE) <= MAX_SUN_RAY_COUNT) {
+ mIsExpandSunRay = true;
+ mSunRayCount = (int) (mSunRayRotation / SUN_RAY_INTERVAL_DEGREE);
+ }
+
+ if ((int) ((MAX_SUN_ROTATE_DEGREE - mSunRayRotation) / SUN_RAY_INTERVAL_DEGREE) <= MAX_SUN_RAY_COUNT) {
+ mIsExpandSunRay = false;
+ mSunRayCount = (int) ((MAX_SUN_ROTATE_DEGREE - mSunRayRotation) / SUN_RAY_INTERVAL_DEGREE);
+ }
+ }
+
+ if (renderProgress <= SUN_DECREASE_DURATION_OFFSET && renderProgress > SUN_ROTATE_DURATION_OFFSET) {
+ float sunDecreaseProgress = (renderProgress - SUN_ROTATE_DURATION_OFFSET) / (SUN_DECREASE_DURATION_OFFSET - SUN_ROTATE_DURATION_OFFSET);
+ mSunCoordinateY = mInitSun$MoonCoordinateY - mMaxSun$MoonRiseDistance * (1.0f - ACCELERATE_INTERPOLATOR.getInterpolation(sunDecreaseProgress));
+ }
+
+ if (renderProgress <= MOON_RISE_DURATION_OFFSET && renderProgress > SUN_DECREASE_DURATION_OFFSET) {
+ float moonRiseProgress = (renderProgress - SUN_DECREASE_DURATION_OFFSET) / (MOON_RISE_DURATION_OFFSET - SUN_DECREASE_DURATION_OFFSET);
+ mMoonRotation = MATERIAL_INTERPOLATOR.getInterpolation(moonRiseProgress) * MAX_MOON_ROTATE_DEGREE;
+ mSunCoordinateY = mInitSun$MoonCoordinateY;
+ mMoonCoordinateY = mInitSun$MoonCoordinateY - mMaxSun$MoonRiseDistance * MATERIAL_INTERPOLATOR.getInterpolation(moonRiseProgress);
+ }
+
+ if (renderProgress <= STAR_DECREASE_START_DURATION_OFFSET && renderProgress > STAR_RISE_START_DURATION_OFFSET) {
+ float starProgress = (renderProgress - STAR_RISE_START_DURATION_OFFSET) / (STAR_DECREASE_START_DURATION_OFFSET - STAR_RISE_START_DURATION_OFFSET);
+ if (starProgress <= STAR_RISE_PROGRESS_OFFSET) {
+ for (int i = 0; i < mStarHolders.size(); i++) {
+ StarHolder starHolder = mStarHolders.get(i);
+ starHolder.mCurrentPoint.y = starHolder.mPoint.y - (1.0f - starHolder.mInterpolator.getInterpolation(starProgress * 5.0f)) * (mMaxStarOffsets * 0.65f);
+ starHolder.mCurrentPoint.x = starHolder.mPoint.x;
+ }
+ }
+
+ if (starProgress > STAR_RISE_PROGRESS_OFFSET && starProgress < STAR_DECREASE_PROGRESS_OFFSET) {
+ for (int i = 0; i < mStarHolders.size(); i++) {
+ StarHolder starHolder = mStarHolders.get(i);
+ if (starHolder.mFlashOffset < starProgress && starProgress < starHolder.mFlashOffset + STAR_FLASH_PROGRESS_PERCENTAGE) {
+ starHolder.mAlpha = (int) (MAX_ALPHA * MATERIAL_INTERPOLATOR.getInterpolation(
+ Math.abs(starProgress - (starHolder.mFlashOffset + STAR_FLASH_PROGRESS_PERCENTAGE / 2.0f)) / (STAR_FLASH_PROGRESS_PERCENTAGE / 2.0f)));
+ }
+ }
+ }
+
+ if (starProgress >= STAR_DECREASE_PROGRESS_OFFSET) {
+ for (int i = 0; i < mStarHolders.size(); i++) {
+ StarHolder starHolder = mStarHolders.get(i);
+ starHolder.mCurrentPoint.y = starHolder.mPoint.y + starHolder.mInterpolator.getInterpolation((starProgress - STAR_DECREASE_PROGRESS_OFFSET) * 5.0f) * mMaxStarOffsets;
+ starHolder.mCurrentPoint.x = starHolder.mPoint.x;
+ }
+ }
+ mShowStar = true;
+ }
+
+ if (renderProgress <= MOON_DECREASE_END_DURATION_OFFSET && renderProgress > MOON_DECREASE_START_DURATION_OFFSET) {
+ float moonDecreaseProgress = (renderProgress - MOON_DECREASE_START_DURATION_OFFSET) / (MOON_DECREASE_END_DURATION_OFFSET - MOON_DECREASE_START_DURATION_OFFSET);
+ mMoonCoordinateY = mInitSun$MoonCoordinateY - mMaxSun$MoonRiseDistance * (1.0f - ACCELERATE_INTERPOLATOR.getInterpolation(moonDecreaseProgress));
+ }
+ }
+
+ @Override
+ protected void setAlpha(int alpha) {
+ mPaint.setAlpha(alpha);
+
+ }
+
+ @Override
+ protected void setColorFilter(ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+
+ }
+
+ @Override
+ protected void reset() {
+ }
+
+ private void initStarHolders(RectF currentBounds) {
+ mStarHolders.add(new StarHolder(0.3f, new PointF(currentBounds.left + currentBounds.width() * 0.175f,
+ currentBounds.top + currentBounds.height() * 0.0934f)));
+ mStarHolders.add(new StarHolder(0.2f, new PointF(currentBounds.left + currentBounds.width() * 0.175f,
+ currentBounds.top + currentBounds.height() * 0.62f)));
+ mStarHolders.add(new StarHolder(0.2f, new PointF(currentBounds.left + currentBounds.width() * 0.2525f,
+ currentBounds.top + currentBounds.height() * 0.43f)));
+ mStarHolders.add(new StarHolder(0.5f, new PointF(currentBounds.left + currentBounds.width() * 0.4075f,
+ currentBounds.top + currentBounds.height() * 0.0934f)));
+ mStarHolders.add(new StarHolder(new PointF(currentBounds.left + currentBounds.width() * 0.825f,
+ currentBounds.top + currentBounds.height() * 0.04f)));
+ mStarHolders.add(new StarHolder(new PointF(currentBounds.left + currentBounds.width() * 0.7075f,
+ currentBounds.top + currentBounds.height() * 0.147f)));
+ mStarHolders.add(new StarHolder(new PointF(currentBounds.left + currentBounds.width() * 0.3475f,
+ currentBounds.top + currentBounds.height() * 0.2567f)));
+ mStarHolders.add(new StarHolder(0.6f, new PointF(currentBounds.left + currentBounds.width() * 0.5825f,
+ currentBounds.top + currentBounds.height() * 0.277f)));
+ mStarHolders.add(new StarHolder(new PointF(currentBounds.left + currentBounds.width() * 0.84f,
+ currentBounds.top + currentBounds.height() * 0.32f)));
+ mStarHolders.add(new StarHolder(new PointF(currentBounds.left + currentBounds.width() * 0.8f,
+ currentBounds.top + currentBounds.height() / 0.502f)));
+ mStarHolders.add(new StarHolder(0.6f, new PointF(currentBounds.left + currentBounds.width() * 0.7f,
+ currentBounds.top + currentBounds.height() * 0.473f)));
+
+ mMaxStarOffsets = currentBounds.height();
+ }
+
+ private Path createMoonPath(float moonCenterX, float moonCenterY) {
+ RectF moonRectF = new RectF(moonCenterX - mSun$MoonRadius, moonCenterY - mSun$MoonRadius,
+ moonCenterX + mSun$MoonRadius, moonCenterY + mSun$MoonRadius);
+ Path path = new Path();
+ path.addArc(moonRectF, -90, 180);
+ path.quadTo(moonCenterX + mSun$MoonRadius / 2.0f, moonCenterY, moonCenterX, moonCenterY - mSun$MoonRadius);
+ return path;
+ }
+
+ private class StarHolder {
+ public int mAlpha;
+ public PointF mCurrentPoint;
+
+ public final PointF mPoint;
+ public final float mFlashOffset;
+ public final Interpolator mInterpolator;
+
+ public StarHolder(PointF point) {
+ this(1.0f, point);
+ }
+
+ public StarHolder(float flashOffset, PointF mPoint) {
+ this.mAlpha = MAX_ALPHA;
+ this.mCurrentPoint = new PointF();
+ this.mPoint = mPoint;
+ this.mFlashOffset = flashOffset;
+ this.mInterpolator = INTERPOLATORS[mRandom.nextInt(INTERPOLATORS.length)];
+ }
+ }
+
+ public static class Builder {
+ private Context mContext;
+
+ public Builder(Context mContext) {
+ this.mContext = mContext;
+ }
+
+ public DayNightLoadingRenderer build() {
+ DayNightLoadingRenderer loadingRenderer = new DayNightLoadingRenderer(mContext);
+ return loadingRenderer;
+ }
+ }
+}
diff --git a/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/scenery/ElectricFanLoadingRenderer.java b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/scenery/ElectricFanLoadingRenderer.java
new file mode 100644
index 00000000..dcd02d96
--- /dev/null
+++ b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/scenery/ElectricFanLoadingRenderer.java
@@ -0,0 +1,474 @@
+package app.dinus.com.loadingdrawable.render.scenery;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.TypeEvaluator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.util.DisplayMetrics;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+
+import androidx.annotation.IntDef;
+import androidx.interpolator.view.animation.FastOutLinearInInterpolator;
+import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+import app.dinus.com.loadingdrawable.DensityUtil;
+import app.dinus.com.loadingdrawable.R;
+import app.dinus.com.loadingdrawable.render.LoadingRenderer;
+
+public class ElectricFanLoadingRenderer extends LoadingRenderer {
+ private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
+ private static final Interpolator MATERIAL_INTERPOLATOR = new FastOutSlowInInterpolator();
+ private static final Interpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator();
+ private static final Interpolator ACCELERATE_INTERPOLATOR = new AccelerateInterpolator();
+ private static final Interpolator FASTOUTLINEARIN_INTERPOLATOR = new FastOutLinearInInterpolator();
+
+ private static final Interpolator[] INTERPOLATORS = new Interpolator[]{LINEAR_INTERPOLATOR,
+ DECELERATE_INTERPOLATOR, ACCELERATE_INTERPOLATOR, FASTOUTLINEARIN_INTERPOLATOR, MATERIAL_INTERPOLATOR};
+ private static final List mLeafHolders = new ArrayList<>();
+ private static final Random mRandom = new Random();
+ private final Animator.AnimatorListener mAnimatorListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationRepeat(Animator animator) {
+ super.onAnimationRepeat(animator);
+ reset();
+ }
+ };
+
+ public static final int MODE_NORMAL = 0;
+ public static final int MODE_LEAF_COUNT = 1;
+
+ @IntDef({MODE_NORMAL, MODE_LEAF_COUNT})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface MODE {
+ }
+
+ private static final String PERCENTAGE_100 = "100%";
+
+ private static final long ANIMATION_DURATION = 7333;
+
+ private static final int LEAF_COUNT = 28;
+ private static final int DEGREE_180 = 180;
+ private static final int DEGREE_360 = 360;
+ private static final int FULL_GROUP_ROTATION = (int) (5.25f * DEGREE_360);
+
+ private static final int DEFAULT_PROGRESS_COLOR = 0xfffca72e;
+ private static final int DEFAULT_PROGRESS_BGCOLOR = 0xfffcd49f;
+ private static final int DEFAULT_ELECTRIC_FAN_BGCOLOR = 0xfffccc59;
+ private static final int DEFAULT_ELECTRIC_FAN_OUTLINE_COLOR = Color.WHITE;
+
+ private static final float DEFAULT_WIDTH = 182.0f;
+ private static final float DEFAULT_HEIGHT = 65.0f;
+ private static final float DEFAULT_TEXT_SIZE = 11.0f;
+ private static final float DEFAULT_STROKE_WIDTH = 2.0f;
+ private static final float DEFAULT_STROKE_INTERVAL = .2f;
+ private static final float DEFAULT_CENTER_RADIUS = 16.0f;
+ private static final float DEFAULT_PROGRESS_CENTER_RADIUS = 11.0f;
+
+ private static final float DEFAULT_LEAF_FLY_DURATION_FACTOR = 0.1f;
+
+ private static final float LEAF_CREATE_DURATION_INTERVAL = 1.0f / LEAF_COUNT;
+ private static final float DECELERATE_DURATION_PERCENTAGE = 0.4f;
+ private static final float ACCELERATE_DURATION_PERCENTAGE = 0.6f;
+
+ private final Paint mPaint = new Paint();
+ private final RectF mTempBounds = new RectF();
+ private final RectF mCurrentProgressBounds = new RectF();
+
+ private float mTextSize;
+ private float mStrokeXInset;
+ private float mStrokeYInset;
+ private float mProgressCenterRadius;
+
+ private float mScale;
+ private float mRotation;
+ private float mProgress;
+
+ private float mNextLeafCreateThreshold;
+
+ private int mProgressColor;
+ private int mProgressBgColor;
+ private int mElectricFanBgColor;
+ private int mElectricFanOutlineColor;
+
+ private float mStrokeWidth;
+ private float mCenterRadius;
+
+ @MODE
+ private int mMode;
+ private int mCurrentLeafCount;
+
+ private Drawable mLeafDrawable;
+ private Drawable mLoadingDrawable;
+ private Drawable mElectricFanDrawable;
+
+ private ElectricFanLoadingRenderer(Context context) {
+ super(context);
+ init(context);
+ setupPaint();
+ addRenderListener(mAnimatorListener);
+ }
+
+ private void init(Context context) {
+ mMode = MODE_NORMAL;
+
+ mWidth = DensityUtil.dip2px(context, DEFAULT_WIDTH);
+ mHeight = DensityUtil.dip2px(context, DEFAULT_HEIGHT);
+ mTextSize = DensityUtil.dip2px(context, DEFAULT_TEXT_SIZE);
+ mStrokeWidth = DensityUtil.dip2px(context, DEFAULT_STROKE_WIDTH);
+ mCenterRadius = DensityUtil.dip2px(context, DEFAULT_CENTER_RADIUS);
+ mProgressCenterRadius = DensityUtil.dip2px(context, DEFAULT_PROGRESS_CENTER_RADIUS);
+
+ mProgressColor = DEFAULT_PROGRESS_COLOR;
+ mProgressBgColor = DEFAULT_PROGRESS_BGCOLOR;
+ mElectricFanBgColor = DEFAULT_ELECTRIC_FAN_BGCOLOR;
+ mElectricFanOutlineColor = DEFAULT_ELECTRIC_FAN_OUTLINE_COLOR;
+
+ mLeafDrawable = context.getResources().getDrawable(R.drawable.ic_leaf);
+ mLoadingDrawable = context.getResources().getDrawable(R.drawable.ic_loading);
+ mElectricFanDrawable = context.getResources().getDrawable(R.drawable.ic_eletric_fan);
+
+ mDuration = ANIMATION_DURATION;
+ setInsets((int) mWidth, (int) mHeight);
+ }
+
+ private void setupPaint() {
+ mPaint.setAntiAlias(true);
+ mPaint.setStrokeWidth(mStrokeWidth);
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setStrokeCap(Paint.Cap.ROUND);
+ }
+
+ @Override
+ protected void draw(Canvas canvas, Rect bounds) {
+ int saveCount = canvas.save();
+
+ RectF arcBounds = mTempBounds;
+ arcBounds.set(bounds);
+ arcBounds.inset(mStrokeXInset, mStrokeYInset);
+
+ mCurrentProgressBounds.set(arcBounds.left, arcBounds.bottom - 2 * mCenterRadius,
+ arcBounds.right, arcBounds.bottom);
+
+ //draw loading drawable
+ mLoadingDrawable.setBounds((int) arcBounds.centerX() - mLoadingDrawable.getIntrinsicWidth() / 2,
+ 0,
+ (int) arcBounds.centerX() + mLoadingDrawable.getIntrinsicWidth() / 2,
+ mLoadingDrawable.getIntrinsicHeight());
+ mLoadingDrawable.draw(canvas);
+
+ //draw progress background
+ float progressInset = mCenterRadius - mProgressCenterRadius;
+ RectF progressRect = new RectF(mCurrentProgressBounds);
+ //sub DEFAULT_STROKE_INTERVAL, otherwise will have a interval between progress background and progress outline
+ progressRect.inset(progressInset - DEFAULT_STROKE_INTERVAL, progressInset - DEFAULT_STROKE_INTERVAL);
+ mPaint.setColor(mProgressBgColor);
+ mPaint.setStyle(Paint.Style.FILL);
+ canvas.drawRoundRect(progressRect, mProgressCenterRadius, mProgressCenterRadius, mPaint);
+
+ //draw progress
+ mPaint.setColor(mProgressColor);
+ mPaint.setStyle(Paint.Style.FILL);
+ canvas.drawPath(createProgressPath(mProgress, mProgressCenterRadius, progressRect), mPaint);
+
+ //draw leaves
+ for (int i = 0; i < mLeafHolders.size(); i++) {
+ int leafSaveCount = canvas.save();
+ LeafHolder leafHolder = mLeafHolders.get(i);
+ Rect leafBounds = leafHolder.mLeafRect;
+
+ canvas.rotate(leafHolder.mLeafRotation, leafBounds.centerX(), leafBounds.centerY());
+ mLeafDrawable.setBounds(leafBounds);
+ mLeafDrawable.draw(canvas);
+
+ canvas.restoreToCount(leafSaveCount);
+ }
+
+ //draw progress background outline,
+ //after drawing the leaves and then draw the outline of the progress background can
+ //prevent the leaves from flying to the outside
+ RectF progressOutlineRect = new RectF(mCurrentProgressBounds);
+ float progressOutlineStrokeInset = (mCenterRadius - mProgressCenterRadius) / 2.0f;
+ progressOutlineRect.inset(progressOutlineStrokeInset, progressOutlineStrokeInset);
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setColor(mProgressBgColor);
+ mPaint.setStrokeWidth(mCenterRadius - mProgressCenterRadius);
+ canvas.drawRoundRect(progressOutlineRect, mCenterRadius, mCenterRadius, mPaint);
+
+ //draw electric fan outline
+ float electricFanCenterX = arcBounds.right - mCenterRadius;
+ float electricFanCenterY = arcBounds.bottom - mCenterRadius;
+
+ mPaint.setColor(mElectricFanOutlineColor);
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setStrokeWidth(mStrokeWidth);
+ canvas.drawCircle(arcBounds.right - mCenterRadius, arcBounds.bottom - mCenterRadius,
+ mCenterRadius - mStrokeWidth / 2.0f, mPaint);
+
+ //draw electric background
+ mPaint.setColor(mElectricFanBgColor);
+ mPaint.setStyle(Paint.Style.FILL);
+ canvas.drawCircle(arcBounds.right - mCenterRadius, arcBounds.bottom - mCenterRadius,
+ mCenterRadius - mStrokeWidth + DEFAULT_STROKE_INTERVAL, mPaint);
+
+ //draw electric fan
+ int rotateSaveCount = canvas.save();
+ canvas.rotate(mRotation, electricFanCenterX, electricFanCenterY);
+ mElectricFanDrawable.setBounds((int) (electricFanCenterX - mElectricFanDrawable.getIntrinsicWidth() / 2 * mScale),
+ (int) (electricFanCenterY - mElectricFanDrawable.getIntrinsicHeight() / 2 * mScale),
+ (int) (electricFanCenterX + mElectricFanDrawable.getIntrinsicWidth() / 2 * mScale),
+ (int) (electricFanCenterY + mElectricFanDrawable.getIntrinsicHeight() / 2 * mScale));
+ mElectricFanDrawable.draw(canvas);
+ canvas.restoreToCount(rotateSaveCount);
+
+ //draw 100% text
+ if (mScale < 1.0f) {
+ mPaint.setTextSize(mTextSize * (1 - mScale));
+ mPaint.setColor(mElectricFanOutlineColor);
+ Rect textRect = new Rect();
+ mPaint.getTextBounds(PERCENTAGE_100, 0, PERCENTAGE_100.length(), textRect);
+ canvas.drawText(PERCENTAGE_100, electricFanCenterX - textRect.width() / 2.0f,
+ electricFanCenterY + textRect.height() / 2.0f, mPaint);
+ }
+
+ canvas.restoreToCount(saveCount);
+ }
+
+ private Path createProgressPath(float progress, float circleRadius, RectF progressRect) {
+ RectF arcProgressRect = new RectF(progressRect.left, progressRect.top, progressRect.left + circleRadius * 2, progressRect.bottom);
+ RectF rectProgressRect = null;
+
+ float progressWidth = progress * progressRect.width();
+ float progressModeWidth = mMode == MODE_LEAF_COUNT ?
+ (float) mCurrentLeafCount / (float) LEAF_COUNT * progressRect.width() : progress * progressRect.width();
+
+ float swipeAngle = DEGREE_180;
+ //the left half circle of the progressbar
+ if (progressModeWidth < circleRadius) {
+ swipeAngle = progressModeWidth / circleRadius * DEGREE_180;
+ }
+
+ //the center rect of the progressbar
+ if (progressModeWidth < progressRect.width() - circleRadius && progressModeWidth >= circleRadius) {
+ rectProgressRect = new RectF(progressRect.left + circleRadius, progressRect.top, progressRect.left + progressModeWidth, progressRect.bottom);
+ }
+
+ //the right half circle of the progressbar
+ if (progressWidth >= progressRect.width() - circleRadius) {
+ rectProgressRect = new RectF(progressRect.left + circleRadius, progressRect.top, progressRect.right - circleRadius, progressRect.bottom);
+ mScale = (progressRect.width() - progressWidth) / circleRadius;
+ }
+
+ //the left of the right half circle
+ if (progressWidth < progressRect.width() - circleRadius) {
+ mRotation = (progressWidth / (progressRect.width() - circleRadius)) * FULL_GROUP_ROTATION % DEGREE_360;
+
+ RectF leafRect = new RectF(progressRect.left + progressWidth, progressRect.top, progressRect.right - circleRadius, progressRect.bottom);
+ addLeaf(progress, leafRect);
+ }
+
+ Path path = new Path();
+ path.addArc(arcProgressRect, DEGREE_180 - swipeAngle / 2, swipeAngle);
+
+ if (rectProgressRect != null) {
+ path.addRect(rectProgressRect, Path.Direction.CW);
+ }
+
+ return path;
+ }
+
+ @Override
+ protected void computeRender(float renderProgress) {
+ if (renderProgress < DECELERATE_DURATION_PERCENTAGE) {
+ mProgress = DECELERATE_INTERPOLATOR.getInterpolation(renderProgress / DECELERATE_DURATION_PERCENTAGE) * DECELERATE_DURATION_PERCENTAGE;
+ } else {
+ mProgress = ACCELERATE_INTERPOLATOR.getInterpolation((renderProgress - DECELERATE_DURATION_PERCENTAGE) / ACCELERATE_DURATION_PERCENTAGE) * ACCELERATE_DURATION_PERCENTAGE + DECELERATE_DURATION_PERCENTAGE;
+ }
+ }
+
+ @Override
+ protected void setAlpha(int alpha) {
+ mPaint.setAlpha(alpha);
+
+ }
+
+ @Override
+ protected void setColorFilter(ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+
+ }
+
+ @Override
+ protected void reset() {
+ mScale = 1.0f;
+ mCurrentLeafCount = 0;
+ mNextLeafCreateThreshold = 0.0f;
+ mLeafHolders.clear();
+ }
+
+ protected void setInsets(int width, int height) {
+ final float minEdge = (float) Math.min(width, height);
+ float insetXs;
+ if (mCenterRadius <= 0 || minEdge < 0) {
+ insetXs = (float) Math.ceil(mCenterRadius / 2.0f);
+ } else {
+ insetXs = mCenterRadius;
+ }
+ mStrokeYInset = (float) Math.ceil(mCenterRadius / 2.0f);
+ mStrokeXInset = insetXs;
+ }
+
+ private void addLeaf(float progress, RectF leafFlyRect) {
+ if (progress < mNextLeafCreateThreshold) {
+ return;
+ }
+ mNextLeafCreateThreshold += LEAF_CREATE_DURATION_INTERVAL;
+
+ LeafHolder leafHolder = new LeafHolder();
+ mLeafHolders.add(leafHolder);
+ Animator leafAnimator = getAnimator(leafHolder, leafFlyRect, progress);
+ leafAnimator.addListener(new AnimEndListener(leafHolder));
+ leafAnimator.start();
+ }
+
+ private Animator getAnimator(LeafHolder target, RectF leafFlyRect, float progress) {
+ ValueAnimator bezierValueAnimator = getBezierValueAnimator(target, leafFlyRect, progress);
+
+ AnimatorSet finalSet = new AnimatorSet();
+ finalSet.playSequentially(bezierValueAnimator);
+ finalSet.setInterpolator(INTERPOLATORS[mRandom.nextInt(INTERPOLATORS.length)]);
+ finalSet.setTarget(target);
+ return finalSet;
+ }
+
+ private ValueAnimator getBezierValueAnimator(LeafHolder target, RectF leafFlyRect, float progress) {
+ BezierEvaluator evaluator = new BezierEvaluator(getPoint1(leafFlyRect), getPoint2(leafFlyRect));
+
+ int leafFlyStartY = (int) (mCurrentProgressBounds.bottom - mLeafDrawable.getIntrinsicHeight());
+ int leafFlyRange = (int) (mCurrentProgressBounds.height() - mLeafDrawable.getIntrinsicHeight());
+
+ int startPointY = leafFlyStartY - mRandom.nextInt(leafFlyRange);
+ int endPointY = leafFlyStartY - mRandom.nextInt(leafFlyRange);
+
+ ValueAnimator animator = ValueAnimator.ofObject(evaluator,
+ new PointF((int) (leafFlyRect.right - mLeafDrawable.getIntrinsicWidth()), startPointY),
+ new PointF(leafFlyRect.left, endPointY));
+ animator.addUpdateListener(new BezierListener(target));
+ animator.setTarget(target);
+
+ animator.setDuration((long) ((mRandom.nextInt(300) + mDuration * DEFAULT_LEAF_FLY_DURATION_FACTOR) * (1.0f - progress)));
+
+ return animator;
+ }
+
+ //get the pointF which belong to the right half side
+ private PointF getPoint1(RectF leafFlyRect) {
+ PointF point = new PointF();
+ point.x = leafFlyRect.right - mRandom.nextInt((int) (leafFlyRect.width() / 2));
+ point.y = (int) (leafFlyRect.bottom - mRandom.nextInt((int) leafFlyRect.height()));
+ return point;
+ }
+
+ //get the pointF which belong to the left half side
+ private PointF getPoint2(RectF leafFlyRect) {
+ PointF point = new PointF();
+ point.x = leafFlyRect.left + mRandom.nextInt((int) (leafFlyRect.width() / 2));
+ point.y = (int) (leafFlyRect.bottom - mRandom.nextInt((int) leafFlyRect.height()));
+ return point;
+ }
+
+ private class BezierEvaluator implements TypeEvaluator {
+
+ private PointF point1;
+ private PointF point2;
+
+ public BezierEvaluator(PointF point1, PointF point2) {
+ this.point1 = point1;
+ this.point2 = point2;
+ }
+
+ //Third-order Bezier curve formula: B(t) = point0 * (1-t)^3 + 3 * point1 * t * (1-t)^2 + 3 * point2 * t^2 * (1-t) + point3 * t^3
+ @Override
+ public PointF evaluate(float fraction, PointF point0, PointF point3) {
+
+ float t = fraction;
+ float tLeft = 1.0f - t;
+
+ float x = (float) (point0.x * Math.pow(tLeft, 3) + 3 * point1.x * t * Math.pow(tLeft, 2) + 3 * point2.x * Math.pow(t, 2) * tLeft + point3.x * Math.pow(t, 3));
+ float y = (float) (point0.y * Math.pow(tLeft, 3) + 3 * point1.y * t * Math.pow(tLeft, 2) + 3 * point2.y * Math.pow(t, 2) * tLeft + point3.y * Math.pow(t, 3));
+
+ return new PointF(x, y);
+ }
+ }
+
+ private class BezierListener implements ValueAnimator.AnimatorUpdateListener {
+
+ private LeafHolder target;
+
+ public BezierListener(LeafHolder target) {
+ this.target = target;
+ }
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ PointF point = (PointF) animation.getAnimatedValue();
+ target.mLeafRect.set((int) point.x, (int) point.y,
+ (int) (point.x + mLeafDrawable.getIntrinsicWidth()), (int) (point.y + mLeafDrawable.getIntrinsicHeight()));
+ target.mLeafRotation = target.mMaxRotation * animation.getAnimatedFraction();
+ }
+ }
+
+ private class AnimEndListener extends AnimatorListenerAdapter {
+ private LeafHolder target;
+
+ public AnimEndListener(LeafHolder target) {
+ this.target = target;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ mLeafHolders.remove(target);
+ mCurrentLeafCount++;
+ }
+ }
+
+ private class LeafHolder {
+ public Rect mLeafRect = new Rect();
+ public float mLeafRotation = 0.0f;
+
+ public float mMaxRotation = mRandom.nextInt(120);
+ }
+
+ public static class Builder {
+ private Context mContext;
+
+ public Builder(Context mContext) {
+ this.mContext = mContext;
+ }
+
+ public ElectricFanLoadingRenderer build() {
+ ElectricFanLoadingRenderer loadingRenderer = new ElectricFanLoadingRenderer(mContext);
+ return loadingRenderer;
+ }
+ }
+}
diff --git a/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/shapechange/CircleBroodLoadingRenderer.java b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/shapechange/CircleBroodLoadingRenderer.java
new file mode 100644
index 00000000..03043416
--- /dev/null
+++ b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/shapechange/CircleBroodLoadingRenderer.java
@@ -0,0 +1,676 @@
+package app.dinus.com.loadingdrawable.render.shapechange;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PathMeasure;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import app.dinus.com.loadingdrawable.DensityUtil;
+import app.dinus.com.loadingdrawable.render.LoadingRenderer;
+
+public class CircleBroodLoadingRenderer extends LoadingRenderer {
+
+ private final Interpolator MOTHER_MOVE_INTERPOLATOR = new MotherMoveInterpolator();
+ private final Interpolator CHILD_MOVE_INTERPOLATOR = new ChildMoveInterpolator();
+
+ private final Interpolator ACCELERATE_INTERPOLATOR03 = new AccelerateInterpolator(0.3f);
+ private final Interpolator ACCELERATE_INTERPOLATOR05 = new AccelerateInterpolator(0.5f);
+ private final Interpolator ACCELERATE_INTERPOLATOR08 = new AccelerateInterpolator(0.8f);
+ private final Interpolator ACCELERATE_INTERPOLATOR10 = new AccelerateInterpolator(1.0f);
+
+ private final Interpolator DECELERATE_INTERPOLATOR03 = new DecelerateInterpolator(0.3f);
+ private final Interpolator DECELERATE_INTERPOLATOR05 = new DecelerateInterpolator(0.5f);
+ private final Interpolator DECELERATE_INTERPOLATOR08 = new DecelerateInterpolator(0.8f);
+ private final Interpolator DECELERATE_INTERPOLATOR10 = new DecelerateInterpolator(1.0f);
+
+ private float STAGE_MOTHER_FORWARD_TOP_LEFT = 0.34f;
+ private float STAGE_MOTHER_BACKWARD_TOP_LEFT = 0.5f;
+ private float STAGE_MOTHER_FORWARD_BOTTOM_LEFT = 0.65f;
+ private float STAGE_MOTHER_BACKWARD_BOTTOM_LEFT = 0.833f;
+
+ private float STAGE_CHILD_DELAY = 0.1f;
+ private float STAGE_CHILD_PRE_FORWARD_TOP_LEFT = 0.26f;
+ private float STAGE_CHILD_FORWARD_TOP_LEFT = 0.34f;
+ private float STAGE_CHILD_PRE_BACKWARD_TOP_LEFT = 0.42f;
+ private float STAGE_CHILD_BACKWARD_TOP_LEFT = 0.5f;
+ private float STAGE_CHILD_FORWARD_BOTTOM_LEFT = 0.7f;
+ private float STAGE_CHILD_BACKWARD_BOTTOM_LEFT = 0.9f;
+
+ private final float OVAL_BEZIER_FACTOR = 0.55152f;
+
+ private final float DEFAULT_WIDTH = 40.0f;
+ private final float DEFAULT_HEIGHT = 20.0f;
+ private final float MAX_MATHER_OVAL_SIZE = 12;//19;
+ private final float MIN_CHILD_OVAL_RADIUS = 1;//5;
+ private final float MAX_MATHER_SHAPE_CHANGE_FACTOR = 0.8452f;
+
+ private final int DEFAULT_OVAL_COLOR = Color.parseColor("#FFBE1C23");
+ private final int DEFAULT_OVAL_DEEP_COLOR = Color.parseColor("#FFB21721");
+ private final int DEFAULT_BACKGROUND_COLOR = Color.parseColor("#FFE3C172");
+ private final int DEFAULT_BACKGROUND_DEEP_COLOR = Color.parseColor("#FFE2B552");
+
+ private final long ANIMATION_DURATION = 4111;
+
+ private final Paint mPaint = new Paint();
+ private final RectF mCurrentBounds = new RectF();
+ private final Path mMotherOvalPath = new Path();
+ private final Path mMotherMovePath = new Path();
+ private final Path mChildMovePath = new Path();
+
+ private final float[] mMotherPosition = new float[2];
+ private final float[] mChildPosition = new float[2];
+ private final PathMeasure mMotherMovePathMeasure = new PathMeasure();
+ private final PathMeasure mChildMovePathMeasure = new PathMeasure();
+
+ private float mChildOvalRadius;
+ private float mBasicChildOvalRadius;
+ private float mMaxMotherOvalSize;
+ private float mMotherOvalHalfWidth;
+ private float mMotherOvalHalfHeight;
+
+ private float mChildLeftXOffset;
+ private float mChildLeftYOffset;
+ private float mChildRightXOffset;
+ private float mChildRightYOffset;
+
+ private int mOvalColor;
+ private int mOvalDeepColor;
+ private int mBackgroundColor;
+ private int mBackgroundDeepColor;
+ private int mCurrentOvalColor;
+ private int mCurrentBackgroundColor;
+
+ private int mRevealCircleRadius;
+ private int mMaxRevealCircleRadius;
+
+ private int mRotateDegrees;
+
+ private float mStageMotherForwardTopLeftLength;
+ private float mStageMotherBackwardTopLeftLength;
+ private float mStageMotherForwardBottomLeftLength;
+ private float mStageMotherBackwardBottomLeftLength;
+
+ private float mStageChildPreForwardTopLeftLength;
+ private float mStageChildForwardTopLeftLength;
+ private float mStageChildPreBackwardTopLeftLength;
+ private float mStageChildBackwardTopLeftLength;
+ private float mStageChildForwardBottomLeftLength;
+ private float mStageChildBackwardBottomLeftLength;
+
+ private CircleBroodLoadingRenderer(Context context) {
+ super(context);
+ init(context);
+ setupPaint();
+ }
+
+ private void init(Context context) {
+ mWidth = DensityUtil.dip2px(context, DEFAULT_WIDTH);
+ mHeight = DensityUtil.dip2px(context, DEFAULT_HEIGHT);
+
+ mMaxMotherOvalSize = DensityUtil.dip2px(context, MAX_MATHER_OVAL_SIZE);
+ mBasicChildOvalRadius = DensityUtil.dip2px(context, MIN_CHILD_OVAL_RADIUS);
+
+ mOvalColor = DEFAULT_OVAL_COLOR;
+ mOvalDeepColor = DEFAULT_OVAL_DEEP_COLOR;
+ mBackgroundColor = DEFAULT_BACKGROUND_COLOR;
+ mBackgroundDeepColor = DEFAULT_BACKGROUND_DEEP_COLOR;
+
+ mMotherOvalHalfWidth = mMaxMotherOvalSize;
+ mMotherOvalHalfHeight = mMaxMotherOvalSize;
+
+ mMaxRevealCircleRadius = (int) (Math.sqrt(mWidth * mWidth + mHeight * mHeight) / 2 + 1);
+
+ mDuration = ANIMATION_DURATION;
+ }
+
+ private void setupPaint() {
+ mPaint.setAntiAlias(true);
+ mPaint.setStyle(Paint.Style.FILL);
+ mPaint.setStrokeWidth(1.0f);
+ }
+
+ @Override
+ protected void draw(Canvas canvas, Rect bounds) {
+ int saveCount = canvas.save();
+
+ RectF arcBounds = mCurrentBounds;
+ arcBounds.set(bounds);
+
+ //draw background
+ canvas.drawColor(mCurrentBackgroundColor);
+ //draw reveal circle
+ if (mRevealCircleRadius > 0) {
+ mPaint.setColor(mCurrentBackgroundColor == mBackgroundColor ? mBackgroundDeepColor : mBackgroundColor);
+ canvas.drawCircle(arcBounds.centerX(), arcBounds.centerY(), mRevealCircleRadius, mPaint);
+ }
+
+ //draw mother oval
+ mPaint.setColor(mCurrentOvalColor);
+
+ int motherSaveCount = canvas.save();
+ canvas.rotate(mRotateDegrees, mMotherPosition[0], mMotherPosition[1]);
+ canvas.drawPath(createMotherPath(), mPaint);
+ canvas.drawPath(createLinkPath(), mPaint);
+ canvas.restoreToCount(motherSaveCount);
+
+ int childSaveCount = canvas.save();
+ canvas.rotate(mRotateDegrees, mChildPosition[0], mChildPosition[1]);
+ canvas.drawPath(createChildPath(), mPaint);
+ canvas.restoreToCount(childSaveCount);
+ canvas.restoreToCount(saveCount);
+
+// canvas.drawPath(mMotherMovePath, mPaint);
+// canvas.drawPath(mChildMovePath, mPaint);
+// canvas.drawLine(mMotherPosition[0], mMotherPosition[1], mChildPosition[0], mChildPosition[1], mPaint);
+ }
+
+ private Path createMotherPath() {
+ mMotherOvalPath.reset();
+
+ mMotherOvalPath.addOval(new RectF(mMotherPosition[0] - mMotherOvalHalfWidth, mMotherPosition[1] - mMotherOvalHalfHeight,
+ mMotherPosition[0] + mMotherOvalHalfWidth, mMotherPosition[1] + mMotherOvalHalfHeight), Path.Direction.CW);
+
+ return mMotherOvalPath;
+ }
+
+ private Path createChildPath() {
+ float bezierOffset = mChildOvalRadius * OVAL_BEZIER_FACTOR;
+
+ Path path = new Path();
+ path.moveTo(mChildPosition[0], mChildPosition[1] - mChildOvalRadius);
+ //left_top arc
+ path.cubicTo(mChildPosition[0] - bezierOffset - mChildLeftXOffset, mChildPosition[1] - mChildOvalRadius,
+ mChildPosition[0] - mChildOvalRadius - mChildLeftXOffset, mChildPosition[1] - bezierOffset + mChildLeftYOffset,
+ mChildPosition[0] - mChildOvalRadius - mChildLeftXOffset, mChildPosition[1]);
+ //left_bottom arc
+ path.cubicTo(mChildPosition[0] - mChildOvalRadius - mChildLeftXOffset, mChildPosition[1] + bezierOffset - mChildLeftYOffset,
+ mChildPosition[0] - bezierOffset - mChildLeftXOffset, mChildPosition[1] + mChildOvalRadius,
+ mChildPosition[0], mChildPosition[1] + mChildOvalRadius);
+
+ //right_bottom arc
+ path.cubicTo(mChildPosition[0] + bezierOffset + mChildRightXOffset, mChildPosition[1] + mChildOvalRadius,
+ mChildPosition[0] + mChildOvalRadius + mChildRightXOffset, mChildPosition[1] + bezierOffset - mChildRightYOffset,
+ mChildPosition[0] + mChildOvalRadius + mChildRightXOffset, mChildPosition[1]);
+ //right_top arc
+ path.cubicTo(mChildPosition[0] + mChildOvalRadius + mChildRightXOffset, mChildPosition[1] - bezierOffset + mChildRightYOffset,
+ mChildPosition[0] + bezierOffset + mChildRightXOffset, mChildPosition[1] - mChildOvalRadius,
+ mChildPosition[0], mChildPosition[1] - mChildOvalRadius);
+
+ return path;
+ }
+
+ private Path createLinkPath() {
+ Path path = new Path();
+ float bezierOffset = mMotherOvalHalfWidth * OVAL_BEZIER_FACTOR;
+
+ float distance = (float) Math.sqrt(Math.pow(mMotherPosition[0] - mChildPosition[0], 2.0f) + Math.pow(mMotherPosition[1] - mChildPosition[1], 2.0f));
+ if (distance <= mMotherOvalHalfWidth + mChildOvalRadius * 1.2f
+ && distance >= mMotherOvalHalfWidth - mChildOvalRadius * 1.2f) {
+ float maxOffsetY = 2 * mChildOvalRadius * 1.2f;
+ float offsetRate = (distance - (mMotherOvalHalfWidth - mChildOvalRadius * 1.2f)) / maxOffsetY;
+
+ float mMotherOvalOffsetY = mMotherOvalHalfHeight - offsetRate * (mMotherOvalHalfHeight - mChildOvalRadius) * 0.85f;
+
+ mMotherOvalPath.addOval(new RectF(mMotherPosition[0] - mMotherOvalHalfWidth, mMotherPosition[1] - mMotherOvalOffsetY,
+ mMotherPosition[0] + mMotherOvalHalfWidth, mMotherPosition[1] + mMotherOvalOffsetY), Path.Direction.CW);
+
+ float mMotherXOffset = distance - mMotherOvalHalfWidth + mChildOvalRadius;
+ float distanceUltraLeft = (float) Math.sqrt(Math.pow(mMotherPosition[0] - mMotherOvalHalfWidth - mChildPosition[0], 2.0f)
+ + Math.pow(mMotherPosition[1] - mChildPosition[1], 2.0f));
+ float distanceUltraRight = (float) Math.sqrt(Math.pow(mMotherPosition[0] + mMotherOvalHalfWidth - mChildPosition[0], 2.0f)
+ + Math.pow(mMotherPosition[1] - mChildPosition[1], 2.0f));
+
+ path.moveTo(mMotherPosition[0], mMotherPosition[1] + mMotherOvalOffsetY);
+ if (distanceUltraRight < distanceUltraLeft) {
+ //right_bottom arc
+ path.cubicTo(mMotherPosition[0] + bezierOffset + mMotherXOffset, mMotherPosition[1] + mMotherOvalOffsetY,
+ mMotherPosition[0] + distance + mChildOvalRadius, mMotherPosition[1] + mChildOvalRadius * 1.5f,
+ mMotherPosition[0] + distance + mChildOvalRadius, mMotherPosition[1]);
+ //right_top arc
+ path.cubicTo(mMotherPosition[0] + distance + mChildOvalRadius, mMotherPosition[1] - mChildOvalRadius * 1.5f,
+ mMotherPosition[0] + bezierOffset + mMotherXOffset, mMotherPosition[1] - mMotherOvalOffsetY,
+ mMotherPosition[0], mMotherPosition[1] - mMotherOvalOffsetY);
+ } else {
+ //left_bottom arc
+ path.cubicTo(mMotherPosition[0] - bezierOffset - mMotherXOffset, mMotherPosition[1] + mMotherOvalOffsetY,
+ mMotherPosition[0] - distance - mChildOvalRadius, mMotherPosition[1] + mChildOvalRadius * 1.5f,
+ mMotherPosition[0] - distance - mChildOvalRadius, mMotherPosition[1]);
+ //left_top arc
+ path.cubicTo(mMotherPosition[0] - distance - mChildOvalRadius, mMotherPosition[1] - mChildOvalRadius * 1.5f,
+ mMotherPosition[0] - bezierOffset - mMotherXOffset, mMotherPosition[1] - mMotherOvalOffsetY,
+ mMotherPosition[0], mMotherPosition[1] - mMotherOvalOffsetY);
+ }
+ path.lineTo(mMotherPosition[0], mMotherPosition[1] + mMotherOvalOffsetY);
+ }
+
+ return path;
+ }
+
+ @Override
+ protected void computeRender(float renderProgress) {
+ if (mCurrentBounds.isEmpty()) {
+ return;
+ }
+
+ if (mMotherMovePath.isEmpty()) {
+ mMotherMovePath.set(createMotherMovePath());
+ mMotherMovePathMeasure.setPath(mMotherMovePath, false);
+
+ mChildMovePath.set(createChildMovePath());
+ mChildMovePathMeasure.setPath(mChildMovePath, false);
+ }
+
+ //mother oval
+ float motherMoveProgress = MOTHER_MOVE_INTERPOLATOR.getInterpolation(renderProgress);
+ mMotherMovePathMeasure.getPosTan(getCurrentMotherMoveLength(motherMoveProgress), mMotherPosition, null);
+ mMotherOvalHalfWidth = mMaxMotherOvalSize;
+ mMotherOvalHalfHeight = mMaxMotherOvalSize * getMotherShapeFactor(motherMoveProgress);
+
+ //child Oval
+ float childMoveProgress = CHILD_MOVE_INTERPOLATOR.getInterpolation(renderProgress);
+ mChildMovePathMeasure.getPosTan(getCurrentChildMoveLength(childMoveProgress), mChildPosition, null);
+ setupChildParams(childMoveProgress);
+
+ mRotateDegrees = (int) (Math.toDegrees(Math.atan((mMotherPosition[1] - mChildPosition[1]) /
+ (mMotherPosition[0] - mChildPosition[0]))));
+
+ mRevealCircleRadius = getCurrentRevealCircleRadius(renderProgress);
+ mCurrentOvalColor = getCurrentOvalColor(renderProgress);
+ mCurrentBackgroundColor = getCurrentBackgroundColor(renderProgress);
+ }
+
+ private void setupChildParams(float input) {
+ mChildOvalRadius = mBasicChildOvalRadius;
+
+ mChildRightXOffset = 0.0f;
+ mChildLeftXOffset = 0.0f;
+
+ if (input <= STAGE_CHILD_PRE_FORWARD_TOP_LEFT) {
+ if (input >= 0.25) {
+ float shapeProgress = (input - 0.25f) / 0.01f;
+ mChildLeftXOffset = (1.0f - shapeProgress) * mChildOvalRadius * 0.25f;
+ } else {
+ mChildLeftXOffset = mChildOvalRadius * 0.25f;
+ }
+ } else if (input <= STAGE_CHILD_FORWARD_TOP_LEFT) {
+ if (input > 0.275f && input < 0.285f) {
+ float shapeProgress = (input - 0.275f) / 0.01f;
+ mChildLeftXOffset = shapeProgress * mChildOvalRadius * 0.25f;
+ } else if (input > 0.285f) {
+ mChildLeftXOffset = mChildOvalRadius * 0.25f;
+ }
+ } else if (input <= STAGE_CHILD_PRE_BACKWARD_TOP_LEFT) {
+ if (input > 0.38f) {
+ float radiusProgress = (input - 0.38f) / 0.04f;
+ mChildOvalRadius = mBasicChildOvalRadius * (1.0f + radiusProgress);
+ }
+ } else if (input <= STAGE_CHILD_BACKWARD_TOP_LEFT) {
+ if (input < 0.46f) {
+ float radiusProgress = (input - 0.42f) / 0.04f;
+ mChildOvalRadius = mBasicChildOvalRadius * (2.0f - radiusProgress);
+ }
+ } else if (input <= STAGE_CHILD_FORWARD_BOTTOM_LEFT) {
+ if (input > 0.65f) {
+ float radiusProgress = (input - 0.65f) / 0.05f;
+ mChildOvalRadius = mBasicChildOvalRadius * (1.0f + radiusProgress);
+ }
+ } else if (input <= STAGE_CHILD_BACKWARD_BOTTOM_LEFT) {
+ if (input < 0.71f) {
+ mChildOvalRadius = mBasicChildOvalRadius * 2.0f;
+ } else if (input < 0.76f) {
+ float radiusProgress = (input - 0.71f) / 0.05f;
+ mChildOvalRadius = mBasicChildOvalRadius * (2.0f - radiusProgress);
+ }
+ } else {
+ }
+
+ mChildRightYOffset = mChildRightXOffset / 2.5f;
+ mChildLeftYOffset = mChildLeftXOffset / 2.5f;
+ }
+
+ private float getMotherShapeFactor(float input) {
+
+ float shapeProgress;
+ if (input <= STAGE_MOTHER_FORWARD_TOP_LEFT) {
+ shapeProgress = input / STAGE_MOTHER_FORWARD_TOP_LEFT;
+ } else if (input <= STAGE_MOTHER_BACKWARD_TOP_LEFT) {
+ shapeProgress = (input - STAGE_MOTHER_FORWARD_TOP_LEFT) / (STAGE_MOTHER_BACKWARD_TOP_LEFT - STAGE_MOTHER_FORWARD_TOP_LEFT);
+ } else if (input <= STAGE_MOTHER_FORWARD_BOTTOM_LEFT) {
+ shapeProgress = (input - STAGE_MOTHER_BACKWARD_TOP_LEFT) / (STAGE_MOTHER_FORWARD_BOTTOM_LEFT - STAGE_MOTHER_BACKWARD_TOP_LEFT);
+ } else if (input <= STAGE_MOTHER_BACKWARD_BOTTOM_LEFT) {
+ shapeProgress = (input - STAGE_MOTHER_FORWARD_BOTTOM_LEFT) / (STAGE_MOTHER_BACKWARD_BOTTOM_LEFT - STAGE_MOTHER_FORWARD_BOTTOM_LEFT);
+ } else {
+ shapeProgress = 1.0f;
+ }
+
+ return shapeProgress < 0.5f ?
+ 1.0f - (1.0f - MAX_MATHER_SHAPE_CHANGE_FACTOR) * shapeProgress * 2.0f :
+ MAX_MATHER_SHAPE_CHANGE_FACTOR + (1.0f - MAX_MATHER_SHAPE_CHANGE_FACTOR) * (shapeProgress - 0.5f) * 2.0f;
+ }
+
+ private float getCurrentMotherMoveLength(float input) {
+ float currentStartDistance = 0.0f;
+ float currentStageDistance = 0.0f;
+ float currentStateStartProgress = 0.0f;
+ float currentStateEndProgress = 0.0f;
+
+ if (input > 0.0f) {
+ currentStartDistance += currentStageDistance;
+ currentStageDistance = mStageMotherForwardTopLeftLength;
+ currentStateStartProgress = 0.0f;
+ currentStateEndProgress = STAGE_MOTHER_FORWARD_TOP_LEFT;
+ }
+
+ if (input > STAGE_MOTHER_FORWARD_TOP_LEFT) {
+ currentStartDistance += currentStageDistance;
+ currentStageDistance = mStageMotherBackwardTopLeftLength;
+ currentStateStartProgress = STAGE_MOTHER_FORWARD_TOP_LEFT;
+ currentStateEndProgress = STAGE_MOTHER_BACKWARD_TOP_LEFT;
+ }
+
+ if (input > STAGE_MOTHER_BACKWARD_TOP_LEFT) {
+ currentStartDistance += currentStageDistance;
+ currentStageDistance = mStageMotherForwardBottomLeftLength;
+ currentStateStartProgress = STAGE_MOTHER_BACKWARD_TOP_LEFT;
+ currentStateEndProgress = STAGE_MOTHER_FORWARD_BOTTOM_LEFT;
+ }
+
+ if (input > STAGE_MOTHER_FORWARD_BOTTOM_LEFT) {
+ currentStartDistance += currentStageDistance;
+ currentStageDistance = mStageMotherBackwardBottomLeftLength;
+ currentStateStartProgress = STAGE_MOTHER_FORWARD_BOTTOM_LEFT;
+ currentStateEndProgress = STAGE_MOTHER_BACKWARD_BOTTOM_LEFT;
+ }
+
+ if (input > STAGE_MOTHER_BACKWARD_BOTTOM_LEFT) {
+ return currentStartDistance + currentStageDistance;
+ }
+
+ return currentStartDistance + (input - currentStateStartProgress) /
+ (currentStateEndProgress - currentStateStartProgress) * currentStageDistance;
+ }
+
+ private float getCurrentChildMoveLength(float input) {
+ float currentStartDistance = 0.0f;
+ float currentStageDistance = 0.0f;
+ float currentStateStartProgress = 0.0f;
+ float currentStateEndProgress = 0.0f;
+
+ if (input > 0.0f) {
+ currentStartDistance += currentStageDistance;
+ currentStageDistance = mStageChildPreForwardTopLeftLength;
+ currentStateStartProgress = 0.0f;
+ currentStateEndProgress = STAGE_CHILD_PRE_FORWARD_TOP_LEFT;
+ }
+
+ if (input > STAGE_CHILD_PRE_FORWARD_TOP_LEFT) {
+ currentStartDistance += currentStageDistance;
+ currentStageDistance = mStageChildForwardTopLeftLength;
+ currentStateStartProgress = STAGE_CHILD_PRE_FORWARD_TOP_LEFT;
+ currentStateEndProgress = STAGE_CHILD_FORWARD_TOP_LEFT;
+ }
+
+ if (input > STAGE_CHILD_FORWARD_TOP_LEFT) {
+ currentStartDistance += currentStageDistance;
+ currentStageDistance = mStageChildPreBackwardTopLeftLength;
+ currentStateStartProgress = STAGE_CHILD_FORWARD_TOP_LEFT;
+ currentStateEndProgress = STAGE_CHILD_PRE_BACKWARD_TOP_LEFT;
+ }
+
+ if (input > STAGE_CHILD_PRE_BACKWARD_TOP_LEFT) {
+ currentStartDistance += currentStageDistance;
+ currentStageDistance = mStageChildBackwardTopLeftLength;
+ currentStateStartProgress = STAGE_CHILD_PRE_BACKWARD_TOP_LEFT;
+ currentStateEndProgress = STAGE_CHILD_BACKWARD_TOP_LEFT;
+ }
+
+ if (input > STAGE_CHILD_BACKWARD_TOP_LEFT) {
+ currentStartDistance += currentStageDistance;
+ currentStageDistance = mStageChildForwardBottomLeftLength;
+ currentStateStartProgress = STAGE_CHILD_BACKWARD_TOP_LEFT;
+ currentStateEndProgress = STAGE_CHILD_FORWARD_BOTTOM_LEFT;
+ }
+
+ if (input > STAGE_CHILD_FORWARD_BOTTOM_LEFT) {
+ currentStartDistance += currentStageDistance;
+ currentStageDistance = mStageChildBackwardBottomLeftLength;
+ currentStateStartProgress = STAGE_CHILD_FORWARD_BOTTOM_LEFT;
+ currentStateEndProgress = STAGE_CHILD_BACKWARD_BOTTOM_LEFT;
+ }
+
+ if (input > STAGE_CHILD_BACKWARD_BOTTOM_LEFT) {
+ return currentStartDistance + currentStageDistance;
+ }
+
+ return currentStartDistance + (input - currentStateStartProgress) /
+ (currentStateEndProgress - currentStateStartProgress) * currentStageDistance;
+ }
+
+ private Path createMotherMovePath() {
+ Path path = new Path();
+
+ float centerX = mCurrentBounds.centerX();
+ float centerY = mCurrentBounds.centerY();
+ float currentPathLength = 0.0f;
+
+ path.moveTo(centerX, centerY);
+ //forward top left
+ path.quadTo(centerX - mMotherOvalHalfWidth * 2.0f, centerY,
+ centerX - mMotherOvalHalfWidth * 2.0f, centerY - mMotherOvalHalfHeight);
+ mStageMotherForwardTopLeftLength = getRestLength(path, currentPathLength);
+ currentPathLength += mStageMotherForwardTopLeftLength;
+
+ //backward top left
+ path.quadTo(centerX - mMotherOvalHalfWidth * 1.0f, centerY - mMotherOvalHalfHeight,
+ centerX, centerY);
+ mStageMotherBackwardTopLeftLength = getRestLength(path, currentPathLength);
+ currentPathLength += mStageMotherBackwardTopLeftLength;
+ //forward bottom left
+ path.quadTo(centerX, centerY + mMotherOvalHalfHeight,
+ centerX - mMotherOvalHalfWidth / 2, centerY + mMotherOvalHalfHeight * 1.1f);
+ mStageMotherForwardBottomLeftLength = getRestLength(path, currentPathLength);
+ currentPathLength += mStageMotherForwardBottomLeftLength;
+ //backward bottom left
+ path.quadTo(centerX - mMotherOvalHalfWidth / 2, centerY + mMotherOvalHalfHeight * 0.6f,
+ centerX, centerY);
+ mStageMotherBackwardBottomLeftLength = getRestLength(path, currentPathLength);
+
+ return path;
+ }
+
+ private Path createChildMovePath() {
+ Path path = new Path();
+
+ float centerX = mCurrentBounds.centerX();
+ float centerY = mCurrentBounds.centerY();
+ float currentPathLength = 0.0f;
+
+ //start
+ path.moveTo(centerX, centerY);
+ //pre forward top left
+ path.lineTo(centerX + mMotherOvalHalfWidth * 0.75f, centerY);
+ mStageChildPreForwardTopLeftLength = getRestLength(path, currentPathLength);
+ currentPathLength += mStageChildPreForwardTopLeftLength;
+ //forward top left
+ path.quadTo(centerX - mMotherOvalHalfWidth * 0.5f, centerY,
+ centerX - mMotherOvalHalfWidth * 2.0f, centerY - mMotherOvalHalfHeight);
+ mStageChildForwardTopLeftLength = getRestLength(path, currentPathLength);
+ currentPathLength += mStageChildForwardTopLeftLength;
+ //pre backward top left
+ path.lineTo(centerX - mMotherOvalHalfWidth * 2.0f + mMotherOvalHalfWidth * 0.2f, centerY - mMotherOvalHalfHeight);
+ path.quadTo(centerX - mMotherOvalHalfWidth * 2.5f, centerY - mMotherOvalHalfHeight * 2,
+ centerX - mMotherOvalHalfWidth * 1.5f, centerY - mMotherOvalHalfHeight * 2.25f);
+ mStageChildPreBackwardTopLeftLength = getRestLength(path, currentPathLength);
+ currentPathLength += mStageChildPreBackwardTopLeftLength;
+ //backward top left
+ path.quadTo(centerX - mMotherOvalHalfWidth * 0.2f, centerY - mMotherOvalHalfHeight * 2.25f,
+ centerX, centerY);
+ mStageChildBackwardTopLeftLength = getRestLength(path, currentPathLength);
+ currentPathLength += mStageChildBackwardTopLeftLength;
+ //forward bottom left
+ path.cubicTo(centerX, centerY + mMotherOvalHalfHeight,
+ centerX - mMotherOvalHalfWidth, centerY + mMotherOvalHalfHeight * 2.5f,
+ centerX - mMotherOvalHalfWidth * 1.5f, centerY + mMotherOvalHalfHeight * 2.5f);
+ mStageChildForwardBottomLeftLength = getRestLength(path, currentPathLength);
+ currentPathLength += mStageChildForwardBottomLeftLength;
+ //backward bottom left
+ path.cubicTo(
+ centerX - mMotherOvalHalfWidth * 2.0f, centerY + mMotherOvalHalfHeight * 2.5f,
+ centerX - mMotherOvalHalfWidth * 3.0f, centerY + mMotherOvalHalfHeight * 0.8f,
+ centerX, centerY);
+ mStageChildBackwardBottomLeftLength = getRestLength(path, currentPathLength);
+
+ return path;
+ }
+
+ private int getCurrentRevealCircleRadius(float input) {
+ int result = 0;
+ if (input > 0.44f && input < 0.48f) {
+ result = (int) ((input - 0.44f) / 0.04f * mMaxRevealCircleRadius);
+ }
+
+ if (input > 0.81f && input < 0.85f) {
+ result = (int) ((input - 0.81f) / 0.04f * mMaxRevealCircleRadius);
+ }
+
+ return result;
+ }
+
+ private int getCurrentBackgroundColor(float input) {
+ return input < 0.48f || input > 0.85f ? mBackgroundColor : mBackgroundDeepColor;
+ }
+
+ private int getCurrentOvalColor(float input) {
+ int result;
+
+ if (input < 0.5f) {
+ result = mOvalColor;
+ } else if (input < 0.75f) {
+ float colorProgress = (input - 0.5f) / 0.2f;
+ result = evaluateColorChange(colorProgress, mOvalColor, mOvalDeepColor);
+ } else if (input < 0.85f) {
+ result = mOvalDeepColor;
+ } else {
+ float colorProgress = (input - 0.9f) / 0.1f;
+ result = evaluateColorChange(colorProgress, mOvalDeepColor, mOvalColor);
+ }
+
+ return result;
+ }
+
+ 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))));
+ }
+
+ private float getRestLength(Path path, float startD) {
+ Path tempPath = new Path();
+ PathMeasure pathMeasure = new PathMeasure(path, false);
+
+ pathMeasure.getSegment(startD, pathMeasure.getLength(), tempPath, true);
+
+ pathMeasure.setPath(tempPath, false);
+
+ return pathMeasure.getLength();
+ }
+
+ @Override
+ protected void setAlpha(int alpha) {
+ mPaint.setAlpha(alpha);
+
+ }
+
+ @Override
+ protected void setColorFilter(ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+
+ }
+
+ @Override
+ protected void reset() {
+ }
+
+ private class MotherMoveInterpolator implements Interpolator {
+ @Override
+ public float getInterpolation(float input) {
+ float result;
+
+ if (input <= STAGE_MOTHER_FORWARD_TOP_LEFT) {
+ result = ACCELERATE_INTERPOLATOR10.getInterpolation(input * 2.941f) / 2.941f;
+ } else if (input <= STAGE_MOTHER_BACKWARD_TOP_LEFT) {
+ result = 0.34f + DECELERATE_INTERPOLATOR10.getInterpolation((input - 0.34f) * 6.25f) / 6.25f;
+ } else if (input <= STAGE_MOTHER_FORWARD_BOTTOM_LEFT) {
+ result = 0.5f + ACCELERATE_INTERPOLATOR03.getInterpolation((input - 0.5f) * 6.666f) / 4.0f;
+ } else if (input <= STAGE_MOTHER_BACKWARD_BOTTOM_LEFT) {
+ result = 0.75f + DECELERATE_INTERPOLATOR03.getInterpolation((input - 0.65f) * 5.46f) / 4.0f;
+ } else {
+ result = 1.0f;
+ }
+
+ return result;
+ }
+ }
+
+ private class ChildMoveInterpolator implements Interpolator {
+
+ @Override
+ public float getInterpolation(float input) {
+ float result;
+
+ if (input < STAGE_CHILD_DELAY) {
+ return 0.0f;
+ } else if (input <= STAGE_CHILD_PRE_FORWARD_TOP_LEFT) {
+ result = DECELERATE_INTERPOLATOR10.getInterpolation((input - 0.1f) * 6.25f) / 3.846f;
+ } else if (input <= STAGE_CHILD_FORWARD_TOP_LEFT) {
+ result = 0.26f + ACCELERATE_INTERPOLATOR10.getInterpolation((input - 0.26f) * 12.5f) / 12.5f;
+ } else if (input <= STAGE_CHILD_PRE_BACKWARD_TOP_LEFT) {
+ result = 0.34f + DECELERATE_INTERPOLATOR08.getInterpolation((input - 0.34f) * 12.5f) / 12.5f;
+ } else if (input <= STAGE_CHILD_BACKWARD_TOP_LEFT) {
+ result = 0.42f + ACCELERATE_INTERPOLATOR08.getInterpolation((input - 0.42f) * 12.5f) / 12.5f;
+ } else if (input <= STAGE_CHILD_FORWARD_BOTTOM_LEFT) {
+ result = 0.5f + DECELERATE_INTERPOLATOR05.getInterpolation((input - 0.5f) * 5.0f) / 5.0f;
+ } else if (input <= STAGE_CHILD_BACKWARD_BOTTOM_LEFT) {
+ result = 0.7f + ACCELERATE_INTERPOLATOR05.getInterpolation((input - 0.7f) * 5.0f) / 3.33f;
+ } else {
+ result = 1.0f;
+ }
+
+ return result;
+ }
+ }
+
+ public static class Builder {
+ private Context mContext;
+
+ public Builder(Context mContext) {
+ this.mContext = mContext;
+ }
+
+ public CircleBroodLoadingRenderer build() {
+ CircleBroodLoadingRenderer loadingRenderer = new CircleBroodLoadingRenderer(mContext);
+ return loadingRenderer;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/shapechange/CoolWaitLoadingRenderer.java b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/shapechange/CoolWaitLoadingRenderer.java
new file mode 100644
index 00000000..a404e786
--- /dev/null
+++ b/Loadinglibrary/src/main/java/app/dinus/com/loadingdrawable/render/shapechange/CoolWaitLoadingRenderer.java
@@ -0,0 +1,270 @@
+package app.dinus.com.loadingdrawable.render.shapechange;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PathMeasure;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.DisplayMetrics;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import app.dinus.com.loadingdrawable.DensityUtil;
+import app.dinus.com.loadingdrawable.render.LoadingRenderer;
+
+public class CoolWaitLoadingRenderer extends LoadingRenderer {
+ private final Interpolator ACCELERATE_INTERPOLATOR08 = new AccelerateInterpolator(0.8f);
+ private final Interpolator ACCELERATE_INTERPOLATOR10 = new AccelerateInterpolator(1.0f);
+ private final Interpolator ACCELERATE_INTERPOLATOR15 = new AccelerateInterpolator(1.5f);
+
+ private final Interpolator DECELERATE_INTERPOLATOR03 = new DecelerateInterpolator(0.3f);
+ private final Interpolator DECELERATE_INTERPOLATOR05 = new DecelerateInterpolator(0.5f);
+ private final Interpolator DECELERATE_INTERPOLATOR08 = new DecelerateInterpolator(0.8f);
+ private final Interpolator DECELERATE_INTERPOLATOR10 = new DecelerateInterpolator(1.0f);
+
+ private static final Interpolator ACCELERATE_DECELERATE_INTERPOLATOR = new AccelerateDecelerateInterpolator();
+
+ private final float DEFAULT_WIDTH = 200.0f;
+ private final float DEFAULT_HEIGHT = 150.0f;
+ private final float DEFAULT_STROKE_WIDTH = 8.0f;
+ private final float WAIT_CIRCLE_RADIUS = 50.0f;
+
+ private static final float WAIT_TRIM_DURATION_OFFSET = 0.5f;
+ private static final float END_TRIM_DURATION_OFFSET = 1.0f;
+
+ private final long ANIMATION_DURATION = 2222;
+
+ private final Paint mPaint = new Paint();
+
+ private final Path mWaitPath = new Path();
+ private final Path mCurrentTopWaitPath = new Path();
+ private final Path mCurrentMiddleWaitPath = new Path();
+ private final Path mCurrentBottomWaitPath = new Path();
+ private final PathMeasure mWaitPathMeasure = new PathMeasure();
+
+ private final RectF mCurrentBounds = new RectF();
+
+ private float mStrokeWidth;
+ private float mWaitCircleRadius;
+ private float mOriginEndDistance;
+ private float mOriginStartDistance;
+ private float mWaitPathLength;
+
+ private int mTopColor;
+ private int mMiddleColor;
+ private int mBottomColor;
+
+ private CoolWaitLoadingRenderer(Context context) {
+ super(context);
+ init(context);
+ setupPaint();
+ }
+
+ private void init(Context context) {
+ mWidth = DensityUtil.dip2px(context, DEFAULT_WIDTH);
+ mHeight = DensityUtil.dip2px(context, DEFAULT_HEIGHT);
+ mStrokeWidth = DensityUtil.dip2px(context, DEFAULT_STROKE_WIDTH);
+ mWaitCircleRadius = DensityUtil.dip2px(context, WAIT_CIRCLE_RADIUS);
+
+ mTopColor = Color.WHITE;
+ mMiddleColor = Color.parseColor("#FFF3C742");
+ mBottomColor = Color.parseColor("#FF89CC59");
+
+ mDuration = ANIMATION_DURATION;
+ }
+
+ private void setupPaint() {
+ mPaint.setAntiAlias(true);
+ mPaint.setStrokeWidth(mStrokeWidth);
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setStrokeJoin(Paint.Join.ROUND);
+ mPaint.setStrokeCap(Paint.Cap.ROUND);
+ }
+
+ @Override
+ protected void draw(Canvas canvas, Rect bounds) {
+ int saveCount = canvas.save();
+ RectF arcBounds = mCurrentBounds;
+ arcBounds.set(bounds);
+
+ mPaint.setColor(mBottomColor);
+ canvas.drawPath(mCurrentBottomWaitPath, mPaint);
+
+ mPaint.setColor(mMiddleColor);
+ canvas.drawPath(mCurrentMiddleWaitPath, mPaint);
+
+ mPaint.setColor(mTopColor);
+ canvas.drawPath(mCurrentTopWaitPath, mPaint);
+
+ canvas.restoreToCount(saveCount);
+ }
+
+ private Path createWaitPath(RectF bounds) {
+ Path path = new Path();
+ //create circle
+ path.moveTo(bounds.centerX() + mWaitCircleRadius, bounds.centerY());
+
+ //create w
+ path.cubicTo(bounds.centerX() + mWaitCircleRadius, bounds.centerY() - mWaitCircleRadius * 0.5f,
+ bounds.centerX() + mWaitCircleRadius * 0.3f, bounds.centerY() - mWaitCircleRadius,
+ bounds.centerX() - mWaitCircleRadius * 0.35f, bounds.centerY() + mWaitCircleRadius * 0.5f);
+ path.quadTo(bounds.centerX() + mWaitCircleRadius, bounds.centerY() - mWaitCircleRadius,
+ bounds.centerX() + mWaitCircleRadius * 0.05f, bounds.centerY() + mWaitCircleRadius * 0.5f);
+ path.lineTo(bounds.centerX() + mWaitCircleRadius * 0.75f, bounds.centerY() - mWaitCircleRadius * 0.2f);
+
+ path.cubicTo(bounds.centerX(), bounds.centerY() + mWaitCircleRadius * 1f,
+ bounds.centerX() + mWaitCircleRadius, bounds.centerY() + mWaitCircleRadius * 0.4f,
+ bounds.centerX() + mWaitCircleRadius, bounds.centerY());
+
+ //create arc
+ path.arcTo(new RectF(bounds.centerX() - mWaitCircleRadius, bounds.centerY() - mWaitCircleRadius,
+ bounds.centerX() + mWaitCircleRadius, bounds.centerY() + mWaitCircleRadius), 0, -359);
+ path.arcTo(new RectF(bounds.centerX() - mWaitCircleRadius, bounds.centerY() - mWaitCircleRadius,
+ bounds.centerX() + mWaitCircleRadius, bounds.centerY() + mWaitCircleRadius), 1, -359);
+ path.arcTo(new RectF(bounds.centerX() - mWaitCircleRadius, bounds.centerY() - mWaitCircleRadius,
+ bounds.centerX() + mWaitCircleRadius, bounds.centerY() + mWaitCircleRadius), 2, -2);
+ //create w
+ path.cubicTo(bounds.centerX() + mWaitCircleRadius, bounds.centerY() - mWaitCircleRadius * 0.5f,
+ bounds.centerX() + mWaitCircleRadius * 0.3f, bounds.centerY() - mWaitCircleRadius,
+ bounds.centerX() - mWaitCircleRadius * 0.35f, bounds.centerY() + mWaitCircleRadius * 0.5f);
+ path.quadTo(bounds.centerX() + mWaitCircleRadius, bounds.centerY() - mWaitCircleRadius,
+ bounds.centerX() + mWaitCircleRadius * 0.05f, bounds.centerY() + mWaitCircleRadius * 0.5f);
+ path.lineTo(bounds.centerX() + mWaitCircleRadius * 0.75f, bounds.centerY() - mWaitCircleRadius * 0.2f);
+
+ path.cubicTo(bounds.centerX(), bounds.centerY() + mWaitCircleRadius * 1f,
+ bounds.centerX() + mWaitCircleRadius, bounds.centerY() + mWaitCircleRadius * 0.4f,
+ bounds.centerX() + mWaitCircleRadius, bounds.centerY());
+
+ return path;
+ }
+
+ @Override
+ protected void computeRender(float renderProgress) {
+ if (mCurrentBounds.isEmpty()) {
+ return;
+ }
+
+ if (mWaitPath.isEmpty()) {
+ mWaitPath.set(createWaitPath(mCurrentBounds));
+ mWaitPathMeasure.setPath(mWaitPath, false);
+ mWaitPathLength = mWaitPathMeasure.getLength();
+
+ mOriginEndDistance = mWaitPathLength * 0.255f;
+ mOriginStartDistance = mWaitPathLength * 0.045f;
+ }
+
+ mCurrentTopWaitPath.reset();
+ mCurrentMiddleWaitPath.reset();
+ mCurrentBottomWaitPath.reset();
+
+ //draw the first half : top
+ if (renderProgress <= WAIT_TRIM_DURATION_OFFSET) {
+ float topTrimProgress = ACCELERATE_DECELERATE_INTERPOLATOR.getInterpolation(renderProgress / WAIT_TRIM_DURATION_OFFSET);
+ float topEndDistance = mOriginEndDistance + mWaitPathLength * 0.3f * topTrimProgress;
+ float topStartDistance = mOriginStartDistance + mWaitPathLength * 0.48f * topTrimProgress;
+ mWaitPathMeasure.getSegment(topStartDistance, topEndDistance, mCurrentTopWaitPath, true);
+ }
+
+ //draw the first half : middle
+ if (renderProgress > 0.02f * WAIT_TRIM_DURATION_OFFSET && renderProgress <= WAIT_TRIM_DURATION_OFFSET * 0.75f) {
+ float middleStartTrimProgress = ACCELERATE_INTERPOLATOR10.getInterpolation((renderProgress - 0.02f * WAIT_TRIM_DURATION_OFFSET) / (WAIT_TRIM_DURATION_OFFSET * 0.73f));
+ float middleEndTrimProgress = DECELERATE_INTERPOLATOR08.getInterpolation((renderProgress - 0.02f * WAIT_TRIM_DURATION_OFFSET) / (WAIT_TRIM_DURATION_OFFSET * 0.73f));
+
+ float middleEndDistance = mOriginStartDistance + mWaitPathLength * 0.42f * middleEndTrimProgress;
+ float middleStartDistance = mOriginStartDistance + mWaitPathLength * 0.42f * middleStartTrimProgress;
+ mWaitPathMeasure.getSegment(middleStartDistance, middleEndDistance, mCurrentMiddleWaitPath, true);
+ }
+
+ //draw the first half : bottom
+ if (renderProgress > 0.04f * WAIT_TRIM_DURATION_OFFSET && renderProgress <= WAIT_TRIM_DURATION_OFFSET * 0.75f) {
+ float bottomStartTrimProgress = ACCELERATE_INTERPOLATOR15.getInterpolation((renderProgress - 0.04f * WAIT_TRIM_DURATION_OFFSET) / (WAIT_TRIM_DURATION_OFFSET * 0.71f));
+ float bottomEndTrimProgress = DECELERATE_INTERPOLATOR05.getInterpolation((renderProgress - 0.04f * WAIT_TRIM_DURATION_OFFSET) / (WAIT_TRIM_DURATION_OFFSET * 0.71f));
+
+ float bottomEndDistance = mOriginStartDistance + mWaitPathLength * 0.42f * bottomEndTrimProgress;
+ float bottomStartDistance = mOriginStartDistance + mWaitPathLength * 0.42f * bottomStartTrimProgress;
+ mWaitPathMeasure.getSegment(bottomStartDistance, bottomEndDistance, mCurrentBottomWaitPath, true);
+ }
+
+ //draw the last half : top
+ if (renderProgress <= END_TRIM_DURATION_OFFSET && renderProgress > WAIT_TRIM_DURATION_OFFSET) {
+ float trimProgress = ACCELERATE_DECELERATE_INTERPOLATOR.getInterpolation((renderProgress - WAIT_TRIM_DURATION_OFFSET) / (END_TRIM_DURATION_OFFSET - WAIT_TRIM_DURATION_OFFSET));
+ float topEndDistance = mOriginEndDistance + mWaitPathLength * 0.3f + mWaitPathLength * 0.45f * trimProgress;
+ float topStartDistance = mOriginStartDistance + mWaitPathLength * 0.48f + mWaitPathLength * 0.27f * trimProgress;
+ mWaitPathMeasure.getSegment(topStartDistance, topEndDistance, mCurrentTopWaitPath, true);
+ }
+
+ //draw the last half : middle
+ if (renderProgress > WAIT_TRIM_DURATION_OFFSET + 0.02f * WAIT_TRIM_DURATION_OFFSET && renderProgress <= WAIT_TRIM_DURATION_OFFSET + WAIT_TRIM_DURATION_OFFSET * 0.62f) {
+ float middleStartTrimProgress = ACCELERATE_INTERPOLATOR08.getInterpolation((renderProgress - WAIT_TRIM_DURATION_OFFSET - 0.02f * WAIT_TRIM_DURATION_OFFSET) / (WAIT_TRIM_DURATION_OFFSET * 0.60f));
+ float middleEndTrimProgress = DECELERATE_INTERPOLATOR03.getInterpolation((renderProgress - WAIT_TRIM_DURATION_OFFSET - 0.02f * WAIT_TRIM_DURATION_OFFSET) / (WAIT_TRIM_DURATION_OFFSET * 0.60f));
+
+ float middleEndDistance = mOriginStartDistance + mWaitPathLength * 0.48f + mWaitPathLength * 0.20f * middleEndTrimProgress;
+ float middleStartDistance = mOriginStartDistance + mWaitPathLength * 0.48f + mWaitPathLength * 0.10f * middleStartTrimProgress;
+ mWaitPathMeasure.getSegment(middleStartDistance, middleEndDistance, mCurrentMiddleWaitPath, true);
+ }
+
+ if (renderProgress > WAIT_TRIM_DURATION_OFFSET + 0.62f * WAIT_TRIM_DURATION_OFFSET && renderProgress <= END_TRIM_DURATION_OFFSET) {
+ float middleStartTrimProgress = DECELERATE_INTERPOLATOR10.getInterpolation((renderProgress - WAIT_TRIM_DURATION_OFFSET - 0.62f * WAIT_TRIM_DURATION_OFFSET) / (WAIT_TRIM_DURATION_OFFSET * 0.38f));
+ float middleEndTrimProgress = DECELERATE_INTERPOLATOR03.getInterpolation((renderProgress - WAIT_TRIM_DURATION_OFFSET - 0.62f * WAIT_TRIM_DURATION_OFFSET) / (WAIT_TRIM_DURATION_OFFSET * 0.38f));
+
+ float middleEndDistance = mOriginStartDistance + mWaitPathLength * 0.68f + mWaitPathLength * 0.325f * middleEndTrimProgress;
+ float middleStartDistance = mOriginStartDistance + mWaitPathLength * 0.58f + mWaitPathLength * 0.17f * middleStartTrimProgress;
+ mWaitPathMeasure.getSegment(middleStartDistance, middleEndDistance, mCurrentMiddleWaitPath, true);
+ }
+
+ //draw the last half : bottom
+ if (renderProgress > WAIT_TRIM_DURATION_OFFSET + 0.10f * WAIT_TRIM_DURATION_OFFSET && renderProgress <= WAIT_TRIM_DURATION_OFFSET + WAIT_TRIM_DURATION_OFFSET * 0.70f) {
+ float bottomStartTrimProgress = ACCELERATE_INTERPOLATOR15.getInterpolation((renderProgress - WAIT_TRIM_DURATION_OFFSET - 0.10f * WAIT_TRIM_DURATION_OFFSET) / (WAIT_TRIM_DURATION_OFFSET * 0.60f));
+ float bottomEndTrimProgress = DECELERATE_INTERPOLATOR03.getInterpolation((renderProgress - WAIT_TRIM_DURATION_OFFSET - 0.10f * WAIT_TRIM_DURATION_OFFSET) / (WAIT_TRIM_DURATION_OFFSET * 0.60f));
+
+ float bottomEndDistance = mOriginStartDistance + mWaitPathLength * 0.48f + mWaitPathLength * 0.20f * bottomEndTrimProgress;
+ float bottomStartDistance = mOriginStartDistance + mWaitPathLength * 0.48f + mWaitPathLength * 0.10f * bottomStartTrimProgress;
+ mWaitPathMeasure.getSegment(bottomStartDistance, bottomEndDistance, mCurrentBottomWaitPath, true);
+ }
+
+ if (renderProgress > WAIT_TRIM_DURATION_OFFSET + 0.70f * WAIT_TRIM_DURATION_OFFSET && renderProgress <= END_TRIM_DURATION_OFFSET) {
+ float bottomStartTrimProgress = DECELERATE_INTERPOLATOR05.getInterpolation((renderProgress - WAIT_TRIM_DURATION_OFFSET - 0.70f * WAIT_TRIM_DURATION_OFFSET) / (WAIT_TRIM_DURATION_OFFSET * 0.30f));
+ float bottomEndTrimProgress = DECELERATE_INTERPOLATOR03.getInterpolation((renderProgress - WAIT_TRIM_DURATION_OFFSET - 0.70f * WAIT_TRIM_DURATION_OFFSET) / (WAIT_TRIM_DURATION_OFFSET * 0.30f));
+
+ float bottomEndDistance = mOriginStartDistance + mWaitPathLength * 0.68f + mWaitPathLength * 0.325f * bottomEndTrimProgress;
+ float bottomStartDistance = mOriginStartDistance + mWaitPathLength * 0.58f + mWaitPathLength * 0.17f * bottomStartTrimProgress;
+ mWaitPathMeasure.getSegment(bottomStartDistance, bottomEndDistance, mCurrentBottomWaitPath, true);
+ }
+
+ }
+
+ @Override
+ protected void setAlpha(int alpha) {
+ mPaint.setAlpha(alpha);
+
+ }
+
+ @Override
+ protected void setColorFilter(ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+
+ }
+
+ @Override
+ protected void reset() {
+ }
+
+ public static class Builder {
+ private Context mContext;
+
+ public Builder(Context mContext) {
+ this.mContext = mContext;
+ }
+
+ public CoolWaitLoadingRenderer build() {
+ CoolWaitLoadingRenderer loadingRenderer = new CoolWaitLoadingRenderer(mContext);
+ return loadingRenderer;
+ }
+ }
+}
diff --git a/Loadinglibrary/src/main/res/drawable-xhdpi/ic_eletric_fan.png b/Loadinglibrary/src/main/res/drawable-xhdpi/ic_eletric_fan.png
new file mode 100644
index 00000000..671be789
Binary files /dev/null and b/Loadinglibrary/src/main/res/drawable-xhdpi/ic_eletric_fan.png differ
diff --git a/Loadinglibrary/src/main/res/drawable-xhdpi/ic_leaf.png b/Loadinglibrary/src/main/res/drawable-xhdpi/ic_leaf.png
new file mode 100644
index 00000000..d2e62924
Binary files /dev/null and b/Loadinglibrary/src/main/res/drawable-xhdpi/ic_leaf.png differ
diff --git a/Loadinglibrary/src/main/res/drawable-xhdpi/ic_loading.png b/Loadinglibrary/src/main/res/drawable-xhdpi/ic_loading.png
new file mode 100644
index 00000000..a87f7803
Binary files /dev/null and b/Loadinglibrary/src/main/res/drawable-xhdpi/ic_loading.png differ
diff --git a/Loadinglibrary/src/main/res/values/attrs.xml b/Loadinglibrary/src/main/res/values/attrs.xml
new file mode 100644
index 00000000..540c1491
--- /dev/null
+++ b/Loadinglibrary/src/main/res/values/attrs.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Loadinglibrary/src/main/res/values/strings.xml b/Loadinglibrary/src/main/res/values/strings.xml
new file mode 100644
index 00000000..6d518563
--- /dev/null
+++ b/Loadinglibrary/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Library
+
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 30daf5d0..d98ee460 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -945,3 +945,9 @@ public static java.lang.String TABLENAME;
-keep class **.R$string { *; }
+-keep class com.qcloud.cos.** { *; }
+-dontwarn com.qcloud.cos.**
+-keep class com.qcloud.cos.model.** { *; }
+-keep class * implements com.qcloud.cos.** { *; }
+-keepattributes Signature, *Annotation*
+-keep class com.qcloud.cos.**$* { *; }
diff --git a/app/releas/release/output-metadata.json b/app/releas/release/output-metadata.json
index fc35392e..e350501c 100644
--- a/app/releas/release/output-metadata.json
+++ b/app/releas/release/output-metadata.json
@@ -11,9 +11,9 @@
"type": "SINGLE",
"filters": [],
"attributes": [],
- "versionCode": 40,
- "versionName": "1.0.5.2",
- "outputFile": "羽声_1.0.5.2_40.apk"
+ "versionCode": 43,
+ "versionName": "1.0.5.3",
+ "outputFile": "羽声_1.0.5.3_43.apk"
}
],
"elementType": "File",
@@ -22,14 +22,14 @@
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
- "baselineProfiles/1/羽声_1.0.5.2_40.dm"
+ "baselineProfiles/1/羽声_1.0.5.3_43.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
- "baselineProfiles/0/羽声_1.0.5.2_40.dm"
+ "baselineProfiles/0/羽声_1.0.5.3_43.dm"
]
}
],
diff --git a/app/release/baselineProfiles/0/秘地_1.0.5_101.dm b/app/release/baselineProfiles/0/秘地_1.0.5_101.dm
deleted file mode 100644
index 1451fef3..00000000
Binary files a/app/release/baselineProfiles/0/秘地_1.0.5_101.dm and /dev/null differ
diff --git a/app/release/baselineProfiles/1/秘地_1.0.5_101.dm b/app/release/baselineProfiles/1/秘地_1.0.5_101.dm
deleted file mode 100644
index b10faf54..00000000
Binary files a/app/release/baselineProfiles/1/秘地_1.0.5_101.dm and /dev/null differ
diff --git a/app/release/秘地_1.0.5_101.apk b/app/release/秘地_1.0.5_101.apk
deleted file mode 100644
index 210e2737..00000000
Binary files a/app/release/秘地_1.0.5_101.apk and /dev/null differ
diff --git a/gradle.properties b/gradle.properties
index 7cfdcb11..90b7bb32 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -28,8 +28,8 @@ isBuildModule=false
#org.gradle.deamon=false
android.injected.testOnly=false
-APP_VERSION_NAME=1.0.5.2
-APP_VERSION_CODE=40
+APP_VERSION_NAME=1.0.5.3
+APP_VERSION_CODE=43
org.gradle.jvm.toolchain.useLegacyAdapters=false
#org.gradle.java.home=C\:\\Users\\qx\\.jdks\\ms-17.0.15
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index a0b83414..16a6b757 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -78,6 +78,7 @@ appcompatVersion = "1.3.1"
legacySupportV4 = "1.0.0"
fragmentKtx = "1.5.6"
cosAndroidLite = "5.9.46"
+interpolator = "1.0.0"
[libraries]
alipay-alipaysdk-android = { module = "com.alipay.sdk:alipaysdk-android", version.ref = "alipayAlipaysdkAndroid" }
@@ -162,6 +163,7 @@ zcw-togglebutton-library = { module = "com.zcw:togglebutton-library", version.re
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompatVersion" }
androidx-legacy-support-v4 = { group = "androidx.legacy", name = "legacy-support-v4", version.ref = "legacySupportV4" }
androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" }
+androidx-interpolator = { group = "androidx.interpolator", name = "interpolator", version.ref = "interpolator" }
[plugins]
diff --git a/moduleLogin/src/main/java/com/xscm/modulelogin/present/ImproveInfoPresenter.java b/moduleLogin/src/main/java/com/xscm/modulelogin/present/ImproveInfoPresenter.java
index bb428bb8..1245787b 100644
--- a/moduleLogin/src/main/java/com/xscm/modulelogin/present/ImproveInfoPresenter.java
+++ b/moduleLogin/src/main/java/com/xscm/modulelogin/present/ImproveInfoPresenter.java
@@ -4,6 +4,7 @@ import android.content.Context;
import com.hjq.toast.ToastUtils;
+import com.xscm.moduleutil.base.CommonAppContext;
import com.xscm.moduleutil.bean.UserBean;
import com.xscm.moduleutil.http.BaseObserver;
import com.xscm.moduleutil.presenter.BasePresenter;
@@ -26,7 +27,7 @@ public class ImproveInfoPresenter extends BasePresenter extends
// 这里可以根据实际需求实现跳转逻辑
// 例如:跳转到礼物详情页面、用户主页等
// 使用缓存数据进入房间
- RoomManager.getInstance().fetchRoomDataAndEnter(getApplicationContext(), xlhBean.getRoom_id(), "");
+ RoomManager.getInstance().fetchRoomDataAndEnter(getApplicationContext(), xlhBean.getRoom_id(), "",null);
// ARouter.getInstance().build(ARouteConstants.ROOM_DETAILS).withString("from", "我的界面").withString("roomId", xlhBean.getRoom_id()).navigation();
}
@@ -1166,7 +1166,7 @@ public abstract class BaseAppCompatActivity extends
// 这里可以根据实际需求实现跳转逻辑
// 例如:跳转到礼物详情页面、用户主页等
// 使用缓存数据进入房间
- RoomManager.getInstance().fetchRoomDataAndEnter(getApplicationContext(), redBean.getRoom_id(), "");
+ RoomManager.getInstance().fetchRoomDataAndEnter(getApplicationContext(), redBean.getRoom_id(), "",null);
// ARouter.getInstance().build(ARouteConstants.ROOM_DETAILS).withString("from", "我的界面").withString("roomId", xlhBean.getRoom_id()).navigation();
}
diff --git a/moduleUtil/src/main/java/com/xscm/moduleutil/activity/WebViewActivity.java b/moduleUtil/src/main/java/com/xscm/moduleutil/activity/WebViewActivity.java
index 934dafae..ea4a4f46 100644
--- a/moduleUtil/src/main/java/com/xscm/moduleutil/activity/WebViewActivity.java
+++ b/moduleUtil/src/main/java/com/xscm/moduleutil/activity/WebViewActivity.java
@@ -390,7 +390,7 @@ public class WebViewActivity extends BaseAppCompatActivity0){
- RoomManager.getInstance().fetchRoomDataAndEnter(getApplicationContext(), item.getRoom_id()+"","");
+ RoomManager.getInstance().fetchRoomDataAndEnter(getApplicationContext(), item.getRoom_id()+"","",null);
// ARouter.getInstance().build(ARouteConstants.ROOM_DETAILS).withString("roomId", item.getRoom_id() + "").navigation();
}else if (item.getRoom_id() == 0 && item.getUrl() != null && !item.getUrl().isEmpty()){
Intent intent = new Intent(OfficialNoticeActivity.this, WebViewActivity.class);
diff --git a/moduleUtil/src/main/java/com/xscm/moduleutil/base/CommonAppContext.java b/moduleUtil/src/main/java/com/xscm/moduleutil/base/CommonAppContext.java
index 3c2f757b..b74d2880 100644
--- a/moduleUtil/src/main/java/com/xscm/moduleutil/base/CommonAppContext.java
+++ b/moduleUtil/src/main/java/com/xscm/moduleutil/base/CommonAppContext.java
@@ -459,7 +459,7 @@ public class CommonAppContext extends MultiDexApplication implements Applicatio
SpUtil.getInstance().setBooleanValue("youth_model_shown", false);
startInitSdk();
// 初始化(通常在Application或Activity的onCreate中)
- CosUploadManager.getInstance().init(this);
+ CosUploadManager.getInstance(CommonAppContext.getInstance());
// 启动IM连接服务
// IMServiceManager.getInstance().startIMService(this);
}
diff --git a/moduleUtil/src/main/java/com/xscm/moduleutil/base/RoomManager.java b/moduleUtil/src/main/java/com/xscm/moduleutil/base/RoomManager.java
index e886c63d..c72f432b 100644
--- a/moduleUtil/src/main/java/com/xscm/moduleutil/base/RoomManager.java
+++ b/moduleUtil/src/main/java/com/xscm/moduleutil/base/RoomManager.java
@@ -35,6 +35,7 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import io.reactivex.disposables.Disposable;
+import org.jetbrains.annotations.NotNull;
/**
* 房间管理器
@@ -102,10 +103,10 @@ public class RoomManager {
if (roomInfo != null) {
// 使用缓存数据直接进入房间
- navigateToRoom(context, roomId, password, roomInfo, false);
+ navigateToRoom(context, roomId, password, roomInfo, false,null);
} else {
// 获取房间数据后进入房间
- fetchRoomDataAndEnter(context, roomId, password);
+ fetchRoomDataAndEnter(context, roomId, password,null);
}
}
@@ -116,19 +117,20 @@ public class RoomManager {
* @param roomId 房间ID
* @param password 房间密码
*/
- public void fetchRoomDataAndEnter(Context context, String roomId, String password) {
+ public void fetchRoomDataAndEnter(Context context, String roomId, String password,String taskId) {
// 显示加载提示
// 这里可以根据需要添加加载对话框
if (CommonAppContext.getInstance().isRoomJoininj){
return;
}
+
CommonAppContext.getInstance().isRoomJoininj=true;
// 检查是否有有效的缓存数据
RoomInfoResp roomInfo = getCachedRoomData(roomId);
// 检查是否是当前房间且用户在线
// boolean isCurrentRoom = isCurrentRoom(roomId);
if (CommonAppContext.getInstance().playId == null) {
- fetchAndJoinRoom(context, roomId, password);
+ fetchAndJoinRoom(context, roomId, password,taskId);
} else {
if (!CommonAppContext.getInstance().playId.equals(roomId)) {
MessageListenerSingleton.getInstance().joinGroup(roomId);
@@ -137,11 +139,13 @@ public class RoomManager {
CommonAppContext.getInstance().isPlaying = false;
CommonAppContext.getInstance().isRoomJoininj=false;
EventBus.getDefault().post(new RoomOutEvent());
+// fetchAndJoinRoom(context, roomId, password);
+// return;
} else if (CommonAppContext.getInstance().lable_id.equals("6")) {
- upInfo(context, roomId, password, true, roomInfo, true);
+ upInfo(context, roomId, password, true, roomInfo, true,taskId);
return;
}
- isUserOnline(context, roomId, password, roomInfo);
+ isUserOnline(context, roomId, password, roomInfo,taskId);
}
@@ -195,58 +199,26 @@ public class RoomManager {
// navigateToRoom(context, roomId, password, null);
}
- private void upInfo(Context context, String roomId, String password, boolean isOnline, RoomInfoResp roomInfo, boolean isCurrentRoom) {
+ private void upInfo(Context context, String roomId, String password, boolean isOnline, RoomInfoResp roomInfo, boolean isCurrentRoom,String taskId) {
if (isOnline) {
- navigateToRoom(context, roomId, password, roomInfo, isOnline);
+ RetrofitClient.getInstance().postRoomInfo(roomId, new BaseObserver() {
+
+ @Override
+ public void onSubscribe(@NotNull Disposable disposable) {
+
+ }
+
+ @Override
+ public void onNext(@NotNull RoomInfoResp roomInfoResp) {
+ navigateToRoom(context, roomId, password, roomInfoResp, false,taskId);
+ }
+ });
+// navigateToRoom(context, roomId, password, roomInfo, isOnline);
} else {
-// CommonAppContext.getInstance().isShow = false;
-// CommonAppContext.getInstance().isPlaying = false;
-// EventBus.getDefault().post(new RoomOutEvent());
-// try {
-// Thread.sleep(300);
-// } catch (InterruptedException e) {
-// Thread.currentThread().interrupt();
-// }
- fetchAndJoinRoom(context, roomId, password);
+ fetchAndJoinRoom(context, roomId, password,taskId);
}
-
-
-// if (isCurrentRoom&& isOnline) {
-// if (roomInfo != null) {
-// navigateToRoom(context, roomId, password, roomInfo);
-// } else {
-// // 即使在线,如果没有缓存数据,也需要获取数据
-// fetchAndJoinRoom(context, roomId, password);
-// }
-// return;
-// }
-
-
- // 如果有缓存数据且用户在线,使用缓存数据进入房间
-// if (roomInfo != null && isOnline) {
-// RetrofitClient.getInstance().postRoomInfo(roomId, new BaseObserver() {
-//
-// @Override
-// public void onSubscribe(Disposable d) {
-//
-// }
-//
-// @Override
-// public void onNext(RoomInfoResp roomInfoResp) {
-//// cacheRoomData(roomId, roomInfo);
-// navigateToRoom(context, roomId, password, roomInfoResp);
-// }
-// });
-
-// cacheRoomData(roomId, roomInfo);
-// navigateToRoom(context, roomId, password, roomInfo);
- return;
-// }
-
- // 其他情况,获取新的房间数据并加入房间
-// fetchAndJoinRoom(context, roomId, password);
}
/**
@@ -256,7 +228,7 @@ public class RoomManager {
* @param roomId 房间ID
* @param password 房间密码
*/
- private void fetchAndJoinRoom(Context context, String roomId, String password) {
+ private void fetchAndJoinRoom(Context context, String roomId, String password,String taskId) {
// 获取房间数据
// 等待一段时间确保退出完成
@@ -266,7 +238,6 @@ public class RoomManager {
// Thread.currentThread().interrupt();
// }
// navigateToRoom(context, roomId, password, null, false);
- LogUtils.dTag("RoomActivity", "fetchAndJoinRoom:"+roomId.toString());
RetrofitClient.getInstance().roomGetIn(roomId, password, new BaseObserver() {
@Override
@@ -292,7 +263,12 @@ public class RoomManager {
AgoraManager.getInstance(context)
.joinRoom(token, roomId, uid, enableMic, enableJs);
cacheRoomData(roomId, resp);
- navigateToRoom(context, roomId, password, resp, false);
+ navigateToRoom(context, roomId, password, resp, false,taskId);
+ }
+
+ @Override
+ public void onError(Throwable e) {
+ super.onError(e);
}
});
}
@@ -328,8 +304,7 @@ public class RoomManager {
* @param password 房间密码
* @param roomInfo 房间信息
*/
- private void navigateToRoom(Context context, String roomId, String password, RoomInfoResp roomInfo, boolean isOnline) {
- LogUtils.dTag("RoomActivity", "navigateToRoom"+roomInfo.toString());
+ private void navigateToRoom(Context context, String roomId, String password, RoomInfoResp roomInfo, boolean isOnline,String taskId) {
try {
// 构建跳转参数
@@ -340,28 +315,23 @@ public class RoomManager {
if (!TextUtils.isEmpty(password)) {
bundle.putString("password", password);
}
+ if (taskId != null){
+ bundle.putString("taskId", taskId);
+ }
if (roomInfo == null){
LogUtils.dTag("RoomActivity", "navigateToRoom:房间信息获取存在问题");
return;
}
- if (isOnline){
- ARouter.getInstance()
- .build(ARouteConstants.ROOM_DETAILS)
-
- .navigation(context);
- }else {
// 使用ARouter跳转到房间页面
ARouter.getInstance()
.build(ARouteConstants.ROOM_DETAILS)
-
.with(bundle)
.navigation(context);
- }
-
} catch (Exception e) {
Logger.e(TAG, "跳转房间页面失败: " + e.getMessage());
+ }finally {
}
}
@@ -416,7 +386,7 @@ public class RoomManager {
* @param roomId 房间ID
* @return true表示用户在线,false表示不在线
*/
- private boolean isUserOnline(Context context, String roomId, String password, RoomInfoResp roomInfo) {
+ private boolean isUserOnline(Context context, String roomId, String password, RoomInfoResp roomInfo,String taskId) {
// 这里应该实现检查用户是否在线的逻辑
// 可以通过检查Agora是否还在房间中,或者通过服务端接口查询用户状态等方式实现
// 目前返回false,需要根据实际需求实现具体逻辑
@@ -426,6 +396,9 @@ public class RoomManager {
// } catch (InterruptedException e) {
// Thread.currentThread().interrupt();
// }
+
+
+
final boolean[] isOnline = {false};
RetrofitClient.getInstance().getRoomOnline(roomId, "1", "50", new BaseObserver() {
@Override
@@ -453,7 +426,8 @@ public class RoomManager {
}
}
}
- upInfo(context, roomId, password, isOnline[0], roomInfo, true);
+ upInfo(context, roomId, password, isOnline[0], roomInfo, true, taskId);
+
} else {
isOnline[0] = false;
}
@@ -462,11 +436,11 @@ public class RoomManager {
e.printStackTrace();
isOnline[0] = false;
// 即使出现异常也继续执行
- upInfo(context, roomId, password, isOnline[0], roomInfo, true);
+ upInfo(context, roomId, password, isOnline[0], roomInfo, true, taskId);
}
}
});
- return isOnline[0];
+ return false;
}
/**
diff --git a/moduleUtil/src/main/java/com/xscm/moduleutil/dialog/LoadingDialog.java b/moduleUtil/src/main/java/com/xscm/moduleutil/dialog/LoadingDialog.java
new file mode 100644
index 00000000..c1e92316
--- /dev/null
+++ b/moduleUtil/src/main/java/com/xscm/moduleutil/dialog/LoadingDialog.java
@@ -0,0 +1,49 @@
+package com.xscm.moduleutil.dialog;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import com.xscm.moduleutil.R;
+
+/**
+ * com.xscm.moduleutil.dialog
+ * qx
+ * 2025/10/27
+ */
+public class LoadingDialog extends Dialog {
+
+
+ public LoadingDialog(@NonNull Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // 设置布局(这里假设你有一个R.layout.loading_dialog布局文件)
+ setContentView(R.layout.loading_dialog);
+
+ // 设置不可取消
+ setCancelable(false);
+ }
+
+ /**
+ * 显示加载对话框
+ */
+ public void showLoading() {
+ if (!isShowing()) {
+ show();
+ }
+ }
+
+ /**
+ * 隐藏加载对话框
+ */
+ public void hideLoading() {
+ if (isShowing()) {
+ dismiss();
+ }
+ }
+}
diff --git a/moduleUtil/src/main/java/com/xscm/moduleutil/dialog/RoomAuctionWebViewDialog.java b/moduleUtil/src/main/java/com/xscm/moduleutil/dialog/RoomAuctionWebViewDialog.java
index ed1ff088..d737f32d 100644
--- a/moduleUtil/src/main/java/com/xscm/moduleutil/dialog/RoomAuctionWebViewDialog.java
+++ b/moduleUtil/src/main/java/com/xscm/moduleutil/dialog/RoomAuctionWebViewDialog.java
@@ -178,7 +178,7 @@ public class RoomAuctionWebViewDialog extends BaseDialog {
@JavascriptInterface
public void jumpRoomPage(String room_id) {
- RoomManager.getInstance().fetchRoomDataAndEnter(getContext(), room_id,"");
+ RoomManager.getInstance().fetchRoomDataAndEnter(getContext(), room_id,"",null);
// ARouter.getInstance().build(ARouteConstants.ROOM_DETAILS).withString("form", "首页").withString("roomId", room_id).navigation();
}
diff --git a/moduleUtil/src/main/java/com/xscm/moduleutil/http/RetrofitClient.java b/moduleUtil/src/main/java/com/xscm/moduleutil/http/RetrofitClient.java
index 377fdab2..188129be 100644
--- a/moduleUtil/src/main/java/com/xscm/moduleutil/http/RetrofitClient.java
+++ b/moduleUtil/src/main/java/com/xscm/moduleutil/http/RetrofitClient.java
@@ -1938,6 +1938,7 @@ public class RetrofitClient {
public void onFailure(Call> call, Throwable t) {
MessageListenerSingleton.getInstance().quitGroup(roomId);
CommonAppContext.getInstance().isRoomJoininj = false;
+ observer.onError( t);
}
});
}
diff --git a/moduleUtil/src/main/java/com/xscm/moduleutil/rtc/AgoraManager.java b/moduleUtil/src/main/java/com/xscm/moduleutil/rtc/AgoraManager.java
index c15c17d3..2c69251b 100644
--- a/moduleUtil/src/main/java/com/xscm/moduleutil/rtc/AgoraManager.java
+++ b/moduleUtil/src/main/java/com/xscm/moduleutil/rtc/AgoraManager.java
@@ -445,6 +445,7 @@ public class AgoraManager {
@Override
public void onLyricResult(String requestId, long songCode, String lyricUrl, int reason) {
if (lyricUrl != null) {
+ LogUtils.e("roomInfoEvent",lyricUrl);
getLyricsInstance(lyricUrl);
}
}
@@ -1449,6 +1450,7 @@ public class AgoraManager {
.build();
if (StatusUtil.isCompleted(task)) {
MusicFileBean musicFileBean = new MusicFileBean();
+ LogUtils.e("roomInfoEvent",convertFileToByteArray(task.getFile()));
musicFileBean.setFileData(convertFileToByteArray(task.getFile()));
EventBus.getDefault().post(musicFileBean);
} else {
@@ -1477,6 +1479,7 @@ public class AgoraManager {
@Override
public void taskEnd(@NonNull DownloadTask task, @NonNull EndCause cause, @Nullable Exception realCause, @NonNull Listener1Assist.Listener1Model model) {
Logger.e("taskEnd", model);
+ LogUtils.e("roomInfoEvent",convertFileToByteArray(task.getFile()));
MusicFileBean musicFileBean = new MusicFileBean();
musicFileBean.setFileData(convertFileToByteArray(task.getFile()));
EventBus.getDefault().post(musicFileBean);
diff --git a/moduleUtil/src/main/java/com/xscm/moduleutil/utils/ARouteConstants.java b/moduleUtil/src/main/java/com/xscm/moduleutil/utils/ARouteConstants.java
index 3eaf1811..6252437f 100644
--- a/moduleUtil/src/main/java/com/xscm/moduleutil/utils/ARouteConstants.java
+++ b/moduleUtil/src/main/java/com/xscm/moduleutil/utils/ARouteConstants.java
@@ -26,6 +26,7 @@ public class ARouteConstants {
public static final String H5 ="/moduleUtil/WebViewActivity"; //网页
// public static final String GIFT_WALL ="/moduleroom/UserGiftWallFragment"; //实名认证
public static final String RECHARGE_ACTIVITY ="/modulevocal/RechargeActivity"; //实名认证
+ public static final String DailyTasksActivity ="/modulevocal/DailyTasksActivity"; //每日任务
diff --git a/moduleUtil/src/main/java/com/xscm/moduleutil/utils/cos/CosUploadManager.java b/moduleUtil/src/main/java/com/xscm/moduleutil/utils/cos/CosUploadManager.java
index c3d916c1..12a8e9b8 100644
--- a/moduleUtil/src/main/java/com/xscm/moduleutil/utils/cos/CosUploadManager.java
+++ b/moduleUtil/src/main/java/com/xscm/moduleutil/utils/cos/CosUploadManager.java
@@ -5,6 +5,7 @@ import android.os.Handler;
import android.os.Looper;
import androidx.annotation.Nullable;
import com.blankj.utilcode.util.LogUtils;
+import com.hjq.toast.ToastUtils;
import com.tencent.cos.xml.CosXmlService;
import com.tencent.cos.xml.CosXmlServiceConfig;
import com.tencent.cos.xml.exception.CosXmlClientException;
@@ -31,33 +32,33 @@ import org.jetbrains.annotations.NotNull;
*/
public class CosUploadManager {
private static volatile CosUploadManager instance;
- private Context context;
+ private Context mContext;
private Handler mainHandler;
// 私有构造函数,防止外部实例化
- private CosUploadManager() {
+ private CosUploadManager(Context context)
+ {
+ mContext = context;
mainHandler = new Handler(Looper.getMainLooper());
}
// 双重检查锁定获取单例实例
- public static CosUploadManager getInstance() {
+ public static CosUploadManager getInstance(Context context) {
if (instance == null) {
synchronized (CosUploadManager.class) {
if (instance == null) {
- instance = new CosUploadManager();
+ instance = new CosUploadManager(context);
}
}
}
return instance;
}
- public void init(Context context) {
- this.context = context.getApplicationContext();
- }
+
public void upParameters( String objectKey, String localPath, UploadCallback callback) {
// 确保已初始化
- if (context == null) {
- callback.onFailure(new IllegalStateException("CosUploadManager not initialized with context"));
- return;
- }
+// if (context == null) {
+// callback.onFailure1(new IllegalStateException("CosUploadManager not initialized with context"));
+// return;
+// }
RetrofitClient.getInstance().getTempKey(new BaseObserver() {
@Override
public void onSubscribe(@NotNull Disposable disposable) {
@@ -91,7 +92,7 @@ public class CosUploadManager {
.setRegion(region)
.isHttps(true) // 使用 HTTPS 请求, 默认为 HTTP 请求
.builder();
- CosXmlService cosXmlService = new CosXmlService(context, serviceConfig);
+ CosXmlService cosXmlService = new CosXmlService(mContext, serviceConfig);
// 任何 CosXmlRequest 都支持这种方式,例如上传 PutObjectRequest、下载 GetObjectRequest、删除 DeleteObjectRequest 等
// 以下用上传进行示例
@@ -142,5 +143,7 @@ public class CosUploadManager {
public interface UploadCallback {
void onSuccess(String url); // 上传成功,返回访问URL
void onFailure(Exception e); // 上传失败
+
+ void onFailure1(IllegalStateException e);
}
}
diff --git a/moduleUtil/src/main/java/com/xscm/moduleutil/widget/DropDayTaskView.java b/moduleUtil/src/main/java/com/xscm/moduleutil/widget/DropDayTaskView.java
new file mode 100644
index 00000000..f9021651
--- /dev/null
+++ b/moduleUtil/src/main/java/com/xscm/moduleutil/widget/DropDayTaskView.java
@@ -0,0 +1,288 @@
+package com.xscm.moduleutil.widget;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewAnimationUtils;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import com.blankj.utilcode.util.ScreenUtils;
+import com.xscm.moduleutil.utils.BarUtils;
+
+/**
+ * com.xscm.moduleutil.widget
+ * qx
+ * 2025/10/27
+ */
+public class DropDayTaskView extends LinearLayout {
+
+ private int rightMargin = 0;
+ private float lastX, lastY;
+ private int screenWidth;
+ private int screenHeight; // 添加屏幕高度变量
+
+ public DropDayTaskView(Context context) {
+ super(context);
+ init();
+ }
+
+ public DropDayTaskView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public DropDayTaskView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init();
+ }
+
+
+ void init() {
+ // 初始化屏幕尺寸
+ screenWidth = ScreenUtils.getScreenWidth();
+ screenHeight = ScreenUtils.getScreenHeight();
+
+ post(new Runnable() {
+ @Override
+ public void run() {
+ //设置初始位置
+ int sh = ScreenUtils.getScreenHeight();
+ int sw = ScreenUtils.getScreenWidth();
+// setBackgroundResource(R.drawable.bg_home_drop_view);
+ int y = (int) (0.7f * sh) - getHeight();
+ // 确保Y坐标不会超出屏幕范围
+ y = Math.max(0, Math.min(y, sh - getHeight()));
+ int x = sw - getWidth();
+ setTranslationX(x);
+ setTranslationY(y);
+ }
+ });
+
+ updateSize();
+ mStatusBarHeight = BarUtils.getStatusBarHeight();
+ }
+
+ /**
+ * 更新屏幕尺寸信息
+ */
+ protected void updateSize() {
+ ViewGroup viewGroup = (ViewGroup) getParent();
+ if (viewGroup != null) {
+ mScreenWidth = viewGroup.getWidth();
+ mScreenHeight = viewGroup.getHeight();
+ } else {
+ // 如果父视图为空,使用屏幕的实际宽度和高度
+ mScreenWidth = getResources().getDisplayMetrics().widthPixels;
+ mScreenHeight = getResources().getDisplayMetrics().heightPixels;
+ }
+ }
+
+ boolean starDrap = false;
+ float X1;
+ float X2;
+ float Y1;
+ float Y2;
+ // 记录视图初始位置
+ private float originalX;
+ private float originalY;
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ if (starDrap) return true;
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ X1 = event.getRawX();
+ Y1 = event.getRawY();
+ // 记录视图当前位置
+ originalX = getTranslationX();
+ originalY = getTranslationY();
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ X2 = event.getRawX();
+ Y2 = event.getRawY();
+ Action(X1, X2, Y1, Y2);
+ break;
+
+
+ }
+ return starDrap;
+ }
+
+ String TAG = "DropHourlView";
+
+ public boolean Action(float X1, float X2, float Y1, float Y2) {
+ float ComparedX = X2 - X1;//第二次的X坐标的位置减去第一次X坐标的位置,代表X坐标上的变化情况
+ float ComparedY = Y2 - Y1;//同理
+ //当X坐标的变化量的绝对值大于Y坐标的变化量的绝对值,以X坐标的变化情况作为判断依据
+ //上下左右的判断,都在一条直线上,但手指的操作不可能划直线,所有选择变化量大的方向上的量
+ //作为判断依据
+ if (Math.abs(ComparedX) > 30 || Math.abs(ComparedY) > 30) {
+ Log.i(TAG, "Action: 拖动");
+ starDrap = true;
+// setBackgroundResource(R.drawable.bg_home_drop_view);
+ return true;
+ } else {
+ starDrap = false;
+ return false;
+ }
+ }
+
+ private float mOriginalRawX;
+ private float mOriginalRawY;
+ private float mOriginalX;
+ private float mOriginalY;
+ protected int mScreenWidth;
+ private int mScreenHeight;
+ private int mStatusBarHeight;
+
+ private void updateViewPosition(MotionEvent event) {
+ // 计算新的Y位置
+ float desY = mOriginalY + event.getRawY() - mOriginalRawY;
+
+ // 限制Y位置不超出屏幕边界
+ if (desY < mStatusBarHeight) {
+ desY = mStatusBarHeight;
+ }
+ if (desY > mScreenHeight - getHeight()) {
+ desY = mScreenHeight - getHeight();
+ }
+
+ // 计算新的X位置
+ float desX = mOriginalX + event.getRawX() - mOriginalRawX;
+
+ // 限制X位置不超出屏幕边界
+ if (desX < 0) {
+ desX = 0;
+ }
+ if (desX > mScreenWidth - getWidth()) {
+ desX = mScreenWidth - getWidth();
+ }
+
+ // 设置视图的新位置
+ setX(desX);
+ setY(desY);
+ }
+
+ private void changeOriginalTouchParams(MotionEvent event) {
+ mOriginalX = getX();//getX()相对于控件X坐标的距离
+ mOriginalY = getY();
+ mOriginalRawX = event.getRawX();//getRawX()指控件在屏幕上的X坐标
+ mOriginalRawY = event.getRawY();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (event == null) {
+ return false;
+ }
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ changeOriginalTouchParams(event);
+ updateSize(); // 添加这行确保尺寸是最新的
+ // ... 其他现有代码 ...
+
+ break;
+ case MotionEvent.ACTION_MOVE:
+
+ updateViewPosition(event); // 使用更新后的带边界检查的方法
+
+// setBackgroundResource(R.drawable.bg_home_drop_view);
+ // 使用屏幕绝对坐标计算新位置
+// float newX = originalX + (event.getRawX() - X1);
+// float newY = originalY + (event.getRawY() - Y1);
+//
+// // 限制X和Y坐标在屏幕范围内
+// newX = Math.max(0, Math.min(newX, screenWidth - getWidth()));
+// newY = Math.max(0, Math.min(newY, screenHeight - getHeight()));
+//
+// setTranslationX(newX);
+// setTranslationY(newY);
+// X2 = event.getRawX();
+ break;
+ case MotionEvent.ACTION_UP:
+ starDrap = false;
+ int sw = ScreenUtils.getScreenWidth();
+ Log.i(TAG, "onTouchEvent: " + sw + "," + X2);
+ boolean isR = getTranslationX() + getWidth() / 2 >= sw / 2;//贴边方向
+
+ // 获取当前Y坐标
+ float currentY = getTranslationY();
+
+ // 创建X轴和Y轴的动画
+ ObjectAnimator animX = ObjectAnimator.ofFloat(this, "translationX", isR ? sw - getWidth() : 0f).setDuration(200);
+ // Y轴保持当前位置,但确保在屏幕范围内
+ currentY = Math.max(0, Math.min(currentY, screenHeight - getHeight()));
+ ObjectAnimator animY = ObjectAnimator.ofFloat(this, "translationY", currentY).setDuration(200);
+
+ animX.start();
+ animY.start();
+
+ break;
+
+ }
+
+ return true;
+ }
+
+
+ public void doRevealAnimation(View mPuppet, boolean flag) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ int[] vLocation = new int[2];
+ getLocationInWindow(vLocation);
+ int centerX = vLocation[0] + getMeasuredWidth() / 2;
+ int centerY = vLocation[1] + getMeasuredHeight() / 2;
+
+ int height = ScreenUtils.getScreenHeight();
+ int width = ScreenUtils.getScreenWidth();
+ int maxRradius = (int) Math.hypot(height, width);
+ Log.e("hei", maxRradius + "");
+
+ if (flag) {
+ mPuppet.setVisibility(VISIBLE);
+ Animator animator = ViewAnimationUtils.createCircularReveal(mPuppet, centerX, centerY, maxRradius, 0);
+ animator.setDuration(600);
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ mPuppet.setVisibility(View.GONE);
+ }
+ });
+ animator.start();
+ flag = false;
+ } else {
+ Animator animator = ViewAnimationUtils.createCircularReveal(mPuppet, centerX, centerY, 0, maxRradius);
+ animator.setDuration(1000);
+ animator.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mPuppet.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+
+ }
+ });
+ animator.start();
+ flag = true;
+ }
+ }
+ }
+}
diff --git a/moduleUtil/src/main/res/drawable/bg_r8_tm.xml b/moduleUtil/src/main/res/drawable/bg_r8_tm.xml
index 1f053f32..22c78766 100644
--- a/moduleUtil/src/main/res/drawable/bg_r8_tm.xml
+++ b/moduleUtil/src/main/res/drawable/bg_r8_tm.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/moduleUtil/src/main/res/drawable/ease_row_pubilc_sys_bg.xml b/moduleUtil/src/main/res/drawable/ease_row_pubilc_sys_bg.xml
index b2f611f8..8b9c2f00 100644
--- a/moduleUtil/src/main/res/drawable/ease_row_pubilc_sys_bg.xml
+++ b/moduleUtil/src/main/res/drawable/ease_row_pubilc_sys_bg.xml
@@ -4,8 +4,8 @@
xmlns:android="http://schemas.android.com/apk/res/android">
+ android:topLeftRadius="12dp"
+ android:topRightRadius="12dp"
+ android:bottomLeftRadius="12dp"
+ android:bottomRightRadius="12dp"/>
\ No newline at end of file
diff --git a/moduleUtil/src/main/res/drawable/ease_row_pubilc_user_bg.xml b/moduleUtil/src/main/res/drawable/ease_row_pubilc_user_bg.xml
index 3b941adc..17f9b81f 100644
--- a/moduleUtil/src/main/res/drawable/ease_row_pubilc_user_bg.xml
+++ b/moduleUtil/src/main/res/drawable/ease_row_pubilc_user_bg.xml
@@ -2,12 +2,12 @@
-
+ android:bottom="2dp">
-
+
\ No newline at end of file
diff --git a/moduleUtil/src/main/res/layout/ease_row_received_message_giftr_send.xml b/moduleUtil/src/main/res/layout/ease_row_received_message_giftr_send.xml
index 204cd23f..268e7345 100644
--- a/moduleUtil/src/main/res/layout/ease_row_received_message_giftr_send.xml
+++ b/moduleUtil/src/main/res/layout/ease_row_received_message_giftr_send.xml
@@ -6,100 +6,31 @@
android:id="@+id/bubble1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:minWidth="@dimen/dp_256"
- android:maxWidth="@dimen/dp_256"
- android:padding="@dimen/dp_8"
- android:background="@drawable/ease_row_pubilc_user_bg"
+ android:layout_marginTop="@dimen/dp_3"
+ android:layout_marginStart="@dimen/dp_8"
+ android:paddingStart="@dimen/dp_8"
+ android:paddingTop="@dimen/dp_2"
+ android:paddingBottom="@dimen/dp_2"
+ android:background="@drawable/ease_row_pubilc_sys_bg"
>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:lineHeight="@dimen/dp_20"
+ android:lineSpacingExtra="@dimen/dp_2"
+ android:paddingTop="@dimen/dp_2"
+ android:paddingBottom="@dimen/dp_2"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ android:textColor="@color/white"
+ android:textSize="@dimen/sp_12"
+ tools:text="饶利: 潇洒亼◇生2.0:"
+ tools:visibility="visible"/>
-
-
-
-
diff --git a/moduleUtil/src/main/res/layout/ease_row_received_message_system.xml b/moduleUtil/src/main/res/layout/ease_row_received_message_system.xml
index 31765018..8b9089ad 100644
--- a/moduleUtil/src/main/res/layout/ease_row_received_message_system.xml
+++ b/moduleUtil/src/main/res/layout/ease_row_received_message_system.xml
@@ -4,9 +4,10 @@
android:layout_height="wrap_content"
android:id="@+id/bubble"
android:layout_marginTop="@dimen/dp_3"
- android:layout_marginBottom="@dimen/dp_3"
- android:padding="@dimen/dp_8"
android:layout_marginStart="@dimen/dp_8"
+ android:paddingStart="@dimen/dp_8"
+ android:paddingTop="@dimen/dp_2"
+ android:paddingBottom="@dimen/dp_2"
android:background="@drawable/ease_row_pubilc_sys_bg"
>
@@ -21,11 +22,11 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
- android:paddingTop="@dimen/dp_5"
- android:paddingBottom="@dimen/dp_5"
+ android:paddingTop="@dimen/dp_2"
+ android:paddingBottom="@dimen/dp_2"
android:paddingEnd="@dimen/dp_8"
android:text="我是房间公告"
- android:textSize="@dimen/sp_14"/>
+ android:textSize="@dimen/sp_11"/>
diff --git a/moduleUtil/src/main/res/layout/loading_dialog.xml b/moduleUtil/src/main/res/layout/loading_dialog.xml
new file mode 100644
index 00000000..964764ec
--- /dev/null
+++ b/moduleUtil/src/main/res/layout/loading_dialog.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/moduleUtil/src/main/res/mipmap-hdpi/day_task.webp b/moduleUtil/src/main/res/mipmap-hdpi/day_task.webp
new file mode 100644
index 00000000..85de5c22
Binary files /dev/null and b/moduleUtil/src/main/res/mipmap-hdpi/day_task.webp differ
diff --git a/moduleUtil/src/main/res/mipmap-hdpi/room_sound_effects.webp b/moduleUtil/src/main/res/mipmap-hdpi/room_sound_effects.webp
new file mode 100644
index 00000000..a607535b
Binary files /dev/null and b/moduleUtil/src/main/res/mipmap-hdpi/room_sound_effects.webp differ
diff --git a/moduleUtil/src/main/res/mipmap-xhdpi/day_task.webp b/moduleUtil/src/main/res/mipmap-xhdpi/day_task.webp
new file mode 100644
index 00000000..9161936e
Binary files /dev/null and b/moduleUtil/src/main/res/mipmap-xhdpi/day_task.webp differ
diff --git a/moduleUtil/src/main/res/mipmap-xhdpi/room_sound_effects.webp b/moduleUtil/src/main/res/mipmap-xhdpi/room_sound_effects.webp
new file mode 100644
index 00000000..dc627413
Binary files /dev/null and b/moduleUtil/src/main/res/mipmap-xhdpi/room_sound_effects.webp differ
diff --git a/moduleUtil/src/main/res/mipmap-xxhdpi/day_task.webp b/moduleUtil/src/main/res/mipmap-xxhdpi/day_task.webp
new file mode 100644
index 00000000..3a26d66f
Binary files /dev/null and b/moduleUtil/src/main/res/mipmap-xxhdpi/day_task.webp differ
diff --git a/moduleUtil/src/main/res/mipmap-xxhdpi/room_sound_effects.webp b/moduleUtil/src/main/res/mipmap-xxhdpi/room_sound_effects.webp
new file mode 100644
index 00000000..1eaf9b96
Binary files /dev/null and b/moduleUtil/src/main/res/mipmap-xxhdpi/room_sound_effects.webp differ
diff --git a/moduleUtil/src/main/res/mipmap-xxxhdpi/room_sound_effects.webp b/moduleUtil/src/main/res/mipmap-xxxhdpi/room_sound_effects.webp
deleted file mode 100644
index 251c076b..00000000
Binary files a/moduleUtil/src/main/res/mipmap-xxxhdpi/room_sound_effects.webp and /dev/null differ
diff --git a/modulecircle/src/main/java/com/example/modulecircle/activity/DynamicDetailActivity.java b/modulecircle/src/main/java/com/example/modulecircle/activity/DynamicDetailActivity.java
index ba52272e..d77f686e 100644
--- a/modulecircle/src/main/java/com/example/modulecircle/activity/DynamicDetailActivity.java
+++ b/modulecircle/src/main/java/com/example/modulecircle/activity/DynamicDetailActivity.java
@@ -129,7 +129,7 @@ public class DynamicDetailActivity extends BaseMvpActivity implem
String url = OSSOperUtils.getPath(file, type);
- CosUploadManager.getInstance().upParameters(url,file.getPath(), new CosUploadManager.UploadCallback() {
+ CosUploadManager.getInstance(CommonAppContext.getInstance()).upParameters(url,file.getPath(), new CosUploadManager.UploadCallback() {
@Override
public void onSuccess(String url) {
if (MvpRef==null){
@@ -67,6 +68,13 @@ public class ReleasePresenter extends BasePresenter implem
public void onFailure(Exception e) {
ToastUtils.show("上传失败");
}
+
+ @Override
+ public void onFailure1(IllegalStateException e) {
+ ToastUtils.show("上传失败");
+ }
+
+
});
// OSSOperUtils.newInstance().putObjectMethod(url, file.getPath(), new OSSOperUtils.OssCallback() {
// @Override
diff --git a/modulemain/src/main/AndroidManifest.xml b/modulemain/src/main/AndroidManifest.xml
index d0d91f6c..3c40bf31 100644
--- a/modulemain/src/main/AndroidManifest.xml
+++ b/modulemain/src/main/AndroidManifest.xml
@@ -9,7 +9,6 @@
diff --git a/modulemain/src/main/java/com/xscm/modulemain/activity/MainActivity.java b/modulemain/src/main/java/com/xscm/modulemain/activity/MainActivity.java
index bd6b03ae..48e40c86 100644
--- a/modulemain/src/main/java/com/xscm/modulemain/activity/MainActivity.java
+++ b/modulemain/src/main/java/com/xscm/modulemain/activity/MainActivity.java
@@ -379,9 +379,10 @@ public class MainActivity extends BaseMvpActivity
+
+
\ No newline at end of file
diff --git a/moduleroom/src/main/AndroidManifest.xml b/moduleroom/src/main/AndroidManifest.xml
index 6c3fb2fa..f22d5030 100644
--- a/moduleroom/src/main/AndroidManifest.xml
+++ b/moduleroom/src/main/AndroidManifest.xml
@@ -17,9 +17,10 @@
android:name=".activity.RoomActivity"
android:screenOrientation="portrait"
android:launchMode="singleTask"
- android:excludeFromRecents="true"
- android:exported="true"
+ android:excludeFromRecents="false"
+ android:exported="false"
android:windowSoftInputMode="adjustPan"
+ android:alwaysRetainTaskState="true"
android:enableOnBackInvokedCallback="false"
android:theme="@style/TransparentActivityTheme"
android:configChanges="orientation|screenSize|keyboardHidden"
diff --git a/moduleroom/src/main/java/com/example/moduleroom/activity/RoomActivity.kt b/moduleroom/src/main/java/com/example/moduleroom/activity/RoomActivity.kt
index 81b10fd0..4774ec22 100644
--- a/moduleroom/src/main/java/com/example/moduleroom/activity/RoomActivity.kt
+++ b/moduleroom/src/main/java/com/example/moduleroom/activity/RoomActivity.kt
@@ -125,7 +125,8 @@ class RoomActivity : BaseMvpActivity(),
var roomId: String? = null
var mRoomInfoResp: RoomInfoResp? = null
-
+ @JvmField
+ @Autowired
var taskId: String? = null
var likeUserAdapter: LikeUserAdapter? = null
@@ -179,6 +180,7 @@ class RoomActivity : BaseMvpActivity(),
roomId = intent.getStringExtra("roomId")
mRoomInfoResp = intent.getSerializableExtra("roomInfo") as RoomInfoResp?
LogUtils.dTag("RoomActivity", "doDone" + mRoomInfoResp.toString())
+ taskId = intent.getStringExtra("taskId")
}
@@ -482,6 +484,20 @@ class RoomActivity : BaseMvpActivity(),
private val testHandler = Handler()
private var testRunnable: Runnable? = null
+// override fun onNewIntent(intent: Intent?) {
+// super.onNewIntent(intent)
+// setIntent( intent)
+// Log.d("TAG", "=== onNewIntent called ===");
+// if (intent != null) {
+// val roomId = intent.getStringExtra("roomId")
+// redPacketInfo = intent.getSerializableExtra("redPacketInfo") as RedPacketInfo?
+// }
+// }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ Log.d("TAG", "=== onDestroy called ===");
+ }
override fun onCreate(savedInstanceState: Bundle?) {
// 在super.onCreate之前设置主题以避免闪白屏
@@ -489,7 +505,6 @@ class RoomActivity : BaseMvpActivity(),
super.onCreate(savedInstanceState)
// // 进入房间10s后检查是否显示提示上麦对话框
LogUtils.e("RoomActivity", "onCreate")
-
isSave = false
sDestroied = false
isMinimized = false
@@ -862,6 +877,10 @@ class RoomActivity : BaseMvpActivity(),
val fragment = HourlyChartDialog.newInstance()
fragment.show(supportFragmentManager, "HourlyChartDialog")
}
+ mBinding!!.clDayTask.visibility = View.VISIBLE
+ mBinding!!.imDayTask.setOnClickListener {
+ ARouter.getInstance().build(ARouteConstants.DailyTasksActivity).navigation()
+ }
mBinding!!.drvRed.visibility = View.GONE
mBinding!!.redBj.setOnClickListener {
@@ -3508,8 +3527,7 @@ class RoomActivity : BaseMvpActivity(),
mBinding!!.ivQuanC.setOnClickListener { v: View? ->
if (mRoomInfoResp!!.room_info.head_line.room_id != null && mRoomInfoResp!!.room_info.head_line.room_id.isNotEmpty()) {
if (mRoomInfoResp!!.room_info.head_line.room_id != roomId) {
- RoomManager.getInstance()
- .fetchRoomDataAndEnter(applicationContext, mRoomInfoResp!!.room_info.head_line.room_id, "")
+ RoomManager.getInstance().fetchRoomDataAndEnter(applicationContext, mRoomInfoResp!!.room_info.head_line.room_id, "",null)
} else {
com.blankj.utilcode.util.ToastUtils.showLong("您就在当前房间")
}
diff --git a/moduleroom/src/main/java/com/example/moduleroom/adapter/EaseChatAdapter.java b/moduleroom/src/main/java/com/example/moduleroom/adapter/EaseChatAdapter.java
index 5ed426df..e947f94c 100644
--- a/moduleroom/src/main/java/com/example/moduleroom/adapter/EaseChatAdapter.java
+++ b/moduleroom/src/main/java/com/example/moduleroom/adapter/EaseChatAdapter.java
@@ -75,7 +75,7 @@ public class EaseChatAdapter extends BaseMultiItemQuickAdapter images1 = emMessage.getText().getFromUserInfo().getIcon();
// LinearLayout ll_images1 = helper.getView(com.xscm.moduleutil.R.id.line);
// ll_images1.removeAllViews();
diff --git a/moduleroom/src/main/java/com/example/moduleroom/dialog/HourlyChartDialog.java b/moduleroom/src/main/java/com/example/moduleroom/dialog/HourlyChartDialog.java
index 30c80ec0..29996445 100644
--- a/moduleroom/src/main/java/com/example/moduleroom/dialog/HourlyChartDialog.java
+++ b/moduleroom/src/main/java/com/example/moduleroom/dialog/HourlyChartDialog.java
@@ -131,7 +131,7 @@ public class HourlyChartDialog extends BaseMvpDialogFragment {
- upDtaView(true);
- });
- Observable.timer(500, TimeUnit.MILLISECONDS)
+// Observable.timer(20, TimeUnit.MILLISECONDS)
+// .observeOn(AndroidSchedulers.mainThread())
+// .subscribe(aLong -> {
+// upDtaView(true);
+// });
+ Observable.timer(10, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(aLong -> {
upDtaView(false);
@@ -165,7 +166,7 @@ public class SingSongFragment extends BaseRoomFragment
+
+
+
+
+
+
+
+
diff --git a/modulevocal/src/main/java/com/example/modulevocal/activity/DailyTasksActivity.java b/modulevocal/src/main/java/com/example/modulevocal/activity/DailyTasksActivity.java
index d31c3d6b..394b7a3a 100644
--- a/modulevocal/src/main/java/com/example/modulevocal/activity/DailyTasksActivity.java
+++ b/modulevocal/src/main/java/com/example/modulevocal/activity/DailyTasksActivity.java
@@ -4,9 +4,11 @@ package com.example.modulevocal.activity;
import android.content.Intent;
import android.graphics.Color;
+import android.view.View;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
+import com.alibaba.android.arouter.facade.annotation.Route;
import com.alibaba.android.arouter.launcher.ARouter;
import com.example.modulevocal.R;
import com.example.modulevocal.adapter.TaskBoxAdapter;
@@ -19,6 +21,7 @@ import com.hjq.toast.ToastUtils;
import com.xscm.moduleutil.activity.BaseMvpActivity;
import com.xscm.moduleutil.activity.WebViewActivity;
import com.xscm.moduleutil.base.CommonAppContext;
+import com.xscm.moduleutil.base.RoomManager;
import com.xscm.moduleutil.bean.GiftBoxBean;
import com.xscm.moduleutil.bean.GiftName;
import com.xscm.moduleutil.bean.TaskItem;
@@ -33,6 +36,7 @@ import java.util.List;
* @data 2025/5/27
* @description: 每日任务
*/
+@Route(path = ARouteConstants.DailyTasksActivity)
public class DailyTasksActivity extends BaseMvpActivity implements DailyTasksConacts.View {
private TaskBoxAdapter mTaskBoxAdapter;
@@ -142,11 +146,16 @@ public class DailyTasksActivity extends BaseMvpActivity {
- RoomManager.getInstance().fetchRoomDataAndEnter(getApplicationContext(), CommonAppContext.getInstance().playId, "");
+ isShowLoading(true);
+ RoomManager.getInstance().fetchRoomDataAndEnter(getApplicationContext(), CommonAppContext.getInstance().playId, "",null);
});
mBinding.ivGuanbi.setOnClickListener(v -> {
mBinding.ll.setVisibility(View.INVISIBLE);
@@ -202,6 +203,20 @@ public class MyRoomActivity extends BaseMvpActivity items) {
+ private MyRoomActivity myRoomActivity;
+
+ // public void submitList(List items) {
// viewItems.clear();
// viewItems.addAll(items);
// notifyDataSetChanged();
// }
+ public void setMyRoomActivity(MyRoomActivity myRoomActivity) {
+ this.myRoomActivity = myRoomActivity;
+ }
public void submitList(List items) {
if (items == null || items.isEmpty()) {
@@ -217,7 +224,7 @@ public class MyCreateAdapter extends RecyclerView.Adapter
public void uploadFile(File file, int type, int index, int size) {
MvpRef.get().showLoadings("上传中...");
String url = OSSOperUtils.getPath(file, type);
- CosUploadManager.getInstance().upParameters(url,file.getPath(), new CosUploadManager.UploadCallback() {
+ CosUploadManager.getInstance(CommonAppContext.getInstance()).upParameters(url,file.getPath(), new CosUploadManager.UploadCallback() {
@Override
public void onSuccess(String url) {
if (isViewAttach()) {
@@ -64,6 +65,12 @@ public class AlbumDetailPresenter extends BasePresenter
ToastUtils.show("上传失败");
MvpRef.get().disLoadings();
}
+
+ @Override
+ public void onFailure1(IllegalStateException e) {
+ ToastUtils.show("上传失败");
+ MvpRef.get().disLoadings();
+ }
});
// OSSOperUtils.newInstance().putObjectMethod(url, file.getPath(), new OSSOperUtils.OssCallback() {
diff --git a/modulevocal/src/main/java/com/example/modulevocal/presenter/CreatedRoomPresenter.java b/modulevocal/src/main/java/com/example/modulevocal/presenter/CreatedRoomPresenter.java
index 9a737660..433d49e1 100644
--- a/modulevocal/src/main/java/com/example/modulevocal/presenter/CreatedRoomPresenter.java
+++ b/modulevocal/src/main/java/com/example/modulevocal/presenter/CreatedRoomPresenter.java
@@ -4,6 +4,7 @@ import android.content.Context;
import com.example.modulevocal.conacts.CreatedRoomConactos;
import com.hjq.toast.ToastUtils;
+import com.xscm.moduleutil.base.CommonAppContext;
import com.xscm.moduleutil.http.BaseObserver;
import com.xscm.moduleutil.presenter.BasePresenter;
import com.xscm.moduleutil.utils.cos.CosUploadManager;
@@ -81,7 +82,7 @@ public class CreatedRoomPresenter extends BasePresenter impl
public void uploadFile(File file, int type, int index, int size) {
MvpRef.get().showLoadings("上传中...");
String url = OSSOperUtils.getPath(file, type);
- CosUploadManager.getInstance().upParameters(url,file.getPath(), new CosUploadManager.UploadCallback() {
+ CosUploadManager.getInstance(CommonAppContext.getInstance()).upParameters(url,file.getPath(), new CosUploadManager.UploadCallback() {
@Override
public void onSuccess(String url) {
if (isViewAttach()) {
@@ -41,7 +42,13 @@ public class EditUserPresenter extends BasePresenter impl
@Override
public void onFailure(Exception e) {
- ToastUtils.show("上传失败");
+ ToastUtils.show("上传失败",e);
+ MvpRef.get().disLoadings();
+ }
+
+ @Override
+ public void onFailure1(IllegalStateException e) {
+ ToastUtils.show("上传失败",e);
MvpRef.get().disLoadings();
}
});
diff --git a/modulevocal/src/main/java/com/example/modulevocal/presenter/MyAlbumPresenter.java b/modulevocal/src/main/java/com/example/modulevocal/presenter/MyAlbumPresenter.java
index 19714b18..3af59501 100644
--- a/modulevocal/src/main/java/com/example/modulevocal/presenter/MyAlbumPresenter.java
+++ b/modulevocal/src/main/java/com/example/modulevocal/presenter/MyAlbumPresenter.java
@@ -4,6 +4,7 @@ import android.content.Context;
import com.example.modulevocal.conacts.MyAlbumConacts;
import com.hjq.toast.ToastUtils;
+import com.xscm.moduleutil.base.CommonAppContext;
import com.xscm.moduleutil.bean.AlbumBean;
import com.xscm.moduleutil.http.BaseObserver;
import com.xscm.moduleutil.presenter.BasePresenter;
@@ -55,7 +56,7 @@ public class MyAlbumPresenter extends BasePresenter impleme
public void uploadFile(File file, int type) {
MvpRef.get().showLoadings("上传中...");
String url = OSSOperUtils.getPath(file, type);
- CosUploadManager.getInstance().upParameters(url,file.getPath(), new CosUploadManager.UploadCallback() {
+ CosUploadManager.getInstance(CommonAppContext.getInstance()).upParameters(url,file.getPath(), new CosUploadManager.UploadCallback() {
@Override
public void onSuccess(String url) {
if (isViewAttach()) {
@@ -70,6 +71,12 @@ public class MyAlbumPresenter extends BasePresenter impleme
ToastUtils.show("上传失败");
MvpRef.get().disLoadings();
}
+
+ @Override
+ public void onFailure1(IllegalStateException e) {
+ ToastUtils.show("上传失败");
+ MvpRef.get().disLoadings();
+ }
});
// OSSOperUtils.newInstance().putObjectMethod(url, file.getPath(), new OSSOperUtils.OssCallback() {
// @Override
diff --git a/modulevocal/src/main/res/layout/activity_daily_tasks.xml b/modulevocal/src/main/res/layout/activity_daily_tasks.xml
index 15d18237..9d5e9e10 100644
--- a/modulevocal/src/main/res/layout/activity_daily_tasks.xml
+++ b/modulevocal/src/main/res/layout/activity_daily_tasks.xml
@@ -10,7 +10,7 @@
-
+
diff --git a/modulevocal/src/main/res/layout/room_activity_my_room.xml b/modulevocal/src/main/res/layout/room_activity_my_room.xml
index 89353f9d..cd78b4df 100644
--- a/modulevocal/src/main/res/layout/room_activity_my_room.xml
+++ b/modulevocal/src/main/res/layout/room_activity_my_room.xml
@@ -242,5 +242,18 @@
android:layout_marginRight="@dimen/dp_14"
android:src="@mipmap/icon_guanbi" />
+
+
\ No newline at end of file
diff --git a/modulevoice/src/main/java/com/example/modulevoice/activity/PopularRoomActivity.java b/modulevoice/src/main/java/com/example/modulevoice/activity/PopularRoomActivity.java
index 44a8643d..159df31b 100644
--- a/modulevoice/src/main/java/com/example/modulevoice/activity/PopularRoomActivity.java
+++ b/modulevoice/src/main/java/com/example/modulevoice/activity/PopularRoomActivity.java
@@ -94,14 +94,21 @@ public class PopularRoomActivity extends BaseMvpActivity= mAdapter.getData().size()) {
return;
@@ -134,7 +139,7 @@ public class HotListFragment extends BaseMvpFragment info;
CarouselBannerAdapter carouselBannerAdapter;
- public static VoiceCategoryFragment newInstance() {
- return new VoiceCategoryFragment();
+ private VoiceFragment voiceFragment;
+
+ private VoiceCategoryFragment (){}
+
+
+ private VoiceCategoryFragment (VoiceFragment voiceFragment){
+ this.voiceFragment = voiceFragment;
+ }
+
+ public static VoiceCategoryFragment newInstance(VoiceFragment voiceFragment) {
+ return new VoiceCategoryFragment(voiceFragment);
+ }
+
+ public void showLoading(){
+ voiceFragment.isShowLoading(true);
}
@Nullable
@@ -156,7 +169,8 @@ public class VoiceCategoryFragment extends BaseMvpFragment {
// 示例:跳转到房间 详情页
if (data != null) {
- RoomManager.getInstance().fetchRoomDataAndEnter(getActivity(), data.getRoom_id() ,"");
+ showLoading();
+ RoomManager.getInstance().fetchRoomDataAndEnter(getActivity(), data.getRoom_id() ,"",null);
}
});
@@ -178,7 +192,8 @@ public class VoiceCategoryFragment extends BaseMvpFragment list;
@@ -408,7 +423,7 @@ public class VoiceCategoryFragment extends BaseMvpFragment fragments = new ArrayList<>();
- fragments.add(VoiceCategoryFragment.newInstance());
+ fragments.add(VoiceCategoryFragment.newInstance(VoiceFragment.this));
mBinding.viewPager.setAdapter(new MyFragmentPagerAdapter(fragments, getChildFragmentManager()));
}
diff --git a/modulevoice/src/main/res/layout/activity_popular_room.xml b/modulevoice/src/main/res/layout/activity_popular_room.xml
index 33304ae1..c6149403 100644
--- a/modulevoice/src/main/res/layout/activity_popular_room.xml
+++ b/modulevoice/src/main/res/layout/activity_popular_room.xml
@@ -42,5 +42,18 @@
android:paddingBottom="@dimen/dp_80"
tools:listitem="@layout/item_popular_room"/>
+
+
\ No newline at end of file
diff --git a/modulevoice/src/main/res/layout/activity_ranking_list.xml b/modulevoice/src/main/res/layout/activity_ranking_list.xml
index 9f18967a..bdcd0714 100644
--- a/modulevoice/src/main/res/layout/activity_ranking_list.xml
+++ b/modulevoice/src/main/res/layout/activity_ranking_list.xml
@@ -29,6 +29,7 @@
android:layout_marginRight="@dimen/dp_10"
app:tabIndicatorHeight="10dp"
app:tabIndicator="@mipmap/tab_x"
+ app:tabIndicatorGravity="bottom"
app:tabTextColor="@color/white"
app:tabSelectedTextColor="@color/white"
app:tabTextAppearance="@style/CustomTabTextAppearance"
diff --git a/modulevoice/src/main/res/layout/fragment_voice.xml b/modulevoice/src/main/res/layout/fragment_voice.xml
index 9ae30933..06c9e780 100644
--- a/modulevoice/src/main/res/layout/fragment_voice.xml
+++ b/modulevoice/src/main/res/layout/fragment_voice.xml
@@ -7,104 +7,119 @@
-
+ android:layout_height="match_parent">
-
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:background="@mipmap/home_bj">
-
-
-
-
-
+ android:gravity="center_vertical">
+
+
-
+ android:orientation="horizontal"
+ android:visibility="gone">
-
+
-
+
+
-
+
+
+
+
+
-
+
-
+
-
+
-
+
+
+
+
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index 97e128ee..7999babb 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -61,4 +61,5 @@ include ':tuichat'
include ':tuiconversation'
include ':tuicore'
include ':moduleroom'
+include ':Loadinglibrary'