// Copyright 2016 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.bottombar;
import static org.chromium.chrome.browser.compositor.layouts.ChromeAnimation.AnimatableAnimation.createAnimation;
import android.content.Context;
import android.view.animation.Interpolator;
import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel.PanelState;
import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel.StateChangeReason;
import org.chromium.chrome.browser.compositor.layouts.ChromeAnimation;
import org.chromium.chrome.browser.compositor.layouts.ChromeAnimation.Animatable;
import org.chromium.chrome.browser.compositor.layouts.ChromeAnimation.Animation;
import org.chromium.chrome.browser.compositor.layouts.LayoutUpdateHost;
import org.chromium.chrome.browser.util.MathUtils;
/**
* Base abstract class for animating the Overlay Panel.
*/
public abstract class OverlayPanelAnimation extends OverlayPanelBase
implements Animatable<OverlayPanelAnimation.Property> {
/**
* Animation properties.
*/
protected enum Property {
PANEL_HEIGHT
}
/**
* The base duration of animations in milliseconds. This value is based on
* the Kennedy specification for slow animations.
*/
public static final long BASE_ANIMATION_DURATION_MS = 218;
/** The maximum animation duration in milliseconds. */
public static final long MAXIMUM_ANIMATION_DURATION_MS = 350;
/** The minimum animation duration in milliseconds. */
private static final long MINIMUM_ANIMATION_DURATION_MS = Math.round(7 * 1000 / 60);
/** Average animation velocity in dps per second. */
private static final float INITIAL_ANIMATION_VELOCITY_DP_PER_SECOND = 1750f;
/** The PanelState to which the Panel is being animated. */
private PanelState mAnimatingState;
/** The StateChangeReason for which the Panel is being animated. */
private StateChangeReason mAnimatingStateReason;
/** The animation set. */
private ChromeAnimation<Animatable<?>> mLayoutAnimations;
/** The {@link LayoutUpdateHost} used to request a new frame to be updated and rendered. */
private final LayoutUpdateHost mUpdateHost;
// ============================================================================================
// Constructor
// ============================================================================================
/**
* @param context The current Android {@link Context}.
* @param updateHost The {@link LayoutUpdateHost} used to request updates in the Layout.
*/
public OverlayPanelAnimation(Context context, LayoutUpdateHost updateHost) {
super(context);
mUpdateHost = updateHost;
}
// ============================================================================================
// Animation API
// ============================================================================================
/**
* Animates the Overlay Panel to its maximized state.
*
* @param reason The reason for the change of panel state.
*/
protected void maximizePanel(StateChangeReason reason) {
animatePanelToState(PanelState.MAXIMIZED, reason);
}
/**
* Animates the Overlay Panel to its intermediary state.
*
* @param reason The reason for the change of panel state.
*/
protected void expandPanel(StateChangeReason reason) {
animatePanelToState(PanelState.EXPANDED, reason);
}
/**
* Animates the Overlay Panel to its peeked state.
*
* @param reason The reason for the change of panel state.
*/
protected void peekPanel(StateChangeReason reason) {
updateBasePageTargetY();
// TODO(pedrosimonetti): Implement custom animation with the following values.
// int SEARCH_BAR_ANIMATION_DURATION_MS = 218;
// float SEARCH_BAR_SLIDE_OFFSET_DP = 40;
// float mSearchBarHeightDp;
// setTranslationY(mIsShowingFirstRunFlow
// ? mSearchBarHeightDp : SEARCH_BAR_SLIDE_OFFSET_DP);
// setInterpolator(BakedBezierInterpolator.FADE_IN_CURVE);
animatePanelToState(PanelState.PEEKED, reason);
}
@Override
protected void closePanel(StateChangeReason reason, boolean animate) {
if (animate) {
// Only animates the closing action if not doing that already.
if (mAnimatingState != PanelState.CLOSED) {
animatePanelToState(PanelState.CLOSED, reason);
}
} else {
resizePanelToState(PanelState.CLOSED, reason);
}
}
@Override
protected void handleSizeChanged(float width, float height, float previousWidth) {
if (!isShowing()) return;
boolean wasFullWidthSizePanel = doesMatchFullWidthCriteria(previousWidth);
boolean isFullWidthSizePanel = isFullWidthSizePanel();
// We support resize from any full width to full width, or from narrow width to narrow width
// when the width does not change (as when the keyboard is shown/hidden).
boolean isPanelResizeSupported = isFullWidthSizePanel && wasFullWidthSizePanel
|| !isFullWidthSizePanel && !wasFullWidthSizePanel && width == previousWidth;
// TODO(pedrosimonetti): See crbug.com/568351.
// We can't keep the panel opened after a viewport size change when the panel's
// ContentView needs to be resized to a non-default size. The panel provides
// different desired MeasureSpecs when full-width vs narrow-width
// (See {@link OverlayPanel#createNewOverlayPanelContentInternal()}).
// When the activity is resized, ContentViewClient asks for the MeasureSpecs
// before the panel is notified of the size change, resulting in the panel's
// ContentView being laid out incorrectly.
if (isPanelResizeSupported) {
if (mAnimatingState != PanelState.UNDEFINED) {
// If the size changes when an animation is happening, then we need to restart the
// animation, because the size of the Panel might have changed as well.
animatePanelToState(mAnimatingState, mAnimatingStateReason);
} else {
updatePanelForSizeChange();
}
} else {
// TODO(pedrosimonetti): Find solution that does not require async handling.
// NOTE(pedrosimonetti): Should close the Panel asynchronously because
// we might be in the middle of laying out the CompositorViewHolder
// View. See {@link CompositorViewHolder#onLayout()}. Closing the Panel
// has the effect of destroying the Views used by the Panel (which are
// children of the CompositorViewHolder), and if we do that synchronously
// it will cause a crash in {@link FrameLayout#layoutChildren()}.
mContainerView.getHandler().post(new Runnable() {
@Override
public void run() {
closePanel(StateChangeReason.UNKNOWN, false);
}
});
}
}
/**
* Updates the Panel so it preserves its state when the size changes.
*/
protected void updatePanelForSizeChange() {
resizePanelToState(getPanelState(), StateChangeReason.UNKNOWN);
}
/**
* Animates the Overlay Panel to a given |state| with a default duration.
*
* @param state The state to animate to.
* @param reason The reason for the change of panel state.
*/
private void animatePanelToState(PanelState state, StateChangeReason reason) {
animatePanelToState(state, reason, BASE_ANIMATION_DURATION_MS);
}
/**
* Animates the Overlay Panel to a given |state| with a custom |duration|.
*
* @param state The state to animate to.
* @param reason The reason for the change of panel state.
* @param duration The animation duration in milliseconds.
*/
protected void animatePanelToState(PanelState state, StateChangeReason reason, long duration) {
mAnimatingState = state;
mAnimatingStateReason = reason;
final float height = getPanelHeightFromState(state);
animatePanelTo(height, duration);
}
/**
* Resizes the Overlay Panel to a given |state|.
*
* @param state The state to resize to.
* @param reason The reason for the change of panel state.
*/
protected void resizePanelToState(PanelState state, StateChangeReason reason) {
cancelHeightAnimation();
final float height = getPanelHeightFromState(state);
setPanelHeight(height);
setPanelState(state, reason);
requestUpdate();
}
// ============================================================================================
// Animation Helpers
// ============================================================================================
/**
* Animates the Panel to its nearest state.
*/
protected void animateToNearestState() {
// Calculate the nearest state from the current position, and then calculate the duration
// of the animation that will start with a desired initial velocity and move the desired
// amount of dps (displacement).
final PanelState nearestState = findNearestPanelStateFromHeight(getHeight(), 0.0f);
final float displacement = getPanelHeightFromState(nearestState) - getHeight();
final long duration = calculateAnimationDuration(
INITIAL_ANIMATION_VELOCITY_DP_PER_SECOND, displacement);
animatePanelToState(nearestState, StateChangeReason.SWIPE, duration);
}
/**
* Animates the Panel to its projected state, given a particular vertical |velocity|.
*
* @param velocity The velocity of the gesture in dps per second.
*/
protected void animateToProjectedState(float velocity) {
PanelState projectedState = getProjectedState(velocity);
final float displacement = getPanelHeightFromState(projectedState) - getHeight();
final long duration = calculateAnimationDuration(velocity, displacement);
animatePanelToState(projectedState, StateChangeReason.FLING, duration);
}
/**
* @param velocity The given velocity.
* @return The projected state the Panel will be if the given velocity is applied.
*/
protected PanelState getProjectedState(float velocity) {
final float kickY = calculateAnimationDisplacement(velocity, BASE_ANIMATION_DURATION_MS);
final float projectedHeight = getHeight() - kickY;
// Calculate the projected state the Panel will be at the end of the fling movement and the
// duration of the animation given the current velocity and the projected displacement.
PanelState projectedState = findNearestPanelStateFromHeight(projectedHeight, velocity);
return projectedState;
}
/**
* Calculates the animation displacement given the |initialVelocity| and a
* desired |duration|.
*
* @param initialVelocity The initial velocity of the animation in dps per second.
* @param duration The desired duration of the animation in milliseconds.
* @return The animation displacement in dps.
*/
protected float calculateAnimationDisplacement(float initialVelocity, float duration) {
// NOTE(pedrosimonetti): This formula assumes the deceleration curve is
// quadratic (t^2),
// hence the displacement formula should be:
// displacement = initialVelocity * duration / 2
//
// We are also converting the duration from milliseconds to seconds,
// which explains why
// we are dividing by 2000 (2 * 1000) instead of 2.
return initialVelocity * duration / 2000;
}
/**
* Calculates the animation duration given the |initialVelocity| and a
* desired |displacement|.
*
* @param initialVelocity The initial velocity of the animation in dps per second.
* @param displacement The displacement of the animation in dps.
* @return The animation duration in milliseconds.
*/
private long calculateAnimationDuration(float initialVelocity, float displacement) {
// NOTE(pedrosimonetti): This formula assumes the deceleration curve is
// quadratic (t^2),
// hence the duration formula should be:
// duration = 2 * displacement / initialVelocity
//
// We are also converting the duration from seconds to milliseconds,
// which explains why
// we are multiplying by 2000 (2 * 1000) instead of 2.
return MathUtils.clamp(Math.round(Math.abs(2000 * displacement / initialVelocity)),
MINIMUM_ANIMATION_DURATION_MS, MAXIMUM_ANIMATION_DURATION_MS);
}
/**
* Cancels any height animation in progress.
*/
protected void cancelHeightAnimation() {
cancelAnimation(this, Property.PANEL_HEIGHT);
}
// ============================================================================================
// Layout Integration
// ============================================================================================
/**
* Requests a new frame to be updated and rendered.
*/
protected void requestUpdate() {
// NOTE(pedrosimonetti): mUpdateHost will be null in the ContextualSearchEventFilterTest,
// so we always need to check if it's null before calling requestUpdate.
if (mUpdateHost != null) {
mUpdateHost.requestUpdate();
}
}
// ============================================================================================
// Animation Framework
// ============================================================================================
/**
* Animates the Overlay Panel to a given |height| with a custom |duration|.
*
* @param height The height to animate to.
* @param duration The animation duration in milliseconds.
*/
private void animatePanelTo(float height, long duration) {
animateProperty(Property.PANEL_HEIGHT, getHeight(), height, duration);
}
/**
* Animates the Overlay Panel.
*
* @param property The property which will be animated.
* @param start The initial value.
* @param end The final value.
* @param duration The animation duration in milliseconds.
*/
protected void animateProperty(Property property, float start, float end, long duration) {
if (duration > 0) {
if (animationIsRunning()) {
cancelAnimation(this, property);
}
addToAnimation(this, property, start, end, duration, 0);
}
}
/**
* Sets a property for an animation.
*
* @param prop The property to update.
* @param value New value of the property.
*/
@Override
public void setProperty(Property prop, float value) {
if (prop == Property.PANEL_HEIGHT) {
setPanelHeight(value);
}
}
@Override
public void onPropertyAnimationFinished(Property prop) {}
/**
* Steps the animation forward and updates all the animated values.
* @param time The current time of the app in ms.
* @param jumpToEnd Whether to finish the animation.
* @return Whether the animation was finished.
*/
public boolean onUpdateAnimation(long time, boolean jumpToEnd) {
boolean finished = true;
if (mLayoutAnimations != null) {
if (jumpToEnd) {
finished = mLayoutAnimations.finished();
mLayoutAnimations.updateAndFinish();
} else {
finished = mLayoutAnimations.update(time);
}
if (finished || jumpToEnd) {
mLayoutAnimations = null;
onAnimationFinished();
}
requestUpdate();
}
return finished;
}
/**
* Called when layout-specific actions are needed after the animation finishes.
*/
protected void onAnimationStarted() {
}
/**
* Called when layout-specific actions are needed after the animation finishes.
*/
protected void onAnimationFinished() {
// If animating to a particular PanelState, and after completing
// resizing the Panel to its desired state, then the Panel's state
// should be updated. This method also is called when an animation
// is cancelled (which can happen by a subsequent gesture while
// an animation is happening). That's why the actual height should
// be checked.
// TODO(mdjones): Move animations not directly related to the panel's state into their
// own animation handler (i.e. peek promo, G sprite, etc.). See https://crbug.com/617307.
if (mAnimatingState != null && mAnimatingState != PanelState.UNDEFINED
&& getHeight() == getPanelHeightFromState(mAnimatingState)) {
setPanelState(mAnimatingState, mAnimatingStateReason);
}
mAnimatingState = PanelState.UNDEFINED;
mAnimatingStateReason = StateChangeReason.UNKNOWN;
}
/**
* Creates an {@link org.chromium.chrome.browser.compositor.layouts.ChromeAnimation.Animatable}
* and adds it to the animation.
* Automatically sets the start value at the beginning of the animation.
*/
public <T extends Enum<?>> void addToAnimation(Animatable<T> object, T prop, float start,
float end, long duration, long startTime) {
addToAnimation(object, prop, start, end, duration, startTime, false);
}
/**
* Creates an {@link org.chromium.chrome.browser.compositor.layouts.ChromeAnimation.Animatable}
* and adds it to the animation. Uses a deceleration interpolator by default.
*/
public <T extends Enum<?>> void addToAnimation(Animatable<T> object, T prop, float start,
float end, long duration, long startTime, boolean setStartValueAfterDelay) {
addToAnimation(object, prop, start, end, duration, startTime, setStartValueAfterDelay,
ChromeAnimation.getDecelerateInterpolator());
}
/**
* Creates an {@link org.chromium.chrome.browser.compositor.layouts.ChromeAnimation.Animatable}
* and adds it to the animation.
*
* @param <T> The Enum type of the Property being used
* @param object The object being animated
* @param prop The property being animated
* @param start The starting value of the animation
* @param end The ending value of the animation
* @param duration The duration of the animation in ms
* @param startTime The start time in ms
* @param setStartValueAfterDelay See {@link Animation#setStartValueAfterStartDelay(boolean)}
* @param interpolator The interpolator to use for the animation
*/
public <T extends Enum<?>> void addToAnimation(Animatable<T> object, T prop, float start,
float end, long duration, long startTime, boolean setStartValueAfterDelay,
Interpolator interpolator) {
ChromeAnimation.Animation<Animatable<?>> component = createAnimation(object, prop, start,
end, duration, startTime, setStartValueAfterDelay, interpolator);
addToAnimation(component);
}
/**
* Appends an Animation to the current animation set and starts it immediately. If the set is
* already finished or doesn't exist, the animation set is also started.
*/
protected void addToAnimation(ChromeAnimation.Animation<Animatable<?>> component) {
if (mLayoutAnimations == null || mLayoutAnimations.finished()) {
onAnimationStarted();
mLayoutAnimations = new ChromeAnimation<Animatable<?>>();
mLayoutAnimations.start();
}
component.start();
mLayoutAnimations.add(component);
requestUpdate();
}
/**
* @return whether or not the animation is currently being run.
*/
public boolean animationIsRunning() {
return mLayoutAnimations != null && !mLayoutAnimations.finished();
}
/**
* Cancels any animation for the given object and property.
* @param object The object being animated.
* @param prop The property to search for.
*/
public <T extends Enum<?>> void cancelAnimation(Animatable<T> object, T prop) {
if (mLayoutAnimations != null) {
mLayoutAnimations.cancel(object, prop);
}
}
}