Adds followAnimatedTargetAlongPath, and modifies the expanded controller to use it.
This also moves the end action methods into the controller, which is a more logical place for it. That way, PhysicsAnimationLayout only has one public method (setActiveController). This sets us up better for future abstractions that don't rely on a custom view at all. (also updates the docs for these changes and several recent ones) Test: atest SystemUITests Bug: 134077101 Change-Id: I6fcc14587b07f14371fa75fbbe0cc31353aa5c0b
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
# Physics Animation Layout
|
||||
|
||||
## Overview
|
||||
**PhysicsAnimationLayout** works with an implementation of **PhysicsAnimationController** to construct and maintain physics animations for each of its child views. During the initial construction of the animations, the layout queries the controller for configuration settings such as which properties to animate, which animations to chain together, and what stiffness or bounciness to use. Once the animations are built to the controller’s specifications, the controller can then ask the layout to start, stop and manipulate them arbitrarily to achieve any desired animation effect. The controller is notified whenever children are added or removed from the layout, so that it can animate their entrance or exit, respectively.
|
||||
**PhysicsAnimationLayout** works with implementations of **PhysicsAnimationController** to configure and run physics-based animations for each of its child views. During the initial construction of the animations, the layout queries the controller for basic configuration settings such as which properties to animate, which animations to chain together, and the default physics parameters to use.
|
||||
|
||||
Once the animations are built, the controller can access **PhysicsPropertyAnimator** instances to run them. The animator behaves similarly to the familiar `ViewPropertyAnimator`, with the ability to animate `alpha`, `translation`, and `scale` values. It also supports additional functionality such as `followAnimatedTargetAlongPath` for more advanced motion.
|
||||
|
||||
The controller is notified whenever children are added or removed from the layout, so that it can animate their entrance or exit, respectively.
|
||||
|
||||
An example usage is Bubbles, which uses a PhysicsAnimationLayout for its stack of bubbles. Bubbles has controller subclasses including StackAnimationController and ExpansionAnimationController. StackAnimationController tells the layout to configure the translation animations to be chained (for the ‘following’ drag effect), and has methods such as ```moveStack(x, y)``` to animate the stack to a given point. ExpansionAnimationController asks for no animations to be chained, and exposes methods like ```expandStack()``` and ```collapseStack()```, which animate the bubbles to positions along the bottom of the screen.
|
||||
|
||||
@@ -27,7 +31,7 @@ Returns a SpringForce instance to use for animations of the given property. This
|
||||
### Animation Control Methods
|
||||
Once the layout has used the controller’s configuration properties to build the animations, the controller can use them to actually run animations. This is done for two reasons - reacting to a view being added or removed, or responding to another class (such as a touch handler or broadcast receiver) requesting an animation. ```onChildAdded```, ```onChildRemoved```, and ```setChildVisibility``` are called automatically by the layout, giving the controller the opportunity to animate the child in/out/visible/gone. Custom methods are called by anyone with access to the controller instance to do things like expand, collapse, or move the child views.
|
||||
|
||||
In either case, the controller can use `super.animationForChild` to retrieve a `PhysicsPropertyAnimator` instance. This object behaves similarly to the `ViewPropertyAnimator` object you would receive from `View.animate()`.
|
||||
In either case, the controller can use `super.animationForChild` to retrieve a `PhysicsPropertyAnimator` instance. This object behaves similarly to the `ViewPropertyAnimator` object you would receive from `View.animate()`.
|
||||
|
||||
#### PhysicsPropertyAnimator
|
||||
|
||||
@@ -36,9 +40,14 @@ Like `ViewPropertyAnimator`, `PhysicsPropertyAnimator` provides the following me
|
||||
- `translationX/Y/Z(float)`
|
||||
- `scaleX/Y(float)`
|
||||
|
||||
...as well as shortcut methods to reduce the amount of boilerplate code needed for common use cases:
|
||||
- `position(float, float, Runnable…)`, which starts translationX and translationY animations, and calls the provided callbacks only when both animations have completed.
|
||||
- `followAnimatedTargetAlongPath(Path, int, TimeInterpolator)`, which animates a ‘target’ point along the given path using a traditional Animator. As the target moves, the translationX/Y physics animations are updated to follow the target, similarly to how they might follow a touch event location. This results in the view roughly following the path, but with natural motion that takes momentum into account. For example, if a path makes a 90 degree turn to the right, the physics animations will cause the view to curve naturally towards the new trajectory.
|
||||
|
||||
It also provides the following configuration methods:
|
||||
- `withStartDelay(int)`, for starting the animation after a given delay.
|
||||
- `withStartVelocity(float)`, for starting the animation with the given start velocity.
|
||||
- `withStiffness(float)` and `withDampingRatio(float)`, for overriding the default physics param values returned by the controller’s getSpringForce method.
|
||||
- `withPositionStartVelocities(float, float)`, for setting specific start velocities for TRANSLATION_X and TRANSLATION_Y, since these typically differ.
|
||||
- `start(Runnable)`, to start the animation, with an optional end action to call when the animations for every property (including chained animations) have completed.
|
||||
|
||||
@@ -61,8 +70,7 @@ The animator has additional functionality to reduce the amount of boilerplate re
|
||||
|
||||
- Often, animations will set starting values for properties before the animation begins. Property methods like `translationX` have an overloaded variant: `translationX(from, to)`. When `start()` is called, the animation will set the view's translationX property to `from` before beginning the animation to `to`.
|
||||
- We may want to use different end actions for each property. For example, if we're animating a view to the bottom of the screen, and also fading it out, we might want to perform an action as soon as the fade out is complete. We can use `alpha(to, endAction)`, which will call endAction as soon as the alpha animation is finished. A special case is `position(x, y, endAction)`, where the endAction is called when both translationX and translationY animations have completed.
|
||||
|
||||
`PhysicsAnimationController` also provides `animationsForChildrenFromIndex(int, ChildAnimationConfigurator)`. This is a convenience method for starting animations on multiple child views, starting at the given index. The `ChildAnimationConfigurator` is called with a `PhysicsPropertyAnimator` for each child, where calls to methods like `translationX` and `withStartVelocity` can be made. `animationsForChildrenFromIndex` returns a `MultiAnimationStarter` with a single method, `startAll(endAction)`, which starts all of the animations and calls the end action when they have all completed.
|
||||
- `PhysicsAnimationController` also provides `animationsForChildrenFromIndex(int, ChildAnimationConfigurator)`. This is a convenience method for starting animations on multiple child views, starting at the given index. The `ChildAnimationConfigurator` is called with a `PhysicsPropertyAnimator` for each child, where calls to methods like `translationX` and `withStartVelocity` can be made. `animationsForChildrenFromIndex` returns a `MultiAnimationStarter` with a single method, `startAll(endAction)`, which starts all of the animations and calls the end action when they have all completed.
|
||||
|
||||
##### Examples
|
||||
Spring the stack of bubbles (whose animations are chained) to the bottom of the screen, shrinking them to 50% size. Once the first bubble is done shrinking, begin fading them out, and then remove them all from the parent once all bubbles have faded out:
|
||||
@@ -93,16 +101,22 @@ animationsForChildrenFromIndex(1, (index, anim) -> anim.translationX((index - 1)
|
||||
.startAll(removeFirstView);
|
||||
```
|
||||
|
||||
Move a view up along the left side of the screen, and then to the top right of the screen (assume a view that is currently halfway down the left side of the screen). When the translation animations have finished following the target, call a callback:
|
||||
|
||||
```
|
||||
Path path = new Path();
|
||||
path.moveTo(view.getTranslationX(), view.getTranslationY());
|
||||
path.lineTo(view.getTranslationX(), 0);
|
||||
path.lineTo(mScreenWidth, 0);
|
||||
animationForChild(view)
|
||||
.followAnimatedTargetAlongPath(path, 100, new LinearInterpolator())
|
||||
.start(callbackAfterFollowingFinished);
|
||||
```
|
||||
|
||||
## PhysicsAnimationLayout
|
||||
The layout itself is a FrameLayout descendant with a few extra methods:
|
||||
|
||||
```setController(PhysicsAnimationController controller)```
|
||||
Attaches the layout to the controller, so that the controller can access the layout’s protected methods. It also constructs or reconfigures the physics animations according to the new controller’s configuration methods.
|
||||
```setActiveController(PhysicsAnimationController controller)```
|
||||
Sets the given controller as the active controller for the layout. This causes the layout to construct or reconfigure the physics animations according to the new controller’s configuration methods, and halt any in-progress animations.
|
||||
|
||||
```setEndListenerForProperty(ViewProperty property, AnimationEndListener endListener)```
|
||||
Sets an end listener that is called when all animations on the given property have ended.
|
||||
|
||||
```setMaxRenderedChildren(int max)```
|
||||
Child views beyond this limit will be set to GONE, and won't be animated, for performance reasons. Defaults to **5**.
|
||||
|
||||
It has one protected method, ```animateValueForChildAtIndex(ViewProperty property, int index, float value)```, which is visible to PhysicsAnimationController descendants. This method dispatches the given value to the appropriate animation.
|
||||
Only the currently active controller is allowed to start animations. If a different controller is set as the active controller, the previous controller will no longer be able to start animations. Attempts to do so will have no effect. This is to ensure that multiple controllers aren’t updating animations at the same time, which can cause undefined behavior.
|
||||
@@ -127,6 +127,7 @@
|
||||
<item type="id" name="scale_x_dynamicanimation_tag"/>
|
||||
<item type="id" name="scale_y_dynamicanimation_tag"/>
|
||||
<item type="id" name="physics_animator_tag"/>
|
||||
<item type="id" name="target_animator_tag" />
|
||||
|
||||
<!-- Global Actions Menu -->
|
||||
<item type="id" name="global_actions_view" />
|
||||
|
||||
@@ -18,6 +18,7 @@ package com.android.systemui.bubbles.animation;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.PointF;
|
||||
import android.view.View;
|
||||
@@ -27,6 +28,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.dynamicanimation.animation.DynamicAnimation;
|
||||
import androidx.dynamicanimation.animation.SpringForce;
|
||||
|
||||
import com.android.systemui.Interpolators;
|
||||
import com.android.systemui.R;
|
||||
|
||||
import com.google.android.collect.Sets;
|
||||
@@ -47,11 +49,11 @@ public class ExpandedAnimationController
|
||||
*/
|
||||
private static final int ANIMATE_TRANSLATION_FACTOR = 4;
|
||||
|
||||
/** How much to scale down bubbles when they're animating in/out. */
|
||||
private static final float ANIMATE_SCALE_PERCENT = 0.5f;
|
||||
/** Duration of the expand/collapse target path animation. */
|
||||
private static final int EXPAND_COLLAPSE_TARGET_ANIM_DURATION = 175;
|
||||
|
||||
/** The stack position to collapse back to in {@link #collapseBackToStack}. */
|
||||
private PointF mCollapseToPoint;
|
||||
/** Stiffness for the expand/collapse path-following animation. */
|
||||
private static final int EXPAND_COLLAPSE_ANIM_STIFFNESS = 1000;
|
||||
|
||||
/** Horizontal offset between bubbles, which we need to know to re-stack them. */
|
||||
private float mStackOffsetPx;
|
||||
@@ -116,7 +118,7 @@ public class ExpandedAnimationController
|
||||
mAnimatingExpand = true;
|
||||
mAfterExpand = after;
|
||||
|
||||
startOrUpdateExpandAnimation();
|
||||
startOrUpdatePathAnimation(true /* expanding */);
|
||||
}
|
||||
|
||||
/** Animate collapsing the bubbles back to their stacked position. */
|
||||
@@ -126,7 +128,7 @@ public class ExpandedAnimationController
|
||||
mAfterCollapse = after;
|
||||
mCollapsePoint = collapsePoint;
|
||||
|
||||
startOrUpdateCollapseAnimation();
|
||||
startOrUpdatePathAnimation(false /* expanding */);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,40 +143,86 @@ public class ExpandedAnimationController
|
||||
mScreenWidth = mDisplaySize.x;
|
||||
}
|
||||
|
||||
private void startOrUpdateExpandAnimation() {
|
||||
animationsForChildrenFromIndex(
|
||||
0, /* startIndex */
|
||||
(index, animation) -> animation.position(getBubbleLeft(index), getExpandedY()))
|
||||
.startAll(() -> {
|
||||
mAnimatingExpand = false;
|
||||
/**
|
||||
* Animates the bubbles along a curved path, either to expand them along the top or collapse
|
||||
* them back into a stack.
|
||||
*/
|
||||
private void startOrUpdatePathAnimation(boolean expanding) {
|
||||
Runnable after;
|
||||
|
||||
if (mAfterExpand != null) {
|
||||
mAfterExpand.run();
|
||||
}
|
||||
if (expanding) {
|
||||
after = () -> {
|
||||
mAnimatingExpand = false;
|
||||
|
||||
mAfterExpand = null;
|
||||
});
|
||||
}
|
||||
if (mAfterExpand != null) {
|
||||
mAfterExpand.run();
|
||||
}
|
||||
|
||||
private void startOrUpdateCollapseAnimation() {
|
||||
// Stack to the left if we're going to the left, or right if not.
|
||||
final float sideMultiplier = mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x) ? -1 : 1;
|
||||
animationsForChildrenFromIndex(
|
||||
0, /* startIndex */
|
||||
(index, animation) -> {
|
||||
animation.position(
|
||||
mCollapsePoint.x + (sideMultiplier * index * mStackOffsetPx),
|
||||
mCollapsePoint.y);
|
||||
})
|
||||
.startAll(() -> {
|
||||
mAnimatingCollapse = false;
|
||||
mAfterExpand = null;
|
||||
};
|
||||
} else {
|
||||
after = () -> {
|
||||
mAnimatingCollapse = false;
|
||||
|
||||
if (mAfterCollapse != null) {
|
||||
mAfterCollapse.run();
|
||||
}
|
||||
if (mAfterCollapse != null) {
|
||||
mAfterCollapse.run();
|
||||
}
|
||||
|
||||
mAfterCollapse = null;
|
||||
});
|
||||
mAfterCollapse = null;
|
||||
};
|
||||
}
|
||||
|
||||
// Animate each bubble individually, since each path will end in a different spot.
|
||||
animationsForChildrenFromIndex(0, (index, animation) -> {
|
||||
final View bubble = mLayout.getChildAt(index);
|
||||
|
||||
// Start a path at the bubble's current position.
|
||||
final Path path = new Path();
|
||||
path.moveTo(bubble.getTranslationX(), bubble.getTranslationY());
|
||||
|
||||
final float expandedY = getExpandedY();
|
||||
if (expanding) {
|
||||
// If we're expanding, first draw a line from the bubble's current position to the
|
||||
// top of the screen.
|
||||
path.lineTo(bubble.getTranslationX(), expandedY);
|
||||
|
||||
// Then, draw a line across the screen to the bubble's resting position.
|
||||
path.lineTo(getBubbleLeft(index), expandedY);
|
||||
} else {
|
||||
final float sideMultiplier =
|
||||
mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x) ? -1 : 1;
|
||||
final float stackedX = mCollapsePoint.x + (sideMultiplier * index * mStackOffsetPx);
|
||||
|
||||
// If we're collapsing, draw a line from the bubble's current position to the side
|
||||
// of the screen where the bubble will be stacked.
|
||||
path.lineTo(stackedX, expandedY);
|
||||
|
||||
// Then, draw a line down to the stack position.
|
||||
path.lineTo(stackedX, mCollapsePoint.y);
|
||||
}
|
||||
|
||||
// The lead bubble should be the bubble with the longest distance to travel when we're
|
||||
// expanding, and the bubble with the shortest distance to travel when we're collapsing.
|
||||
// During expansion from the left side, the last bubble has to travel to the far right
|
||||
// side, so we have it lead and 'pull' the rest of the bubbles into place. From the
|
||||
// right side, the first bubble is traveling to the top left, so it leads. During
|
||||
// collapse to the left, the first bubble has the shortest travel time back to the stack
|
||||
// position, so it leads (and vice versa).
|
||||
final boolean firstBubbleLeads =
|
||||
(expanding && !mLayout.isFirstChildXLeftOfCenter(bubble.getTranslationX()))
|
||||
|| (!expanding && mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x));
|
||||
final int startDelay = firstBubbleLeads
|
||||
? (index * 10)
|
||||
: ((mLayout.getChildCount() - index) * 10);
|
||||
|
||||
animation
|
||||
.followAnimatedTargetAlongPath(
|
||||
path,
|
||||
EXPAND_COLLAPSE_TARGET_ANIM_DURATION /* targetAnimDuration */,
|
||||
Interpolators.LINEAR /* targetAnimInterpolator */)
|
||||
.withStartDelay(startDelay)
|
||||
.withStiffness(EXPAND_COLLAPSE_ANIM_STIFFNESS);
|
||||
}).startAll(after);
|
||||
}
|
||||
|
||||
/** Prepares the given bubble to be dragged out. */
|
||||
@@ -358,9 +406,9 @@ public class ExpandedAnimationController
|
||||
// If a bubble is added while the expand/collapse animations are playing, update the
|
||||
// animation to include the new bubble.
|
||||
if (mAnimatingExpand) {
|
||||
startOrUpdateExpandAnimation();
|
||||
startOrUpdatePathAnimation(true /* expanding */);
|
||||
} else if (mAnimatingCollapse) {
|
||||
startOrUpdateCollapseAnimation();
|
||||
startOrUpdatePathAnimation(false /* expanding */);
|
||||
} else {
|
||||
child.setTranslationX(getBubbleLeft(index));
|
||||
animationForChild(child)
|
||||
|
||||
@@ -16,7 +16,14 @@
|
||||
|
||||
package com.android.systemui.bubbles.animation;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.TimeInterpolator;
|
||||
import android.content.Context;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.PointF;
|
||||
import android.util.FloatProperty;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -232,7 +239,7 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
}
|
||||
|
||||
if (endActions != null) {
|
||||
mLayout.setEndActionForMultipleProperties(
|
||||
setEndActionForMultipleProperties(
|
||||
runAllEndActions,
|
||||
allAnimatedProperties.toArray(
|
||||
new DynamicAnimation.ViewProperty[0]));
|
||||
@@ -243,6 +250,44 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an end action that will be run when all child animations for a given property have
|
||||
* stopped running.
|
||||
*/
|
||||
protected void setEndActionForProperty(
|
||||
Runnable action, DynamicAnimation.ViewProperty property) {
|
||||
mLayout.mEndActionForProperty.put(property, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an end action that will be run when all child animations for all of the given
|
||||
* properties have stopped running.
|
||||
*/
|
||||
protected void setEndActionForMultipleProperties(
|
||||
Runnable action, DynamicAnimation.ViewProperty... properties) {
|
||||
final Runnable checkIfAllFinished = () -> {
|
||||
if (!mLayout.arePropertiesAnimating(properties)) {
|
||||
action.run();
|
||||
|
||||
for (DynamicAnimation.ViewProperty property : properties) {
|
||||
removeEndActionForProperty(property);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (DynamicAnimation.ViewProperty property : properties) {
|
||||
setEndActionForProperty(checkIfAllFinished, property);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the end listener that would have been called when all child animations for a
|
||||
* given property stopped running.
|
||||
*/
|
||||
protected void removeEndActionForProperty(DynamicAnimation.ViewProperty property) {
|
||||
mLayout.mEndActionForProperty.remove(property);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -275,43 +320,6 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an end action that will be run when all child animations for a given property have
|
||||
* stopped running.
|
||||
*/
|
||||
public void setEndActionForProperty(Runnable action, DynamicAnimation.ViewProperty property) {
|
||||
mEndActionForProperty.put(property, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an end action that will be run when all child animations for all of the given properties
|
||||
* have stopped running.
|
||||
*/
|
||||
public void setEndActionForMultipleProperties(
|
||||
Runnable action, DynamicAnimation.ViewProperty... properties) {
|
||||
final Runnable checkIfAllFinished = () -> {
|
||||
if (!arePropertiesAnimating(properties)) {
|
||||
action.run();
|
||||
|
||||
for (DynamicAnimation.ViewProperty property : properties) {
|
||||
removeEndActionForProperty(property);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (DynamicAnimation.ViewProperty property : properties) {
|
||||
setEndActionForProperty(checkIfAllFinished, property);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the end listener that would have been called when all child animations for a given
|
||||
* property stopped running.
|
||||
*/
|
||||
public void removeEndActionForProperty(DynamicAnimation.ViewProperty property) {
|
||||
mEndActionForProperty.remove(property);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addView(View child, int index, ViewGroup.LayoutParams params) {
|
||||
addViewInternal(child, index, params, false /* isReorder */);
|
||||
@@ -372,11 +380,22 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
/** Checks whether any animations of the given properties are running on the given view. */
|
||||
public boolean arePropertiesAnimatingOnView(
|
||||
View view, DynamicAnimation.ViewProperty... properties) {
|
||||
final ObjectAnimator targetAnimator = getTargetAnimatorFromView(view);
|
||||
for (DynamicAnimation.ViewProperty property : properties) {
|
||||
final SpringAnimation animation = getAnimationFromView(property, view);
|
||||
if (animation != null && animation.isRunning()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the target animator is running, its update listener will trigger the translation
|
||||
// physics animations at some point. We should consider the translation properties to be
|
||||
// be animating in this case, even if the physics animations haven't been started yet.
|
||||
final boolean isTranslation =
|
||||
property.equals(DynamicAnimation.TRANSLATION_X)
|
||||
|| property.equals(DynamicAnimation.TRANSLATION_Y);
|
||||
if (isTranslation && targetAnimator != null && targetAnimator.isRunning()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -400,6 +419,14 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
|
||||
/** Cancels all of the physics animations running on the given view. */
|
||||
public void cancelAnimationsOnView(View view) {
|
||||
// If present, cancel the target animator so it doesn't restart the translation physics
|
||||
// animations.
|
||||
final ObjectAnimator targetAnimator = getTargetAnimatorFromView(view);
|
||||
if (targetAnimator != null) {
|
||||
targetAnimator.cancel();
|
||||
}
|
||||
|
||||
// Cancel physics animations on the view.
|
||||
for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) {
|
||||
getAnimationFromView(property, view).cancel();
|
||||
}
|
||||
@@ -470,6 +497,11 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
return (SpringAnimation) view.getTag(getTagIdForProperty(property));
|
||||
}
|
||||
|
||||
/** Retrieves the target animator from the view via the view tag system. */
|
||||
@Nullable private ObjectAnimator getTargetAnimatorFromView(View view) {
|
||||
return (ObjectAnimator) view.getTag(R.id.target_animator_tag);
|
||||
}
|
||||
|
||||
/** Sets up SpringAnimations of the given property for each child view in the layout. */
|
||||
private void setUpAnimationsForProperty(DynamicAnimation.ViewProperty property) {
|
||||
for (int i = 0; i < getChildCount(); i++) {
|
||||
@@ -603,6 +635,46 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
/** The animation controller that last retrieved this animator instance. */
|
||||
private PhysicsAnimationController mAssociatedController;
|
||||
|
||||
/**
|
||||
* Animator used to traverse the path provided to {@link #followAnimatedTargetAlongPath}. As
|
||||
* the path is traversed, the view's translation spring animation final positions are
|
||||
* updated such that the view 'follows' the current position on the path.
|
||||
*/
|
||||
@Nullable private ObjectAnimator mPathAnimator;
|
||||
|
||||
/** Current position on the path. This is animated by {@link #mPathAnimator}. */
|
||||
private PointF mCurrentPointOnPath = new PointF();
|
||||
|
||||
/**
|
||||
* FloatProperty instances that can be passed to {@link ObjectAnimator} to animate the value
|
||||
* of {@link #mCurrentPointOnPath}.
|
||||
*/
|
||||
private final FloatProperty<PhysicsPropertyAnimator> mCurrentPointOnPathXProperty =
|
||||
new FloatProperty<PhysicsPropertyAnimator>("PathX") {
|
||||
@Override
|
||||
public void setValue(PhysicsPropertyAnimator object, float value) {
|
||||
mCurrentPointOnPath.x = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Float get(PhysicsPropertyAnimator object) {
|
||||
return mCurrentPointOnPath.x;
|
||||
}
|
||||
};
|
||||
|
||||
private final FloatProperty<PhysicsPropertyAnimator> mCurrentPointOnPathYProperty =
|
||||
new FloatProperty<PhysicsPropertyAnimator>("PathY") {
|
||||
@Override
|
||||
public void setValue(PhysicsPropertyAnimator object, float value) {
|
||||
mCurrentPointOnPath.y = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Float get(PhysicsPropertyAnimator object) {
|
||||
return mCurrentPointOnPath.y;
|
||||
}
|
||||
};
|
||||
|
||||
protected PhysicsPropertyAnimator(View view) {
|
||||
this.mView = view;
|
||||
}
|
||||
@@ -628,6 +700,7 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
|
||||
/** Animate the view's translationX value to the provided value. */
|
||||
public PhysicsPropertyAnimator translationX(float translationX, Runnable... endActions) {
|
||||
mPathAnimator = null; // We aren't using the path anymore if we're translating.
|
||||
return property(DynamicAnimation.TRANSLATION_X, translationX, endActions);
|
||||
}
|
||||
|
||||
@@ -640,6 +713,7 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
|
||||
/** Animate the view's translationY value to the provided value. */
|
||||
public PhysicsPropertyAnimator translationY(float translationY, Runnable... endActions) {
|
||||
mPathAnimator = null; // We aren't using the path anymore if we're translating.
|
||||
return property(DynamicAnimation.TRANSLATION_Y, translationY, endActions);
|
||||
}
|
||||
|
||||
@@ -661,6 +735,46 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
return translationY(translationY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Animates a 'target' point that moves along the given path, using the provided duration
|
||||
* and interpolator to animate the target. The view itself is animated using physics-based
|
||||
* animations, whose final positions are updated to the target position as it animates. This
|
||||
* results in the view 'following' the target in a realistic way.
|
||||
*
|
||||
* This method will override earlier calls to {@link #translationX}, {@link #translationY},
|
||||
* or {@link #position}, ultimately animating the view's position to the final point on the
|
||||
* given path.
|
||||
*
|
||||
* Any provided end listeners will be called when the physics-based animations kicked off by
|
||||
* the moving target have completed - not when the target animation completes.
|
||||
*/
|
||||
public PhysicsPropertyAnimator followAnimatedTargetAlongPath(
|
||||
Path path,
|
||||
int targetAnimDuration,
|
||||
TimeInterpolator targetAnimInterpolator,
|
||||
Runnable... endActions) {
|
||||
mPathAnimator = ObjectAnimator.ofFloat(
|
||||
this, mCurrentPointOnPathXProperty, mCurrentPointOnPathYProperty, path);
|
||||
mPathAnimator.setDuration(targetAnimDuration);
|
||||
mPathAnimator.setInterpolator(targetAnimInterpolator);
|
||||
|
||||
mPositionEndActions = endActions;
|
||||
|
||||
// Remove translation related values since we're going to ignore them and follow the
|
||||
// path instead.
|
||||
clearTranslationValues();
|
||||
return this;
|
||||
}
|
||||
|
||||
private void clearTranslationValues() {
|
||||
mAnimatedProperties.remove(DynamicAnimation.TRANSLATION_X);
|
||||
mAnimatedProperties.remove(DynamicAnimation.TRANSLATION_Y);
|
||||
mInitialPropertyValues.remove(DynamicAnimation.TRANSLATION_X);
|
||||
mInitialPropertyValues.remove(DynamicAnimation.TRANSLATION_Y);
|
||||
mEndActionForProperty.remove(DynamicAnimation.TRANSLATION_X);
|
||||
mEndActionForProperty.remove(DynamicAnimation.TRANSLATION_Y);
|
||||
}
|
||||
|
||||
/** Animate the view's scaleX value to the provided value. */
|
||||
public PhysicsPropertyAnimator scaleX(float scaleX, Runnable... endActions) {
|
||||
return property(DynamicAnimation.SCALE_X, scaleX, endActions);
|
||||
@@ -742,7 +856,7 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
if (after != null && after.length > 0) {
|
||||
final DynamicAnimation.ViewProperty[] propertiesArray =
|
||||
properties.toArray(new DynamicAnimation.ViewProperty[0]);
|
||||
setEndActionForMultipleProperties(() -> {
|
||||
mAssociatedController.setEndActionForMultipleProperties(() -> {
|
||||
for (Runnable callback : after) {
|
||||
callback.run();
|
||||
}
|
||||
@@ -774,8 +888,20 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
new Runnable[]{waitForBothXAndY});
|
||||
}
|
||||
|
||||
if (mPathAnimator != null) {
|
||||
startPathAnimation();
|
||||
}
|
||||
|
||||
// Actually start the animations.
|
||||
for (DynamicAnimation.ViewProperty property : properties) {
|
||||
// Don't start translation animations if we're using a path animator, the update
|
||||
// listeners added to that animator will take care of that.
|
||||
if (mPathAnimator != null
|
||||
&& (property.equals(DynamicAnimation.TRANSLATION_X)
|
||||
|| property.equals(DynamicAnimation.TRANSLATION_Y))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mInitialPropertyValues.containsKey(property)) {
|
||||
property.setValue(mView, mInitialPropertyValues.get(property));
|
||||
}
|
||||
@@ -797,7 +923,16 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
|
||||
/** Returns the set of properties that will animate once {@link #start} is called. */
|
||||
protected Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
|
||||
return mAnimatedProperties.keySet();
|
||||
final HashSet<DynamicAnimation.ViewProperty> animatedProperties = new HashSet<>(
|
||||
mAnimatedProperties.keySet());
|
||||
|
||||
// If we're using a path animator, it'll kick off translation animations.
|
||||
if (mPathAnimator != null) {
|
||||
animatedProperties.add(DynamicAnimation.TRANSLATION_X);
|
||||
animatedProperties.add(DynamicAnimation.TRANSLATION_Y);
|
||||
}
|
||||
|
||||
return animatedProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -812,7 +947,7 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
long startDelay,
|
||||
float stiffness,
|
||||
float dampingRatio,
|
||||
Runnable[] afterCallbacks) {
|
||||
Runnable... afterCallbacks) {
|
||||
if (view != null) {
|
||||
final SpringAnimation animation =
|
||||
(SpringAnimation) view.getTag(getTagIdForProperty(property));
|
||||
@@ -855,6 +990,92 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the final position of a view's animation, without changing any of the animation's
|
||||
* other settings. Calling this before an initial call to {@link #animateValueForChild} will
|
||||
* work, but result in unknown values for stiffness, etc. and is not recommended.
|
||||
*/
|
||||
private void updateValueForChild(
|
||||
DynamicAnimation.ViewProperty property, View view, float position) {
|
||||
if (view != null) {
|
||||
final SpringAnimation animation =
|
||||
(SpringAnimation) view.getTag(getTagIdForProperty(property));
|
||||
final SpringForce animationSpring = animation.getSpring();
|
||||
|
||||
if (animationSpring == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
animationSpring.setFinalPosition(position);
|
||||
animation.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the path animator to respect the settings passed into the animation builder
|
||||
* and adds update listeners that update the translation physics animations. Then, starts
|
||||
* the path animation.
|
||||
*/
|
||||
protected void startPathAnimation() {
|
||||
final SpringForce defaultSpringForceX = mController.getSpringForce(
|
||||
DynamicAnimation.TRANSLATION_X, mView);
|
||||
final SpringForce defaultSpringForceY = mController.getSpringForce(
|
||||
DynamicAnimation.TRANSLATION_Y, mView);
|
||||
|
||||
if (mStartDelay > 0) {
|
||||
mPathAnimator.setStartDelay(mStartDelay);
|
||||
}
|
||||
|
||||
final Runnable updatePhysicsAnims = () -> {
|
||||
updateValueForChild(
|
||||
DynamicAnimation.TRANSLATION_X, mView, mCurrentPointOnPath.x);
|
||||
updateValueForChild(
|
||||
DynamicAnimation.TRANSLATION_Y, mView, mCurrentPointOnPath.y);
|
||||
};
|
||||
|
||||
mPathAnimator.addUpdateListener(pathAnim -> updatePhysicsAnims.run());
|
||||
mPathAnimator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animation) {
|
||||
animateValueForChild(
|
||||
DynamicAnimation.TRANSLATION_X,
|
||||
mView,
|
||||
mCurrentPointOnPath.x,
|
||||
mDefaultStartVelocity,
|
||||
0 /* startDelay */,
|
||||
mStiffness >= 0 ? mStiffness : defaultSpringForceX.getStiffness(),
|
||||
mDampingRatio >= 0
|
||||
? mDampingRatio
|
||||
: defaultSpringForceX.getDampingRatio());
|
||||
|
||||
animateValueForChild(
|
||||
DynamicAnimation.TRANSLATION_Y,
|
||||
mView,
|
||||
mCurrentPointOnPath.y,
|
||||
mDefaultStartVelocity,
|
||||
0 /* startDelay */,
|
||||
mStiffness >= 0 ? mStiffness : defaultSpringForceY.getStiffness(),
|
||||
mDampingRatio >= 0
|
||||
? mDampingRatio
|
||||
: defaultSpringForceY.getDampingRatio());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
updatePhysicsAnims.run();
|
||||
}
|
||||
});
|
||||
|
||||
// If there's a target animator saved for the view, make sure it's not running.
|
||||
final ObjectAnimator targetAnimator = getTargetAnimatorFromView(mView);
|
||||
if (targetAnimator != null) {
|
||||
targetAnimator.cancel();
|
||||
}
|
||||
|
||||
mView.setTag(R.id.target_animator_tag, mPathAnimator);
|
||||
mPathAnimator.start();
|
||||
}
|
||||
|
||||
private void clearAnimator() {
|
||||
mInitialPropertyValues.clear();
|
||||
mAnimatedProperties.clear();
|
||||
@@ -864,6 +1085,7 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
mStiffness = -1;
|
||||
mDampingRatio = -1;
|
||||
mEndActionsForProperty.clear();
|
||||
mPathAnimator = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -199,18 +199,18 @@ public class StackAnimationController extends
|
||||
*/
|
||||
public void springStack(float destinationX, float destinationY) {
|
||||
springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X,
|
||||
new SpringForce()
|
||||
new SpringForce()
|
||||
.setStiffness(SPRING_AFTER_FLING_STIFFNESS)
|
||||
.setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
|
||||
0 /* startXVelocity */,
|
||||
destinationX);
|
||||
0 /* startXVelocity */,
|
||||
destinationX);
|
||||
|
||||
springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y,
|
||||
new SpringForce()
|
||||
new SpringForce()
|
||||
.setStiffness(SPRING_AFTER_FLING_STIFFNESS)
|
||||
.setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
|
||||
0 /* startYVelocity */,
|
||||
destinationY);
|
||||
0 /* startYVelocity */,
|
||||
destinationY);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -264,12 +264,12 @@ public class StackAnimationController extends
|
||||
.setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
|
||||
/* destination */ null);
|
||||
|
||||
mLayout.setEndActionForMultipleProperties(
|
||||
setEndActionForMultipleProperties(
|
||||
() -> {
|
||||
mRestingStackPosition = new PointF();
|
||||
mRestingStackPosition.set(mStackPosition);
|
||||
mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_X);
|
||||
mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_Y);
|
||||
removeEndActionForProperty(DynamicAnimation.TRANSLATION_X);
|
||||
removeEndActionForProperty(DynamicAnimation.TRANSLATION_Y);
|
||||
},
|
||||
DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
|
||||
|
||||
@@ -319,7 +319,7 @@ public class StackAnimationController extends
|
||||
SpringForce spring,
|
||||
Float finalPosition) {
|
||||
Log.d(TAG, String.format("Flinging %s.",
|
||||
PhysicsAnimationLayout.getReadablePropertyName(property)));
|
||||
PhysicsAnimationLayout.getReadablePropertyName(property)));
|
||||
|
||||
StackPositionProperty firstBubbleProperty = new StackPositionProperty(property);
|
||||
final float currentValue = firstBubbleProperty.getValue(this);
|
||||
@@ -370,8 +370,8 @@ public class StackAnimationController extends
|
||||
cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_X);
|
||||
cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_Y);
|
||||
|
||||
mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_X);
|
||||
mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_Y);
|
||||
removeEndActionForProperty(DynamicAnimation.TRANSLATION_X);
|
||||
removeEndActionForProperty(DynamicAnimation.TRANSLATION_Y);
|
||||
}
|
||||
|
||||
/** Save the current IME height so that we know where the stack bounds should be. */
|
||||
|
||||
@@ -156,8 +156,8 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase {
|
||||
});
|
||||
|
||||
// Set end listeners for both x and y.
|
||||
mLayout.setEndActionForProperty(xEndAction, DynamicAnimation.TRANSLATION_X);
|
||||
mLayout.setEndActionForProperty(yEndAction, DynamicAnimation.TRANSLATION_Y);
|
||||
mTestableController.setEndActionForProperty(xEndAction, DynamicAnimation.TRANSLATION_X);
|
||||
mTestableController.setEndActionForProperty(yEndAction, DynamicAnimation.TRANSLATION_Y);
|
||||
|
||||
// Animate x, and wait for it to finish.
|
||||
mTestableController.animationForChildAtIndex(0)
|
||||
@@ -190,7 +190,7 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase {
|
||||
});
|
||||
|
||||
// Set the end listener.
|
||||
mLayout.setEndActionForProperty(xEndListener, DynamicAnimation.TRANSLATION_X);
|
||||
mTestableController.setEndActionForProperty(xEndListener, DynamicAnimation.TRANSLATION_X);
|
||||
|
||||
// Animate x, and wait for it to finish.
|
||||
mTestableController.animationForChildAtIndex(0)
|
||||
@@ -205,7 +205,7 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase {
|
||||
mTestableController.animationForChildAtIndex(0)
|
||||
.translationX(1000)
|
||||
.start();
|
||||
mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_X);
|
||||
mTestableController.removeEndActionForProperty(DynamicAnimation.TRANSLATION_X);
|
||||
xLatch.await(1, TimeUnit.SECONDS);
|
||||
|
||||
// Make sure the end listener was not called.
|
||||
|
||||
@@ -195,14 +195,13 @@ public class PhysicsAnimationLayoutTestCase extends SysuiTestCase {
|
||||
private void setTestEndActionForProperty(
|
||||
Runnable action, DynamicAnimation.ViewProperty property) {
|
||||
final Runnable realEndAction = mEndActionForProperty.get(property);
|
||||
|
||||
setEndActionForProperty(() -> {
|
||||
mLayout.mEndActionForProperty.put(property, () -> {
|
||||
if (realEndAction != null) {
|
||||
realEndAction.run();
|
||||
}
|
||||
|
||||
action.run();
|
||||
}, property);
|
||||
});
|
||||
}
|
||||
|
||||
/** PhysicsPropertyAnimator that posts its animations to the main thread. */
|
||||
@@ -219,6 +218,11 @@ public class PhysicsAnimationLayoutTestCase extends SysuiTestCase {
|
||||
property, view, value, startVel, startDelay, stiffness, dampingRatio,
|
||||
afterCallbacks));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startPathAnimation() {
|
||||
mMainThreadHandler.post(super::startPathAnimation);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user