diff --git a/app/build.gradle b/app/build.gradle index eaabe38d4..63041b029 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -78,7 +78,7 @@ allprojects { } dependencies { implementation project(':autoimageslider') - implementation 'androidx.appcompat:appcompat:1.5.1' + implementation 'androidx.appcompat:appcompat:1.6.0' implementation 'com.google.android.material:material:1.7.0' @@ -94,7 +94,6 @@ dependencies { implementation "org.conscrypt:conscrypt-android:2.5.2" implementation 'com.vanniktech:emoji-one:0.6.0' implementation 'com.github.GrenderG:Toasty:1.5.2' - implementation 'org.framagit.tom79:SparkButton:1.0.13' implementation "com.github.bumptech.glide:glide:4.14.2" implementation "com.github.bumptech.glide:okhttp3-integration:4.14.2" implementation("com.github.bumptech.glide:recyclerview-integration:4.14.2") { @@ -107,7 +106,7 @@ dependencies { implementation 'com.github.mergehez:ArgPlayer:v3.1' implementation project(path: ':mytransl') implementation project(path: ':ratethisapp') - + implementation project(path: ':sparkbutton') implementation 'com.burhanrashid52:photoeditor:1.5.1' implementation("com.vanniktech:android-image-cropper:4.3.3") diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/StatusAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/StatusAdapter.java index ca686c881..450473b35 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/StatusAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/StatusAdapter.java @@ -638,21 +638,13 @@ public class StatusAdapter extends RecyclerView.Adapter } - holder.binding.actionButtonFavorite.pressOnTouch(false); - holder.binding.actionButtonBoost.pressOnTouch(false); - holder.binding.actionButtonBookmark.pressOnTouch(false); + holder.binding.actionButtonFavorite.setActiveImage(R.drawable.ic_round_star_24); holder.binding.actionButtonFavorite.setInactiveImage(R.drawable.ic_round_star_border_24); holder.binding.actionButtonBookmark.setActiveImage(R.drawable.ic_round_bookmark_24); holder.binding.actionButtonBookmark.setInactiveImage(R.drawable.ic_round_bookmark_border_24); holder.binding.actionButtonBoost.setActiveImage(R.drawable.ic_round_repeat_24); holder.binding.actionButtonBoost.setInactiveImage(R.drawable.ic_round_repeat_24); - holder.binding.actionButtonFavorite.setDisableCircle(true); - holder.binding.actionButtonBoost.setDisableCircle(true); - holder.binding.actionButtonBookmark.setDisableCircle(true); - holder.binding.actionButtonFavorite.setActiveImageTint(R.color.marked_icon); - holder.binding.actionButtonBoost.setActiveImageTint(R.color.boost_icon); - holder.binding.actionButtonBookmark.setActiveImageTint(R.color.marked_icon); applyColor(context, holder); if (status.pinned) { @@ -2319,9 +2311,6 @@ public class StatusAdapter extends RecyclerView.Adapter Helper.changeDrawableColor(context, R.drawable.ic_bot, theme_icons_color); Helper.changeDrawableColor(context, R.drawable.ic_round_reply_24, theme_icons_color); Helper.changeDrawableColor(context, holder.binding.actionButtonTranslate, theme_icons_color); - holder.binding.actionButtonFavorite.setInActiveImageTintColor(theme_icons_color); - holder.binding.actionButtonBookmark.setInActiveImageTintColor(theme_icons_color); - holder.binding.actionButtonBoost.setInActiveImageTintColor(theme_icons_color); holder.binding.replyCount.setTextColor(theme_icons_color); } if (theme_statuses_color != -1) { diff --git a/app/src/main/res/drawable/ic_round_bookmark_24.xml b/app/src/main/res/drawable/ic_round_bookmark_24.xml index fdc2c64d4..f8e4675ec 100644 --- a/app/src/main/res/drawable/ic_round_bookmark_24.xml +++ b/app/src/main/res/drawable/ic_round_bookmark_24.xml @@ -1,7 +1,7 @@ + + diff --git a/app/src/main/res/drawable/ic_round_star_24.xml b/app/src/main/res/drawable/ic_round_star_24.xml index 3421678ad..335e3bd84 100644 --- a/app/src/main/res/drawable/ic_round_star_24.xml +++ b/app/src/main/res/drawable/ic_round_star_24.xml @@ -1,7 +1,7 @@ + app:activeImage="@drawable/ic_round_repeat_active_24" + app:iconSize="28dp" + app:inactiveImage="@drawable/ic_round_repeat_24" + app:primaryColor="@color/boost_icon" + app:secondaryColor="@color/boost_icon" /> + app:activeImage="@drawable/ic_round_star_24" + app:animationSpeed="1.5" + app:inactiveImage="@drawable/ic_round_star_border_24" + app:primaryColor="@color/marked_icon" + app:secondaryColor="@color/marked_icon" + sparkbutton:iconSize="28dp" /> + app:activeImage="@drawable/ic_round_bookmark_24" + app:animationSpeed="1.5" + app:inactiveImage="@drawable/ic_round_bookmark_border_24" + app:primaryColor="@color/marked_icon" + app:secondaryColor="@color/marked_icon" + sparkbutton:iconSize="28dp" /> + uploadArchives { + repositories { + mavenDeployer { + beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } + + pom.groupId = GROUP + pom.artifactId = POM_ARTIFACT_ID + pom.version = VERSION_NAME + + repository(url: getReleaseRepositoryUrl()) { + authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) + } + snapshotRepository(url: getSnapshotRepositoryUrl()) { + authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) + } + + pom.project { + name POM_NAME + packaging POM_PACKAGING + description POM_DESCRIPTION + url POM_URL + + scm { + url POM_SCM_URL + connection POM_SCM_CONNECTION + developerConnection POM_SCM_DEV_CONNECTION + } + + licenses { + license { + name POM_LICENCE_NAME + url POM_LICENCE_URL + distribution POM_LICENCE_DIST + } + } + + developers { + developer { + id POM_DEVELOPER_ID + name POM_DEVELOPER_NAME + } + } + } + } + } + } + + signing { + required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") } + sign configurations.archives + } + + //task androidJavadocs(type: Javadoc) { + //source = android.sourceSets.main.allJava + //} + + //task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { + //classifier = 'javadoc' + //from androidJavadocs.destinationDir + //} + + task androidSourcesJar(type: Jar) { + classifier = 'sources' + from android.sourceSets.main.java.sourceFiles + } + + artifacts { + archives androidSourcesJar + } +} \ No newline at end of file diff --git a/sparkbutton/proguard-rules.pro b/sparkbutton/proguard-rules.pro new file mode 100644 index 000000000..5407790ba --- /dev/null +++ b/sparkbutton/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 /android-sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/sparkbutton/src/main/AndroidManifest.xml b/sparkbutton/src/main/AndroidManifest.xml new file mode 100644 index 000000000..1f76ffe6a --- /dev/null +++ b/sparkbutton/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/sparkbutton/src/main/java/com/varunest/sparkbutton/SparkButton.java b/sparkbutton/src/main/java/com/varunest/sparkbutton/SparkButton.java new file mode 100644 index 000000000..19787c5b8 --- /dev/null +++ b/sparkbutton/src/main/java/com/varunest/sparkbutton/SparkButton.java @@ -0,0 +1,313 @@ +package com.varunest.sparkbutton; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Build; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.OvershootInterpolator; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import androidx.annotation.ColorInt; +import androidx.annotation.DrawableRes; +import androidx.annotation.Px; +import androidx.appcompat.widget.AppCompatImageView; +import androidx.core.content.ContextCompat; + +import com.varunest.sparkbutton.helpers.SparkAnimationView; +import com.varunest.sparkbutton.helpers.Utils; + +/** + * @author varun 7th July 2016 + */ +public class SparkButton extends FrameLayout implements View.OnClickListener { + private static final DecelerateInterpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator(); + private static final AccelerateDecelerateInterpolator ACCELERATE_DECELERATE_INTERPOLATOR = new AccelerateDecelerateInterpolator(); + private static final OvershootInterpolator OVERSHOOT_INTERPOLATOR = new OvershootInterpolator(4); + + private static final int INVALID_RESOURCE_ID = -1; + private static final float ANIMATIONVIEW_SIZE_FACTOR = 3; + private static final float DOTS_SIZE_FACTOR = .08f; + int activeImageTint; + int inActiveImageTint; + private @DrawableRes + int imageResourceIdActive = INVALID_RESOURCE_ID; + private @DrawableRes + int imageResourceIdInactive = INVALID_RESOURCE_ID; + private @Px + int imageSize; + private @ColorInt + int primaryColor; + private @ColorInt + int secondaryColor; + private SparkAnimationView sparkAnimationView; + private ImageView imageView; + private float animationSpeed = 1; + private boolean isChecked = false; + private AnimatorSet animatorSet; + private SparkEventListener listener; + + SparkButton(Context context) { + super(context); + } + + public SparkButton(Context context, AttributeSet attrs) { + super(context, attrs); + initFromXML(attrs); + init(); + } + + public SparkButton(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initFromXML(attrs); + init(); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public SparkButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + initFromXML(attrs); + init(); + } + + + void init() { + int animationViewSize = (int) (imageSize * ANIMATIONVIEW_SIZE_FACTOR); + + sparkAnimationView = new SparkAnimationView(getContext()); + LayoutParams dotsViewLayoutParams = new LayoutParams(animationViewSize, animationViewSize, Gravity.CENTER); + sparkAnimationView.setLayoutParams(dotsViewLayoutParams); + + sparkAnimationView.setColors(secondaryColor, primaryColor); + sparkAnimationView.setMaxDotSize((int) (imageSize * DOTS_SIZE_FACTOR)); + + addView(sparkAnimationView); + + imageView = new AppCompatImageView(getContext()); + LayoutParams imageViewLayoutParams = new LayoutParams(imageSize, imageSize, Gravity.CENTER); + imageView.setLayoutParams(imageViewLayoutParams); + + addView(imageView); + + if (imageResourceIdInactive != INVALID_RESOURCE_ID) { + // should load inactive img first + imageView.setImageResource(imageResourceIdInactive); + } else if (imageResourceIdActive != INVALID_RESOURCE_ID) { + imageView.setImageResource(imageResourceIdActive); + } else { + throw new IllegalArgumentException("One of Inactive/Active Image Resources is required!"); + } + setOnTouchListener(); + setOnClickListener(this); + } + + /** + * Call this function to start spark animation + */ + public void playAnimation() { + if (animatorSet != null) { + animatorSet.cancel(); + } + + imageView.animate().cancel(); + imageView.setScaleX(0); + imageView.setScaleY(0); + sparkAnimationView.setInnerCircleRadiusProgress(0); + sparkAnimationView.setOuterCircleRadiusProgress(0); + sparkAnimationView.setCurrentProgress(0); + + animatorSet = new AnimatorSet(); + + ObjectAnimator outerCircleAnimator = ObjectAnimator.ofFloat(sparkAnimationView, SparkAnimationView.OUTER_CIRCLE_RADIUS_PROGRESS, 0.1f, 1f); + outerCircleAnimator.setDuration((long) (250 / animationSpeed)); + outerCircleAnimator.setInterpolator(DECELERATE_INTERPOLATOR); + + ObjectAnimator innerCircleAnimator = ObjectAnimator.ofFloat(sparkAnimationView, SparkAnimationView.INNER_CIRCLE_RADIUS_PROGRESS, 0.1f, 1f); + innerCircleAnimator.setDuration((long) (200 / animationSpeed)); + innerCircleAnimator.setStartDelay((long) (200 / animationSpeed)); + innerCircleAnimator.setInterpolator(DECELERATE_INTERPOLATOR); + + ObjectAnimator starScaleYAnimator = ObjectAnimator.ofFloat(imageView, ImageView.SCALE_Y, 0.2f, 1f); + starScaleYAnimator.setDuration((long) (350 / animationSpeed)); + starScaleYAnimator.setStartDelay((long) (250 / animationSpeed)); + starScaleYAnimator.setInterpolator(OVERSHOOT_INTERPOLATOR); + + ObjectAnimator starScaleXAnimator = ObjectAnimator.ofFloat(imageView, ImageView.SCALE_X, 0.2f, 1f); + starScaleXAnimator.setDuration((long) (350 / animationSpeed)); + starScaleXAnimator.setStartDelay((long) (250 / animationSpeed)); + starScaleXAnimator.setInterpolator(OVERSHOOT_INTERPOLATOR); + + ObjectAnimator dotsAnimator = ObjectAnimator.ofFloat(sparkAnimationView, SparkAnimationView.DOTS_PROGRESS, 0, 1f); + dotsAnimator.setDuration((long) (900 / animationSpeed)); + dotsAnimator.setStartDelay((long) (50 / animationSpeed)); + dotsAnimator.setInterpolator(ACCELERATE_DECELERATE_INTERPOLATOR); + + animatorSet.playTogether( + outerCircleAnimator, + innerCircleAnimator, + starScaleYAnimator, + starScaleXAnimator, + dotsAnimator + ); + + animatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + sparkAnimationView.setInnerCircleRadiusProgress(0); + sparkAnimationView.setOuterCircleRadiusProgress(0); + sparkAnimationView.setCurrentProgress(0); + imageView.setScaleX(1); + imageView.setScaleY(1); + } + + @Override + public void onAnimationEnd(Animator animation) { + } + + @Override + public void onAnimationStart(Animator animation) { + } + }); + + animatorSet.start(); + } + + + public @Px + int getImageSize() { + return imageSize; + } + + public void setImageSize(@Px int imageSize) { + this.imageSize = imageSize; + } + + public @ColorInt + int getPrimaryColor() { + return primaryColor; + } + + public void setPrimaryColor(@ColorInt int primaryColor) { + this.primaryColor = primaryColor; + } + + public @ColorInt + int getSecondaryColor() { + return secondaryColor; + } + + public void setSecondaryColor(@ColorInt int secondaryColor) { + this.secondaryColor = secondaryColor; + } + + public void setAnimationSpeed(float animationSpeed) { + this.animationSpeed = animationSpeed; + } + + /** + * @return Returns whether the button is checked (Active) or not. + */ + public boolean isChecked() { + return isChecked; + } + + /** + * Change Button State (Works only if both active and disabled image resource is defined) + * + * @param flag desired checked state of the button + */ + public void setChecked(boolean flag) { + isChecked = flag; + imageView.setImageResource(isChecked ? imageResourceIdActive : imageResourceIdInactive); + } + + public void setInactiveImage(int inactiveResource) { + this.imageResourceIdInactive = inactiveResource; + imageView.setImageResource(isChecked ? imageResourceIdActive : imageResourceIdInactive); + } + + public void setActiveImage(int activeResource) { + this.imageResourceIdActive = activeResource; + imageView.setImageResource(isChecked ? imageResourceIdActive : imageResourceIdInactive); + } + + @Override + public void onClick(View v) { + boolean shouldPlayAnimation = listener == null || listener.onEvent(this, isChecked); + + if (shouldPlayAnimation) { + if (imageResourceIdInactive != INVALID_RESOURCE_ID) { + isChecked = !isChecked; + + imageView.setImageResource(isChecked ? imageResourceIdActive : imageResourceIdInactive); + + if (animatorSet != null) { + animatorSet.cancel(); + } + if (isChecked) { + sparkAnimationView.setVisibility(VISIBLE); + playAnimation(); + } else { + sparkAnimationView.setVisibility(INVISIBLE); + } + } else { + playAnimation(); + } + } + } + + + private void setOnTouchListener() { + setOnTouchListener((v, event) -> { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + imageView.animate().scaleX(0.8f).scaleY(0.8f).setDuration(150).setInterpolator(DECELERATE_INTERPOLATOR); + setPressed(true); + break; + + case MotionEvent.ACTION_MOVE: + break; + + case MotionEvent.ACTION_UP: + imageView.animate().scaleX(1).scaleY(1).setInterpolator(DECELERATE_INTERPOLATOR); + if (isPressed()) { + performClick(); + setPressed(false); + } + break; + + case MotionEvent.ACTION_CANCEL: + imageView.animate().scaleX(1).scaleY(1).setInterpolator(DECELERATE_INTERPOLATOR); + break; + } + return true; + }); + } + + private int getColor(int id) { + return ContextCompat.getColor(getContext(), id); + } + + + private void initFromXML(AttributeSet attr) { + TypedArray a = getContext().obtainStyledAttributes(attr, R.styleable.SparkButton); + imageSize = a.getDimensionPixelOffset(R.styleable.SparkButton_iconSize, Utils.dpToPx(getContext(), 50)); + imageResourceIdActive = a.getResourceId(R.styleable.SparkButton_activeImage, INVALID_RESOURCE_ID); + imageResourceIdInactive = a.getResourceId(R.styleable.SparkButton_inactiveImage, INVALID_RESOURCE_ID); + primaryColor = ContextCompat.getColor(getContext(), a.getResourceId(R.styleable.SparkButton_primaryColor, R.color.spark_primary_color)); + secondaryColor = ContextCompat.getColor(getContext(), a.getResourceId(R.styleable.SparkButton_secondaryColor, R.color.spark_secondary_color)); + animationSpeed = a.getFloat(R.styleable.SparkButton_animationSpeed, 1); + // recycle typedArray + a.recycle(); + } +} diff --git a/sparkbutton/src/main/java/com/varunest/sparkbutton/SparkButtonBuilder.java b/sparkbutton/src/main/java/com/varunest/sparkbutton/SparkButtonBuilder.java new file mode 100644 index 000000000..f79e492e8 --- /dev/null +++ b/sparkbutton/src/main/java/com/varunest/sparkbutton/SparkButtonBuilder.java @@ -0,0 +1,61 @@ +package com.varunest.sparkbutton; + +import android.content.Context; + +import androidx.annotation.ColorInt; +import androidx.annotation.DrawableRes; + +import com.varunest.sparkbutton.helpers.Utils; + +/** + * @author varun on 07/07/16. + */ +public class SparkButtonBuilder { + private final SparkButton sparkButton; + private final Context context; + + public SparkButtonBuilder(Context context) { + this.context = context; + sparkButton = new SparkButton(context); + } + + public SparkButtonBuilder setActiveImage(@DrawableRes int resourceId) { + sparkButton.setActiveImage(resourceId); + return this; + } + + public SparkButtonBuilder setInactiveImage(@DrawableRes int resourceId) { + sparkButton.setInactiveImage(resourceId); + return this; + } + + public SparkButtonBuilder setPrimaryColor(@ColorInt int color) { + sparkButton.setPrimaryColor(color); + return this; + } + + public SparkButtonBuilder setSecondaryColor(int color) { + sparkButton.setSecondaryColor(color); + return this; + } + + public SparkButtonBuilder setImageSizePx(int px) { + sparkButton.setImageSize(px); + return this; + } + + public SparkButtonBuilder setImageSizeDp(int dp) { + sparkButton.setImageSize(Utils.dpToPx(context, dp)); + return this; + } + + public SparkButtonBuilder setAnimationSpeed(float speed) { + sparkButton.setAnimationSpeed(speed); + return this; + } + + public SparkButton build() { + sparkButton.init(); + return sparkButton; + } +} diff --git a/sparkbutton/src/main/java/com/varunest/sparkbutton/SparkEventListener.java b/sparkbutton/src/main/java/com/varunest/sparkbutton/SparkEventListener.java new file mode 100644 index 000000000..2e02b0891 --- /dev/null +++ b/sparkbutton/src/main/java/com/varunest/sparkbutton/SparkEventListener.java @@ -0,0 +1,10 @@ +package com.varunest.sparkbutton; + +import androidx.annotation.NonNull; + +/** + * @author varun on 07/07/16. + */ +public interface SparkEventListener { + boolean onEvent(@NonNull SparkButton button, boolean buttonState); +} \ No newline at end of file diff --git a/sparkbutton/src/main/java/com/varunest/sparkbutton/helpers/SparkAnimationView.java b/sparkbutton/src/main/java/com/varunest/sparkbutton/helpers/SparkAnimationView.java new file mode 100644 index 000000000..c7c5fb59f --- /dev/null +++ b/sparkbutton/src/main/java/com/varunest/sparkbutton/helpers/SparkAnimationView.java @@ -0,0 +1,249 @@ +package com.varunest.sparkbutton.helpers; + +import android.animation.ArgbEvaluator; +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Property; +import android.view.View; + + +public class SparkAnimationView extends View { + public static final Property INNER_CIRCLE_RADIUS_PROGRESS = + new Property(Float.class, "innerCircleRadiusProgress") { + @Override + public Float get(SparkAnimationView object) { + return object.getInnerCircleRadiusProgress(); + } + + @Override + public void set(SparkAnimationView object, Float value) { + object.setInnerCircleRadiusProgress(value); + } + }; + private static final int DOTS_COUNT = 12; + private static final int OUTER_DOTS_POSITION_ANGLE = 360 / DOTS_COUNT; + private static final ArgbEvaluator argbEvaluator = new ArgbEvaluator(); + public static final Property DOTS_PROGRESS = new Property(Float.class, "dotsProgress") { + @Override + public Float get(SparkAnimationView object) { + return object.getCurrentProgress(); + } + + @Override + public void set(SparkAnimationView object, Float value) { + object.setCurrentProgress(value); + } + }; + public static final Property OUTER_CIRCLE_RADIUS_PROGRESS = + new Property(Float.class, "outerCircleRadiusProgress") { + @Override + public Float get(SparkAnimationView object) { + return object.getOuterCircleRadiusProgress(); + } + + @Override + public void set(SparkAnimationView object, Float value) { + object.setOuterCircleRadiusProgress(value); + } + }; + private final Paint[] dotsPaints = new Paint[4]; + private final Paint circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint maskPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private int primaryColor = 0xFFFFC107; + private int primaryColorDark = 0xFFFF9800; + private int secondaryColor = 0xFFFF5722; + private int secondaryColorDark = 0xFFF44336; + private int centerX; + private int centerY; + private float maxOuterDotsRadius; + private float maxInnerDotsRadius; + private float maxDotSize; + private float currentProgress = 0; + private float currentRadius1 = 0; + private float currentDotSize1 = 0; + private float currentDotSize2 = 0; + private float currentRadius2 = 0; + private float outerCircleRadiusProgress = 0f; + private float innerCircleRadiusProgress = 0f; + private float maxCircleSize; + + public SparkAnimationView(Context context) { + super(context); + init(); + } + + public SparkAnimationView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public SparkAnimationView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public SparkAnimationView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + private void init() { + setLayerType(View.LAYER_TYPE_HARDWARE, null); + + maxDotSize = Utils.dpToPx(getContext(), 4); + for (int i = 0; i < dotsPaints.length; i++) { + dotsPaints[i] = new Paint(Paint.ANTI_ALIAS_FLAG); + } + + maskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + centerX = w / 2; + centerY = h / 2; + maxOuterDotsRadius = w / 2 - maxDotSize * 2; + maxInnerDotsRadius = 0.8f * maxOuterDotsRadius; + maxCircleSize = w / 4.3f; + + } + + @Override + protected void onDraw(Canvas canvas) { + drawOuterDotsFrame(canvas); + drawInnerDotsFrame(canvas); + + canvas.drawCircle(getWidth() / 2, getHeight() / 2, outerCircleRadiusProgress * maxCircleSize, circlePaint); + canvas.drawCircle(getWidth() / 2, getHeight() / 2, innerCircleRadiusProgress * (maxCircleSize + 1), maskPaint); + } + + public void setMaxDotSize(int pxUnits) { + maxDotSize = pxUnits; + } + + private void drawOuterDotsFrame(Canvas canvas) { + for (int i = 0; i < DOTS_COUNT; i++) { + int cX = (int) (centerX + currentRadius1 * Math.cos(i * OUTER_DOTS_POSITION_ANGLE * Math.PI / 180)); + int cY = (int) (centerY + currentRadius1 * Math.sin(i * OUTER_DOTS_POSITION_ANGLE * Math.PI / 180)); + canvas.drawCircle(cX, cY, currentDotSize1, dotsPaints[i % dotsPaints.length]); + } + } + + private void drawInnerDotsFrame(Canvas canvas) { + for (int i = 0; i < DOTS_COUNT; i++) { + int cX = (int) (centerX + currentRadius2 * Math.cos((i * OUTER_DOTS_POSITION_ANGLE - 10) * Math.PI / 180)); + int cY = (int) (centerY + currentRadius2 * Math.sin((i * OUTER_DOTS_POSITION_ANGLE - 10) * Math.PI / 180)); + canvas.drawCircle(cX, cY, currentDotSize2, dotsPaints[(i + 1) % dotsPaints.length]); + } + } + + public float getCurrentProgress() { + return currentProgress; + } + + public void setCurrentProgress(float currentProgress) { + this.currentProgress = currentProgress; + + updateInnerDotsPosition(); + updateOuterDotsPosition(); + updateDotsPaints(); + updateDotsAlpha(); + + postInvalidate(); + } + + private void updateInnerDotsPosition() { + if (currentProgress < 0.3f) { + this.currentRadius2 = (float) Utils.mapValueFromRangeToRange(currentProgress, 0, 0.3f, 0.f, maxInnerDotsRadius); + } else { + this.currentRadius2 = maxInnerDotsRadius; + } + + if (currentProgress < 0.2) { + this.currentDotSize2 = maxDotSize; + } else if (currentProgress < 0.5) { + this.currentDotSize2 = (float) Utils.mapValueFromRangeToRange(currentProgress, 0.2f, 0.5f, maxDotSize, 0.3 * maxDotSize); + } else { + this.currentDotSize2 = (float) Utils.mapValueFromRangeToRange(currentProgress, 0.5f, 1f, maxDotSize * 0.3f, 0); + } + + } + + private void updateOuterDotsPosition() { + if (currentProgress < 0.3f) { + this.currentRadius1 = (float) Utils.mapValueFromRangeToRange(currentProgress, 0.0f, 0.3f, 0, maxOuterDotsRadius * 0.8f); + } else { + this.currentRadius1 = (float) Utils.mapValueFromRangeToRange(currentProgress, 0.3f, 1f, 0.8f * maxOuterDotsRadius, maxOuterDotsRadius); + } + + if (currentProgress < 0.7) { + this.currentDotSize1 = maxDotSize; + } else { + this.currentDotSize1 = (float) Utils.mapValueFromRangeToRange(currentProgress, 0.7f, 1f, maxDotSize, 0); + } + } + + private void updateDotsPaints() { + if (currentProgress < 0.5f) { + float progress = (float) Utils.mapValueFromRangeToRange(currentProgress, 0f, 0.5f, 0, 1f); + dotsPaints[0].setColor((Integer) argbEvaluator.evaluate(progress, primaryColor, primaryColorDark)); + dotsPaints[1].setColor((Integer) argbEvaluator.evaluate(progress, primaryColorDark, secondaryColor)); + dotsPaints[2].setColor((Integer) argbEvaluator.evaluate(progress, secondaryColor, secondaryColorDark)); + dotsPaints[3].setColor((Integer) argbEvaluator.evaluate(progress, secondaryColorDark, primaryColor)); + } else { + float progress = (float) Utils.mapValueFromRangeToRange(currentProgress, 0.5f, 1f, 0, 1f); + dotsPaints[0].setColor((Integer) argbEvaluator.evaluate(progress, primaryColorDark, secondaryColor)); + dotsPaints[1].setColor((Integer) argbEvaluator.evaluate(progress, secondaryColor, secondaryColorDark)); + dotsPaints[2].setColor((Integer) argbEvaluator.evaluate(progress, secondaryColorDark, primaryColor)); + dotsPaints[3].setColor((Integer) argbEvaluator.evaluate(progress, primaryColor, primaryColorDark)); + } + } + + private void updateDotsAlpha() { + float progress = (float) Utils.clamp(currentProgress, 0.6f, 1f); + int alpha = (int) Utils.mapValueFromRangeToRange(progress, 0.6f, 1f, 255, 0); + dotsPaints[0].setAlpha(alpha); + dotsPaints[1].setAlpha(alpha); + dotsPaints[2].setAlpha(alpha); + dotsPaints[3].setAlpha(alpha); + } + + public void setColors(int primaryColor, int secondaryColor) { + this.primaryColor = primaryColor; + this.primaryColorDark = Utils.darkenColor(primaryColor, 1.1f); + this.secondaryColor = secondaryColor; + this.secondaryColorDark = Utils.darkenColor(secondaryColor, 1.1f); + } + + public float getInnerCircleRadiusProgress() { + return innerCircleRadiusProgress; + } + + public void setInnerCircleRadiusProgress(float innerCircleRadiusProgress) { + this.innerCircleRadiusProgress = innerCircleRadiusProgress; + postInvalidate(); + } + + private void updateCircleColor() { + float colorProgress = (float) Utils.clamp(outerCircleRadiusProgress, 0.5, 1); + colorProgress = (float) Utils.mapValueFromRangeToRange(colorProgress, 0.5f, 1f, 0f, 1f); + this.circlePaint.setColor((Integer) argbEvaluator.evaluate(colorProgress, primaryColor, secondaryColor)); + } + + public float getOuterCircleRadiusProgress() { + return outerCircleRadiusProgress; + } + + public void setOuterCircleRadiusProgress(float outerCircleRadiusProgress) { + this.outerCircleRadiusProgress = outerCircleRadiusProgress; + updateCircleColor(); + postInvalidate(); + } +} \ No newline at end of file diff --git a/sparkbutton/src/main/java/com/varunest/sparkbutton/helpers/Utils.java b/sparkbutton/src/main/java/com/varunest/sparkbutton/helpers/Utils.java new file mode 100644 index 000000000..6a42dc42e --- /dev/null +++ b/sparkbutton/src/main/java/com/varunest/sparkbutton/helpers/Utils.java @@ -0,0 +1,30 @@ +package com.varunest.sparkbutton.helpers; + +import android.content.Context; +import android.graphics.Color; +import android.util.DisplayMetrics; + +import androidx.annotation.NonNull; + +public class Utils { + public static double mapValueFromRangeToRange(double value, double fromLow, double fromHigh, double toLow, double toHigh) { + return toLow + ((value - fromLow) / (fromHigh - fromLow) * (toHigh - toLow)); + } + + public static double clamp(double value, double low, double high) { + return Math.min(Math.max(value, low), high); + } + + public static int darkenColor(int color, float multiplier) { + float[] hsv = new float[3]; + + Color.colorToHSV(color, hsv); + hsv[2] *= multiplier; // value component + return Color.HSVToColor(hsv); + } + + public static int dpToPx(@NonNull Context context, int dp) { + DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); + return Math.round(dp * (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT)); + } +} \ No newline at end of file diff --git a/sparkbutton/src/main/res/values/attrs.xml b/sparkbutton/src/main/res/values/attrs.xml new file mode 100644 index 000000000..aeb5de3cc --- /dev/null +++ b/sparkbutton/src/main/res/values/attrs.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/sparkbutton/src/main/res/values/colors.xml b/sparkbutton/src/main/res/values/colors.xml new file mode 100644 index 000000000..fd9674609 --- /dev/null +++ b/sparkbutton/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #FFFFC107 + #FFFF5722 + #00000000 + \ No newline at end of file diff --git a/sparkbutton/src/main/res/values/strings.xml b/sparkbutton/src/main/res/values/strings.xml new file mode 100644 index 000000000..7bf1ce977 --- /dev/null +++ b/sparkbutton/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + SparkButton +