mirror of https://codeberg.org/tom79/Fedilab
Fix issue #745 - Update library for bottom buttons.
parent
d70c285bea
commit
107ac13e15
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="@color/boost_icon"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:pathData="M7,7h10v1.79c0,0.45 0.54,0.67 0.85,0.35l2.79,-2.79c0.2,-0.2 0.2,-0.51 0,-0.71l-2.79,-2.79c-0.31,-0.31 -0.85,-0.09 -0.85,0.36L17,5L6,5c-0.55,0 -1,0.45 -1,1v4c0,0.55 0.45,1 1,1s1,-0.45 1,-1L7,7zM17,17L7,17v-1.79c0,-0.45 -0.54,-0.67 -0.85,-0.35l-2.79,2.79c-0.2,0.2 -0.2,0.51 0,0.71l2.79,2.79c0.31,0.31 0.85,0.09 0.85,-0.36L7,19h11c0.55,0 1,-0.45 1,-1v-4c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v3z" />
|
||||
</vector>
|
@ -0,0 +1 @@
|
||||
/build
|
@ -0,0 +1,27 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
group = 'com.github.tom79'
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 33
|
||||
versionCode 3
|
||||
versionName "1.0.12"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'androidx.appcompat:appcompat:1.6.0'
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
POM_NAME=SparkButton
|
||||
POM_ARTIFACT_ID=sparkbutton
|
||||
POM_PACKAGING=aar
|
@ -0,0 +1,96 @@
|
||||
apply plugin: 'maven'
|
||||
apply plugin: 'signing'
|
||||
|
||||
def isReleaseBuild() {
|
||||
return VERSION_NAME.contains("SNAPSHOT") == false
|
||||
}
|
||||
|
||||
def getReleaseRepositoryUrl() {
|
||||
return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL
|
||||
: "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
|
||||
}
|
||||
|
||||
def getSnapshotRepositoryUrl() {
|
||||
return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL
|
||||
: "https://oss.sonatype.org/content/repositories/snapshots/"
|
||||
}
|
||||
|
||||
def getRepositoryUsername() {
|
||||
return hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : ""
|
||||
}
|
||||
|
||||
def getRepositoryPassword() {
|
||||
return hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : ""
|
||||
}
|
||||
|
||||
afterEvaluate { project ->
|
||||
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
|
||||
}
|
||||
}
|
@ -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 *;
|
||||
#}
|
@ -0,0 +1,4 @@
|
||||
<manifest package="com.varunest.sparkbutton">
|
||||
|
||||
<application />
|
||||
</manifest>
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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<SparkAnimationView, Float> INNER_CIRCLE_RADIUS_PROGRESS =
|
||||
new Property<SparkAnimationView, Float>(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<SparkAnimationView, Float> DOTS_PROGRESS = new Property<SparkAnimationView, Float>(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<SparkAnimationView, Float> OUTER_CIRCLE_RADIUS_PROGRESS =
|
||||
new Property<SparkAnimationView, Float>(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();
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<declare-styleable name="SparkButton">
|
||||
<attr name="iconSize" format="dimension|reference" />
|
||||
<attr name="activeImage" format="reference" />
|
||||
<attr name="inactiveImage" format="reference" />
|
||||
<attr name="primaryColor" format="reference" />
|
||||
<attr name="secondaryColor" format="reference" />
|
||||
<attr name="animationSpeed" format="float" />
|
||||
</declare-styleable>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="spark_primary_color">#FFFFC107</color>
|
||||
<color name="spark_secondary_color">#FFFF5722</color>
|
||||
<color name="spark_image_tint">#00000000</color>
|
||||
</resources>
|
@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">SparkButton</string>
|
||||
</resources>
|
Loading…
Reference in new issue