Commit a278c350 authored by Matthew Jones's avatar Matthew Jones Committed by Commit Bot

Implement CompositorAnimator framework

This change introduces the framework for the CompositorAnimator and a
suite of unit tests. The following classes have been added:

- CompositorAnimator:
  The new Animator for compositor animation in Java. This class
  extends the android.animation.Animator class in order to be
  interoperable and consistent with Android. The AnimatorListener
  is supported but does not support the reverse or repeat events.

  - AnimatorUpdateListener:
    This is an analog to ValueAnimator.AnimatorUpdateListener which
    provides notification of frame updates.

- CompositorAnimationHandler:
  This class is responsible for pushing updates to CompositorAnimators
  associated with a particular instance of Chrome.

The handler, listeners, and basic hookup have been integrated into
Chrome but no existing animations have been converted yet.

BUG=750381

Change-Id: Ia4612901cf1beacc13994b866b5f691ec9b38011
Reviewed-on: https://chromium-review.googlesource.com/627718
Commit-Queue: Matthew Jones <mdjones@chromium.org>
Reviewed-by: default avatarYusuf Ozuysal <yusufo@chromium.org>
Reviewed-by: default avatarDavid Trainor <dtrainor@chromium.org>
Cr-Commit-Position: refs/heads/master@{#504714}
parent 4419eeff
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.compositor.animation;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.support.annotation.NonNull;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.compositor.layouts.LayoutUpdateHost;
import java.util.ArrayList;
/**
* The handler responsible for managing and pushing updates to all of the active
* CompositorAnimators.
*/
public class CompositorAnimationHandler {
/** A list of all the handler's animators. */
private final ArrayList<CompositorAnimator> mAnimators = new ArrayList<>();
/** This handler's update host. */
private final LayoutUpdateHost mUpdateHost;
/**
* A cached copy of the list of {@link CompositorAnimator}s to prevent allocating a new list
* every update.
*/
private final ArrayList<CompositorAnimator> mCachedList = new ArrayList<>();
/**
* Whether or not an update has already been requested for the next frame due to an animation
* starting.
*/
private boolean mWasUpdateRequestedForAnimationStart;
/**
* Default constructor.
* @param host A {@link LayoutUpdateHost} responsible for requesting frames when an animation
* updates.
*/
public CompositorAnimationHandler(@NonNull LayoutUpdateHost host) {
assert host != null;
mUpdateHost = host;
}
/**
* Add an animator to the list of known animators to start receiving updates.
* @param animator The animator to start.
*/
public final void registerAndStartAnimator(final CompositorAnimator animator) {
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator a) {
mAnimators.remove(animator);
animator.removeListener(this);
}
});
mAnimators.add(animator);
if (!mWasUpdateRequestedForAnimationStart) {
mUpdateHost.requestUpdate();
mWasUpdateRequestedForAnimationStart = true;
}
}
/**
* Push an update to all the currently running animators.
* @param deltaTimeMs The time since the previous update in ms.
*/
public final void pushUpdate(long deltaTimeMs) {
mWasUpdateRequestedForAnimationStart = false;
if (mAnimators.isEmpty()) return;
// Do updates to the animators. Use a cloned list so the original list can be modified in
// the update loop.
mCachedList.addAll(mAnimators);
for (int i = 0; i < mCachedList.size(); i++) {
CompositorAnimator currentAnimator = mCachedList.get(i);
currentAnimator.doAnimationFrame(deltaTimeMs);
// Once the animation ends, it no longer needs to receive updates; remove it from the
// handler's list of animations. Restarting the animation will re-add the animation to
// this handler.
if (currentAnimator.hasEnded()) mAnimators.remove(currentAnimator);
}
mCachedList.clear();
mUpdateHost.requestUpdate();
}
/**
* Clean up this handler.
*/
public final void destroy() {
mAnimators.clear();
}
/**
* @return The number of animations that are active inside this handler.
*/
@VisibleForTesting
public int getActiveAnimationCount() {
return mAnimators.size();
}
}
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.compositor.animation;
import android.animation.Animator;
import android.animation.TimeInterpolator;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.compositor.layouts.ChromeAnimation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
/**
* An animator that can be used for animations in the Browser Compositor.
*/
public class CompositorAnimator extends Animator {
/** The different states that this animator can be in. */
@IntDef({AnimationState.STARTED, AnimationState.RUNNING, AnimationState.CANCELED,
AnimationState.ENDED})
@Retention(RetentionPolicy.SOURCE)
private @interface AnimationState {
int STARTED = 0;
int RUNNING = 1;
int CANCELED = 2;
int ENDED = 3;
}
/** The {@link CompositorAnimationHandler} running the animation. */
private final WeakReference<CompositorAnimationHandler> mHandler;
/** The list of listeners for events through the life of an animation. */
private final ArrayList<AnimatorListener> mListeners = new ArrayList<>();
/** The list of frame update listeners for this animation. */
private final ArrayList<AnimatorUpdateListener> mAnimatorUpdateListeners = new ArrayList<>();
/**
* A cached copy of the list of {@link AnimatorUpdateListener}s to prevent allocating a new list
* every update.
*/
private final ArrayList<AnimatorUpdateListener> mCachedList = new ArrayList<>();
/** The time interpolator for the animator. */
private TimeInterpolator mTimeInterpolator;
/**
* The amount of time in ms that has passed since the animation has started. This includes any
* delay that was set.
*/
private long mTimeSinceStartMs;
/**
* The fraction that the animation is complete. This number is in the range [0, 1] and accounts
* for the set time interpolator.
*/
private float mAnimatedFraction;
/** The value that the animation should start with (ending at {@link #mEndValue}). */
private float mStartValue;
/** The value that the animation will transition to (starting at {@link #mStartValue}). */
private float mEndValue;
/** The duration of the animation in ms. */
private long mDurationMs;
/**
* The animator's start delay in ms. Once {@link #start()} is called, updates are not sent until
* this time has passed.
*/
private long mStartDelayMs;
/** The current state of the animation. */
@AnimationState
private int mAnimationState = AnimationState.ENDED;
/**
* Whether the animation ended because of frame updates. This is used to determine if any
* listeners need to be updated one more time.
*/
private boolean mDidUpdateToCompletion;
/** An interface for listening for frames of an animation. */
public interface AnimatorUpdateListener {
/**
* A notification of the occurrence of another frame of the animation.
* @param animator The animator that was updated.
*/
void onAnimationUpdate(CompositorAnimator animator);
}
/**
* Create a new animator for the current context.
* @param handler The {@link CompositorAnimationHandler} responsible for running the animation.
*/
public CompositorAnimator(@NonNull CompositorAnimationHandler handler) {
mHandler = new WeakReference<>(handler);
// The default interpolator is decelerate; this mimics the existing ChromeAnimation
// behavior.
mTimeInterpolator = ChromeAnimation.getDecelerateInterpolator();
// By default, animate for 0 to 1.
mStartValue = 0;
mEndValue = 1;
}
/**
* Push an update to the animation. This should be called while the start delay is active and
* assumes that the animated object is at the starting position when {@link #start} is called.
* @param deltaTimeMs The time since the previous frame.
*/
public void doAnimationFrame(long deltaTimeMs) {
mTimeSinceStartMs += deltaTimeMs;
// Clamp to the animator's duration, taking into account the start delay.
long finalTimeMs = Math.min(mTimeSinceStartMs - mStartDelayMs, mDurationMs);
// Wait until the start delay has passed.
if (finalTimeMs < 0) return;
// In the case where duration is 0, the animation is complete.
mAnimatedFraction = 1;
if (mDurationMs > 0) {
mAnimatedFraction =
mTimeInterpolator.getInterpolation(finalTimeMs / (float) mDurationMs);
}
// Push update to listeners.
mCachedList.addAll(mAnimatorUpdateListeners);
for (int i = 0; i < mCachedList.size(); i++) mCachedList.get(i).onAnimationUpdate(this);
mCachedList.clear();
if (finalTimeMs == mDurationMs) {
mDidUpdateToCompletion = true;
end();
}
}
/**
* @return The animated fraction after being passed through the time interpolator, if set.
*/
@VisibleForTesting
public float getAnimatedFraction() {
return mAnimatedFraction;
}
/**
* Add a listener for frame occurrences.
* @param listener The listener to add.
*/
public void addUpdateListener(AnimatorUpdateListener listener) {
mAnimatorUpdateListeners.add(listener);
}
/**
* @param listener The listener to remove.
*/
public void removeUpdateListener(AnimatorUpdateListener listener) {
mAnimatorUpdateListeners.remove(listener);
}
/**
* @return Whether or not the animation has ended after being started. If the animation is
* started after ending, this value will be reset to true.
*/
public boolean hasEnded() {
return mAnimationState == AnimationState.ENDED;
}
/**
* Set the values to animate between.
* @param start The value to begin the animation with.
* @param end The value to end the animation at.
*/
@VisibleForTesting
public void setValues(float start, float end) {
mStartValue = start;
mEndValue = end;
}
/**
* @return The current value between the floats set by {@link #setValues(float, float)}.
*/
@VisibleForTesting
public float getAnimatedValue() {
return mStartValue + (getAnimatedFraction() * (mEndValue - mStartValue));
}
@Override
public void addListener(AnimatorListener listener) {
mListeners.add(listener);
}
@Override
public void removeListener(AnimatorListener listener) {
mListeners.remove(listener);
}
@Override
public void removeAllListeners() {
mListeners.clear();
mAnimatorUpdateListeners.clear();
}
@Override
public void start() {
if (mAnimationState != AnimationState.ENDED) return;
super.start();
mAnimationState = AnimationState.RUNNING;
mDidUpdateToCompletion = false;
CompositorAnimationHandler handler = mHandler.get();
if (handler != null) handler.registerAndStartAnimator(this);
mTimeSinceStartMs = 0;
ArrayList<AnimatorListener> clonedList = (ArrayList<AnimatorListener>) mListeners.clone();
for (int i = 0; i < clonedList.size(); i++) clonedList.get(i).onAnimationStart(this);
}
@Override
public void cancel() {
if (mAnimationState == AnimationState.ENDED) return;
mAnimationState = AnimationState.CANCELED;
super.cancel();
ArrayList<AnimatorListener> clonedList = (ArrayList<AnimatorListener>) mListeners.clone();
for (int i = 0; i < clonedList.size(); i++) clonedList.get(i).onAnimationCancel(this);
end();
}
@Override
public void end() {
if (mAnimationState == AnimationState.ENDED) return;
super.end();
boolean wasCanceled = mAnimationState == AnimationState.CANCELED;
mAnimationState = AnimationState.ENDED;
// If the animation was ended early but not canceled, push one last update to the listeners.
if (!mDidUpdateToCompletion && !wasCanceled) {
mAnimatedFraction = 1f;
ArrayList<AnimatorUpdateListener> clonedList =
(ArrayList<AnimatorUpdateListener>) mAnimatorUpdateListeners.clone();
for (int i = 0; i < clonedList.size(); i++) clonedList.get(i).onAnimationUpdate(this);
}
ArrayList<AnimatorListener> clonedList = (ArrayList<AnimatorListener>) mListeners.clone();
for (int i = 0; i < clonedList.size(); i++) clonedList.get(i).onAnimationEnd(this);
}
@Override
public long getStartDelay() {
return mStartDelayMs;
}
@Override
public void setStartDelay(long startDelayMs) {
if (startDelayMs < 0) startDelayMs = 0;
mStartDelayMs = startDelayMs;
}
@Override
public CompositorAnimator setDuration(long durationMs) {
if (durationMs < 0) durationMs = 0;
mDurationMs = durationMs;
return this;
}
@Override
public long getDuration() {
return mDurationMs;
}
@Override
public void setInterpolator(TimeInterpolator timeInterpolator) {
assert timeInterpolator != null;
mTimeInterpolator = timeInterpolator;
}
@Override
public boolean isRunning() {
return mAnimationState == AnimationState.RUNNING;
}
}
......@@ -20,6 +20,7 @@ import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.SuppressFBWarnings;
import org.chromium.chrome.browser.UrlConstants;
import org.chromium.chrome.browser.compositor.LayerTitleCache;
import org.chromium.chrome.browser.compositor.animation.CompositorAnimationHandler;
import org.chromium.chrome.browser.compositor.bottombar.OverlayPanelContentViewDelegate;
import org.chromium.chrome.browser.compositor.bottombar.OverlayPanelManager;
import org.chromium.chrome.browser.compositor.bottombar.contextualsearch.ContextualSearchPanel;
......@@ -116,6 +117,9 @@ public class LayoutManager
// Whether the currently active event filter has changed.
private boolean mIsNewEventFilter;
/** The animation handler responsible for updating all the browser compositor's animations. */
private final CompositorAnimationHandler mAnimationHandler;
/**
* Creates a {@link LayoutManager} instance.
* @param host A {@link LayoutManagerHost} instance.
......@@ -127,6 +131,8 @@ public class LayoutManager
mContext = host.getContext();
LayoutRenderHost renderHost = host.getLayoutRenderHost();
mAnimationHandler = new CompositorAnimationHandler(this);
mToolbarOverlay = new ToolbarSceneLayer(mContext, this, renderHost);
mOverlayPanelManager = new OverlayPanelManager();
......@@ -244,6 +250,9 @@ public class LayoutManager
public boolean onUpdate(long timeMs, long dtMs) {
if (!mUpdateRequested) return false;
mUpdateRequested = false;
mAnimationHandler.pushUpdate(dtMs);
final Layout layout = getActiveLayout();
if (layout != null && layout.onUpdate(timeMs, dtMs) && layout.isHiding()) {
layout.doneHiding();
......@@ -314,6 +323,7 @@ public class LayoutManager
* Cleans up and destroys this object. It should not be used after this.
*/
public void destroy() {
mAnimationHandler.destroy();
mSceneChangeObservers.clear();
if (mStaticLayout != null) mStaticLayout.destroy();
if (mOverlayPanelManager != null) mOverlayPanelManager.destroy();
......
......@@ -140,6 +140,8 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/compositor/Invalidator.java",
"java/src/org/chromium/chrome/browser/compositor/LayerTitleCache.java",
"java/src/org/chromium/chrome/browser/compositor/TitleCache.java",
"java/src/org/chromium/chrome/browser/compositor/animation/CompositorAnimationHandler.java",
"java/src/org/chromium/chrome/browser/compositor/animation/CompositorAnimator.java",
"java/src/org/chromium/chrome/browser/compositor/bottombar/OverlayContentDelegate.java",
"java/src/org/chromium/chrome/browser/compositor/bottombar/OverlayContentProgressObserver.java",
"java/src/org/chromium/chrome/browser/compositor/bottombar/OverlayPanel.java",
......@@ -1781,6 +1783,7 @@ chrome_junit_test_java_sources = [
"junit/src/org/chromium/chrome/browser/SSLClientCertificateRequestTest.java",
"junit/src/org/chromium/chrome/browser/background_task_scheduler/NativeBackgroundTaskTest.java",
"junit/src/org/chromium/chrome/browser/browseractions/BrowserActionsIntentTest.java",
"junit/src/org/chromium/chrome/browser/compositor/animation/CompositorAnimatorTest.java",
"junit/src/org/chromium/chrome/browser/compositor/overlays/strip/StripLayoutHelperTest.java",
"junit/src/org/chromium/chrome/browser/compositor/CompositorSurfaceManagerTest.java",
"junit/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchContextForTest.java",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment