Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve animation runner interaction with view #7280

Open
wants to merge 1 commit into
base: google
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -497,14 +497,17 @@ public void test_nextFocusDownId() throws Exception {
@Test
public void startAnimation() {
AlphaAnimation animation = new AlphaAnimation(0, 1);

Animation.AnimationListener listener = mock(Animation.AnimationListener.class);
animation.setAnimationListener(listener);

view.startAnimation(animation);
shadowMainLooper().idle();

verify(listener).onAnimationStart(animation);
verify(listener).onAnimationEnd(animation);
assertThat(animation.isInitialized()).isTrue();
assertThat(view.getAnimation()).isNull();
assertThat(shadowOf(view).getAnimations()).contains(animation);
}

@Test
Expand Down
Expand Up @@ -36,8 +36,10 @@
import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.Transformation;
import com.google.common.collect.ImmutableList;
import java.io.PrintStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
Expand Down Expand Up @@ -85,6 +87,7 @@ public class ShadowView {
private View.OnCreateContextMenuListener onCreateContextMenuListener;
private Rect globalVisibleRect;
private int layerType;
private final ArrayList<Animation> animations = new ArrayList<>();
private AnimationRunner animationRunner;

/**
Expand Down Expand Up @@ -641,12 +644,27 @@ protected int getLayerType() {
return this.layerType;
}

/** Returns a list of all animations that have been set on this view. */
public ImmutableList<Animation> getAnimations() {
return ImmutableList.copyOf(animations);
}

/** Resets the list returned by {@link #getAnimations()} to an empty list. */
public void clearAnimations() {
animations.clear();
}

@Implementation
protected void setAnimation(final Animation animation) {
reflector(_View_.class, realView).setAnimation(animation);

if (animation != null) {
new AnimationRunner(animation);
animations.add(animation);
if (animationRunner != null) {
animationRunner.cancel();
}
animationRunner = new AnimationRunner(animation);
animationRunner.start();
}
}

Expand All @@ -673,61 +691,75 @@ protected boolean initialAwakenScrollBars() {

private class AnimationRunner implements Runnable {
private final Animation animation;
private long startTime, startOffset, elapsedTime;
private final Transformation transformation = new Transformation();
private long startTime;
private long elapsedTime;
private boolean canceled;

AnimationRunner(Animation animation) {
this.animation = animation;
start();
}

private void start() {
startTime = animation.getStartTime();
startOffset = animation.getStartOffset();
Choreographer choreographer = Choreographer.getInstance();
if (animationRunner != null) {
choreographer.removeCallbacks(Choreographer.CALLBACK_ANIMATION, animationRunner, null);
long startOffset = animation.getStartOffset();
long startDelay =
startTime == Animation.START_ON_FIRST_FRAME
? startOffset
: (startTime + startOffset) - SystemClock.uptimeMillis();
Choreographer.getInstance()
.postCallbackDelayed(Choreographer.CALLBACK_ANIMATION, this, null, startDelay);
}

private boolean step() {
long animationTime =
animation.getStartTime() == Animation.START_ON_FIRST_FRAME
? SystemClock.uptimeMillis()
: (animation.getStartTime() + animation.getStartOffset() + elapsedTime);
// Note in real android the parent is non-nullable, retain legacy robolectric behavior which
// allows detached views to animate.
if (!animation.isInitialized() && realView.getParent() != null) {
View parent = (View) realView.getParent();
animation.initialize(
realView.getWidth(), realView.getHeight(), parent.getWidth(), parent.getHeight());
}
animationRunner = this;
int startDelay;
if (startTime == Animation.START_ON_FIRST_FRAME) {
startDelay = (int) startOffset;
} else {
startDelay = (int) ((startTime + startOffset) - SystemClock.uptimeMillis());
boolean next = animation.getTransformation(animationTime, transformation);
// Note in real view implementation it doesn't check the animation equality before clearing,
// but in the real implementation the animation listeners are posted so it doesn't race with
// chained animations.
if (realView.getAnimation() == animation && !next) {
if (!animation.getFillAfter()) {
realView.clearAnimation();
}
}
choreographer.postCallbackDelayed(Choreographer.CALLBACK_ANIMATION, this, null, startDelay);
// We can't handle infinitely repeating animations in the current scheduling model, so abort
// after one iteration.
return next
&& (animation.getRepeatCount() != Animation.INFINITE
|| elapsedTime < animation.getDuration());
}

@Override
public void run() {
// Abort if start time has been messed with, as this simulation is only designed to handle
// standard situations.
if (!canceled
&& (animation.getStartTime() == startTime && animation.getStartOffset() == startOffset)
&& animation.getTransformation(
startTime == Animation.START_ON_FIRST_FRAME
? SystemClock.uptimeMillis()
: (startTime + startOffset + elapsedTime),
new Transformation())
&&
// We can't handle infinitely repeating animations in the current scheduling model,
// so abort after one iteration.
!(animation.getRepeatCount() == Animation.INFINITE
&& elapsedTime >= animation.getDuration())) {
// Update startTime if it had a value of Animation.START_ON_FIRST_FRAME
if (!canceled && animation.getStartTime() == startTime && step()) {
// Start time updates for repeating animations and if START_ON_FIRST_FRAME.
startTime = animation.getStartTime();
elapsedTime +=
ShadowLooper.looperMode().equals(LooperMode.Mode.LEGACY)
? ShadowChoreographer.getFrameInterval() / TimeUtils.NANOS_PER_MS
: ShadowChoreographer.getFrameDelay().toMillis();
Choreographer.getInstance().postCallback(Choreographer.CALLBACK_ANIMATION, this, null);
} else {
} else if (animationRunner == this) {
animationRunner = null;
}
}

public void cancel() {
this.canceled = true;
Choreographer.getInstance()
.removeCallbacks(Choreographer.CALLBACK_ANIMATION, animationRunner, null);
}
}

Expand Down