Not sure my logic is correct or not but it's working fine :P
The animation will start on on tap of the circle and be continue until finger up
here is my xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.myapplication.systemupdate.CircleView
android:id="@+id/voiceView"
android:layout_width="150dp"
android:layout_height="150dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.cardview.widget.CardView
android:id="@+id/iv_square"
android:layout_width="80dp"
android:layout_height="80dp"
app:cardBackgroundColor="@color/black"
app:cardCornerRadius="40dp"
app:cardElevation="0dp"
app:contentPadding="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
this is CircleView.java
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
import java.util.Objects;
/**
* https://github.com/kyze8439690/AndroidVoiceAnimation
*/
public class CircleView extends View {
private Paint mPaint;
private AnimatorSet mAnimatorSet = new AnimatorSet();
private float mMinRadius;
private float mMaxRadius;
private float mCurrentRadius;
private float mCurrentStroke;
private float mMinStroke;
private float mMaxStroke;
public CircleView(Context context) {
super(context);
init();
}
public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public float getmMinRadius() {
return mMinRadius;
}
public float getmMaxRadius() {
return mMaxRadius;
}
public float getmMinStroke() {
return mMinStroke;
}
public float getmMaxStroke() {
return mMaxStroke;
}
private void init() {
mMinRadius = dpToPx(getContext(), 45);
mMinStroke = 15;
mCurrentRadius = mMinRadius;
mCurrentStroke = mMinStroke;
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(mMinStroke);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(Color.rgb(255, 0, 0));
}
@Override
protected void onSizeChanged(int w, int h, int oldWidth, int oldHeight) {
super.onSizeChanged(w, h, oldWidth, oldHeight);
mMaxRadius = (float) (Math.min(w, h) / 2.2);
mMaxStroke = 60;
}
public int dpToPx(Context context, float valueInDp) {
DisplayMetrics metrics = Objects.requireNonNull(context).getResources().getDisplayMetrics();
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, valueInDp, metrics);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float width = getWidth();
float height = getHeight();
float radius = Math.max(mCurrentRadius, mMinRadius);
float stroke = Math.max(mCurrentStroke, mMinStroke);
mPaint.setStrokeWidth(stroke);
canvas.drawCircle(width / 2, height / 2, radius, mPaint);
}
public void animateRadius(float radius, float stroke) {
if (stroke > mMaxStroke) {
stroke = mMaxStroke;
} else if (stroke < mMinStroke) {
stroke = mMinStroke;
}
if (mAnimatorSet.isRunning()) {
mAnimatorSet.cancel();
}
mAnimatorSet = new AnimatorSet();
mAnimatorSet.play(ObjectAnimator.ofFloat(this, "CurrentRadius", getCurrentRadius(), (float) (radius - (stroke / 2.2))))
.with(ObjectAnimator.ofFloat(this, "CurrentStroke", getCurrentStroke(), stroke));
mAnimatorSet.setDuration(80);
mAnimatorSet.setInterpolator(new DecelerateInterpolator());
mAnimatorSet.start();
invalidate();
}
public float getCurrentRadius() {
return mCurrentRadius;
}
/**
* required this method to set ObjectAnimator
*
* @param currentRadius current radius
*/
public void setCurrentRadius(float currentRadius) {
mCurrentRadius = currentRadius;
invalidate();
}
public float getCurrentStroke() {
return mCurrentStroke;
}
public void setCurrentStroke(float mCurrentStroke) {
this.mCurrentStroke = mCurrentStroke;
invalidate();
}
public void endAnimation() {
if (mAnimatorSet != null) mAnimatorSet.end();
}
}
and this is MainActivity.java
import androidx.appcompat.app.AppCompatActivity;
import androidx.cardview.widget.CardView;
import androidx.transition.ChangeBounds;
import androidx.transition.TransitionManager;
import androidx.transition.TransitionSet;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Handler;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import java.util.ArrayList;
public class MainActivity extends AppCompatActivity {
private CircleView circleView;
private CardView cardView;
private Handler handler;
private Runnable runnable;
private int i = 0;
private final ArrayList<Integer> al = new ArrayList<>();
private final ArrayList<Integer> al2 = new ArrayList<>();
@SuppressLint("ClickableViewAccessibility")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
circleView = findViewById(R.id.voiceView);
cardView = findViewById(R.id.iv_square);
cardView.setOnTouchListener((view, motionEvent) -> {
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
startAnimationOfSquare();
circleView.animateRadius(circleView.getmMaxRadius(), circleView.getmMinStroke());
handler.postDelayed(runnable, 80);
return true;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
circleView.animateRadius(circleView.getmMinRadius(), circleView.getmMinStroke());
stopAnimationOfSquare();
handler.removeCallbacks(runnable);
resetAnimation();
return true;
}
return true;
});
resetAnimation();
handler = new Handler();
runnable = () -> {
//to make smooth stroke width animation I increase and decrease value step by step
int random;
if (!al.isEmpty()) {
random = al.get(i++);
if (i >= al.size()) {
for (int j = al.size() - 1; j >= 0; j--) {
al2.add(al.get(j));
}
al.clear();
i = 0;
}
} else {
random = al2.get(i++);
if (i >= al2.size()) {
for (int j = al2.size() - 1; j >= 0; j--) {
al.add(al2.get(j));
}
al2.clear();
i = 0;
}
}
circleView.animateRadius(circleView.getmMaxRadius(), random);
handler.postDelayed(runnable, 130);
};
}
private void resetAnimation() {
i = 0;
al.clear();
al2.clear();
al.add(25);
al.add(30);
al.add(35);
al.add(40);
al.add(45);
// al.add(50);
// al.add(55);
// al.add(60);
circleView.endAnimation();
}
public int dpToPx(float valueInDp) {
DisplayMetrics metrics = getResources().getDisplayMetrics();
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, valueInDp, metrics);
}
private AnimatorSet currentAnimator;
private int settingPopupVisibilityDuration;
private void startAnimationOfSquare() {
settingPopupVisibilityDuration = getResources().getInteger(android.R.integer.config_shortAnimTime);
if (currentAnimator != null) {
currentAnimator.cancel();
}
Rect finalBounds = new Rect();
final Point globalOffset = new Point();
circleView.getGlobalVisibleRect(finalBounds, globalOffset);
TransitionManager.beginDelayedTransition(cardView, new TransitionSet()
.addTransition(new ChangeBounds()).setDuration(settingPopupVisibilityDuration));
ViewGroup.LayoutParams params = cardView.getLayoutParams();
params.height = dpToPx(40);
params.width = dpToPx(40);
cardView.setLayoutParams(params);
AnimatorSet set = new AnimatorSet();
set.play(ObjectAnimator.ofFloat(cardView, "radius", dpToPx(8)));
set.setDuration(settingPopupVisibilityDuration);
set.setInterpolator(new DecelerateInterpolator());
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
finishAnimation();
}
@Override
public void onAnimationCancel(Animator animation) {
finishAnimation();
}
private void finishAnimation() {
currentAnimator = null;
}
});
set.start();
currentAnimator = set;
}
public void stopAnimationOfSquare() {
if (currentAnimator != null) {
currentAnimator.cancel();
}
TransitionManager.beginDelayedTransition(cardView, new TransitionSet()
.addTransition(new ChangeBounds()).setDuration(settingPopupVisibilityDuration));
ViewGroup.LayoutParams params = cardView.getLayoutParams();
params.width = dpToPx(80);
params.height = dpToPx(80);
cardView.setLayoutParams(params);
AnimatorSet set1 = new AnimatorSet();
set1.play(ObjectAnimator.ofFloat(cardView, "radius", dpToPx(40)));//radius = height/2 to make it round
set1.setDuration(settingPopupVisibilityDuration);
set1.setInterpolator(new DecelerateInterpolator());
set1.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
finishAnimation();
}
@Override
public void onAnimationCancel(Animator animation) {
finishAnimation();
}
private void finishAnimation() {
currentAnimator = null;
}
});
set1.start();
currentAnimator = set1;
}
}