mirror of
https://github.com/oxen-io/session-android.git
synced 2025-01-11 19:53:39 +00:00
Fix typing indicator animation.
The Android animators were getting out of sync when frames were dropped (despite my best efforts), so now we just manually render each animation frame as a function of time, so it never gets screwed up. Fixes #8388
This commit is contained in:
parent
36b24d0a20
commit
5d1fcdaded
@ -1,35 +1,35 @@
|
|||||||
package org.thoughtcrime.securesms.components;
|
package org.thoughtcrime.securesms.components;
|
||||||
|
|
||||||
import android.animation.Animator;
|
|
||||||
import android.animation.AnimatorListenerAdapter;
|
|
||||||
import android.animation.AnimatorSet;
|
|
||||||
import android.animation.ObjectAnimator;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.PorterDuff;
|
import android.graphics.PorterDuff;
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
|
||||||
public class TypingIndicatorView extends LinearLayout {
|
public class TypingIndicatorView extends LinearLayout {
|
||||||
|
|
||||||
private static final long DURATION = 300;
|
private static final long DURATION = 300;
|
||||||
private static final long PRE_DELAY = 500;
|
private static final long PRE_DELAY = 500;
|
||||||
private static final long POST_DELAY = 500;
|
private static final long POST_DELAY = 500;
|
||||||
|
private static final long CYCLE_DURATION = 1500;
|
||||||
|
private static final long DOT_DURATION = 600;
|
||||||
|
private static final float MIN_ALPHA = 0.4f;
|
||||||
|
private static final float MIN_SCALE = 0.75f;
|
||||||
|
|
||||||
|
private boolean isActive;
|
||||||
|
private long startTime;
|
||||||
|
|
||||||
private View dot1;
|
private View dot1;
|
||||||
private View dot2;
|
private View dot2;
|
||||||
private View dot3;
|
private View dot3;
|
||||||
|
|
||||||
private AnimatorSet animation1;
|
|
||||||
private AnimatorSet animation2;
|
|
||||||
private AnimatorSet animation3;
|
|
||||||
|
|
||||||
public TypingIndicatorView(Context context) {
|
public TypingIndicatorView(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
initialize(null);
|
initialize(null);
|
||||||
@ -43,6 +43,8 @@ public class TypingIndicatorView extends LinearLayout {
|
|||||||
private void initialize(@Nullable AttributeSet attrs) {
|
private void initialize(@Nullable AttributeSet attrs) {
|
||||||
inflate(getContext(), R.layout.typing_indicator_view, this);
|
inflate(getContext(), R.layout.typing_indicator_view, this);
|
||||||
|
|
||||||
|
setWillNotDraw(false);
|
||||||
|
|
||||||
dot1 = findViewById(R.id.typing_dot1);
|
dot1 = findViewById(R.id.typing_dot1);
|
||||||
dot2 = findViewById(R.id.typing_dot2);
|
dot2 = findViewById(R.id.typing_dot2);
|
||||||
dot3 = findViewById(R.id.typing_dot3);
|
dot3 = findViewById(R.id.typing_dot3);
|
||||||
@ -56,60 +58,66 @@ public class TypingIndicatorView extends LinearLayout {
|
|||||||
dot2.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY);
|
dot2.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY);
|
||||||
dot3.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY);
|
dot3.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
animation1 = getAnimation(dot1, DURATION, 0 );
|
@Override
|
||||||
animation2 = getAnimation(dot2, DURATION, DURATION / 2);
|
protected void onDraw(Canvas canvas) {
|
||||||
animation3 = getAnimation(dot3, DURATION, DURATION );
|
if (!isActive) {
|
||||||
|
super.onDraw(canvas);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
animation3.addListener(new AnimatorListenerAdapter() {
|
long timeInCycle = (System.currentTimeMillis() - startTime) % CYCLE_DURATION;
|
||||||
@Override
|
|
||||||
public void onAnimationEnd(Animator animation) {
|
render(dot1, timeInCycle, 0);
|
||||||
postDelayed(TypingIndicatorView.this::startAnimation, POST_DELAY);
|
render(dot2, timeInCycle, 150);
|
||||||
}
|
render(dot3, timeInCycle, 300);
|
||||||
});
|
|
||||||
|
super.onDraw(canvas);
|
||||||
|
postInvalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void render(View dot, long timeInCycle, long start) {
|
||||||
|
long end = start + DOT_DURATION;
|
||||||
|
long peak = start + (DOT_DURATION / 2);
|
||||||
|
|
||||||
|
if (timeInCycle < start || timeInCycle > end) {
|
||||||
|
renderDefault(dot);
|
||||||
|
} else if (timeInCycle < peak) {
|
||||||
|
renderFadeIn(dot, timeInCycle, start);
|
||||||
|
} else {
|
||||||
|
renderFadeOut(dot, timeInCycle, peak);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderDefault(View dot) {
|
||||||
|
dot.setAlpha(MIN_ALPHA);
|
||||||
|
dot.setScaleX(MIN_SCALE);
|
||||||
|
dot.setScaleY(MIN_SCALE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderFadeIn(View dot, long timeInCycle, long fadeInStart) {
|
||||||
|
float percent = (float) (timeInCycle - fadeInStart) / 300;
|
||||||
|
dot.setAlpha(MIN_ALPHA + (1 - MIN_ALPHA) * percent);
|
||||||
|
dot.setScaleX(MIN_SCALE + (1 - MIN_SCALE) * percent);
|
||||||
|
dot.setScaleY(MIN_SCALE + (1 - MIN_SCALE) * percent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderFadeOut(View dot, long timeInCycle, long fadeOutStart) {
|
||||||
|
float percent = (float) (timeInCycle - fadeOutStart) / 300;
|
||||||
|
dot.setAlpha(1 - (1 - MIN_ALPHA) * percent);
|
||||||
|
dot.setScaleX(1 - (1 - MIN_SCALE) * percent);
|
||||||
|
dot.setScaleY(1 - (1 - MIN_SCALE) * percent);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startAnimation() {
|
public void startAnimation() {
|
||||||
stopAnimation();
|
isActive = true;
|
||||||
postDelayed(() -> {
|
startTime = System.currentTimeMillis();
|
||||||
animation1.start();
|
|
||||||
animation2.start();
|
postInvalidate();
|
||||||
animation3.start();
|
|
||||||
}, PRE_DELAY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stopAnimation() {
|
public void stopAnimation() {
|
||||||
animation1.cancel();
|
isActive = false;
|
||||||
animation2.cancel();
|
|
||||||
animation3.cancel();
|
|
||||||
|
|
||||||
reset(dot1);
|
|
||||||
reset(dot2);
|
|
||||||
reset(dot3);
|
|
||||||
}
|
|
||||||
|
|
||||||
private AnimatorSet getAnimation(@NonNull View view, long duration, long startDelay) {
|
|
||||||
AnimatorSet grow = new AnimatorSet();
|
|
||||||
grow.playTogether(ObjectAnimator.ofFloat(view, View.SCALE_X, 0.5f, 1).setDuration(duration),
|
|
||||||
ObjectAnimator.ofFloat(view, View.SCALE_Y, 0.5f, 1).setDuration(duration),
|
|
||||||
ObjectAnimator.ofFloat(view, View.ALPHA, 0.5f, 1).setDuration(duration));
|
|
||||||
|
|
||||||
AnimatorSet shrink = new AnimatorSet();
|
|
||||||
shrink.playTogether(ObjectAnimator.ofFloat(view, View.SCALE_X, 1, 0.5f).setDuration(duration),
|
|
||||||
ObjectAnimator.ofFloat(view, View.SCALE_Y, 1, 0.5f).setDuration(duration),
|
|
||||||
ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0.5f).setDuration(duration));
|
|
||||||
|
|
||||||
AnimatorSet all = new AnimatorSet();
|
|
||||||
all.playSequentially(grow, shrink);
|
|
||||||
all.setStartDelay(startDelay);
|
|
||||||
|
|
||||||
return all;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void reset(View view) {
|
|
||||||
view.clearAnimation();
|
|
||||||
view.setScaleX(0.5f);
|
|
||||||
view.setScaleY(0.5f);
|
|
||||||
view.setAlpha(0.5f);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user