/*
|
* Copyright (C) 2019 The Android Open Source Project
|
*
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
* you may not use this file except in compliance with the License.
|
* You may obtain a copy of the License at
|
*
|
* http://www.apache.org/licenses/LICENSE-2.0
|
*
|
* Unless required by applicable law or agreed to in writing, software
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* See the License for the specific language governing permissions and
|
* limitations under the License.
|
*/
|
|
package com.android.systemui.bubbles.animation;
|
|
import android.content.res.Resources;
|
import android.graphics.PointF;
|
import android.graphics.RectF;
|
import android.util.Log;
|
import android.view.View;
|
import android.view.WindowInsets;
|
|
import androidx.dynamicanimation.animation.DynamicAnimation;
|
import androidx.dynamicanimation.animation.FlingAnimation;
|
import androidx.dynamicanimation.animation.FloatPropertyCompat;
|
import androidx.dynamicanimation.animation.SpringAnimation;
|
import androidx.dynamicanimation.animation.SpringForce;
|
|
import com.android.systemui.R;
|
|
import com.google.android.collect.Sets;
|
|
import java.util.HashMap;
|
import java.util.Set;
|
|
/**
|
* Animation controller for bubbles when they're in their stacked state. Stacked bubbles sit atop
|
* each other with a slight offset to the left or right (depending on which side of the screen they
|
* are on). Bubbles 'follow' each other when dragged, and can be flung to the left or right sides of
|
* the screen.
|
*/
|
public class StackAnimationController extends
|
PhysicsAnimationLayout.PhysicsAnimationController {
|
|
private static final String TAG = "Bubbs.StackCtrl";
|
|
/** Scale factor to use initially for new bubbles being animated in. */
|
private static final float ANIMATE_IN_STARTING_SCALE = 1.15f;
|
|
/** Translation factor (multiplied by stack offset) to use for bubbles being animated in/out. */
|
private static final int ANIMATE_TRANSLATION_FACTOR = 4;
|
|
/**
|
* Values to use for the default {@link SpringForce} provided to the physics animation layout.
|
*/
|
private static final int DEFAULT_STIFFNESS = 12000;
|
private static final int FLING_FOLLOW_STIFFNESS = 20000;
|
private static final float DEFAULT_BOUNCINESS = 0.9f;
|
|
/**
|
* Friction applied to fling animations. Since the stack must land on one of the sides of the
|
* screen, we want less friction horizontally so that the stack has a better chance of making it
|
* to the side without needing a spring.
|
*/
|
private static final float FLING_FRICTION_X = 2.2f;
|
private static final float FLING_FRICTION_Y = 2.2f;
|
|
/**
|
* Values to use for the stack spring animation used to spring the stack to its final position
|
* after a fling.
|
*/
|
private static final int SPRING_AFTER_FLING_STIFFNESS = 750;
|
private static final float SPRING_AFTER_FLING_DAMPING_RATIO = 0.85f;
|
|
/**
|
* Minimum fling velocity required to trigger moving the stack from one side of the screen to
|
* the other.
|
*/
|
private static final float ESCAPE_VELOCITY = 750f;
|
|
/**
|
* The canonical position of the stack. This is typically the position of the first bubble, but
|
* we need to keep track of it separately from the first bubble's translation in case there are
|
* no bubbles, or the first bubble was just added and being animated to its new position.
|
*/
|
private PointF mStackPosition = new PointF(-1, -1);
|
|
/** Whether or not the stack's start position has been set. */
|
private boolean mStackMovedToStartPosition = false;
|
|
/** The most recent position in which the stack was resting on the edge of the screen. */
|
private PointF mRestingStackPosition;
|
|
/** The height of the most recently visible IME. */
|
private float mImeHeight = 0f;
|
|
/**
|
* The Y position of the stack before the IME became visible, or {@link Float#MIN_VALUE} if the
|
* IME is not visible or the user moved the stack since the IME became visible.
|
*/
|
private float mPreImeY = Float.MIN_VALUE;
|
|
/**
|
* Animations on the stack position itself, which would have been started in
|
* {@link #flingThenSpringFirstBubbleWithStackFollowing}. These animations dispatch to
|
* {@link #moveFirstBubbleWithStackFollowing} to move the entire stack (with 'following' effect)
|
* to a legal position on the side of the screen.
|
*/
|
private HashMap<DynamicAnimation.ViewProperty, DynamicAnimation> mStackPositionAnimations =
|
new HashMap<>();
|
|
/**
|
* Whether the current motion of the stack is due to a fling animation (vs. being dragged
|
* manually).
|
*/
|
private boolean mIsMovingFromFlinging = false;
|
|
/**
|
* Whether the stack is within the dismiss target (either by being dragged, magnet'd, or flung).
|
*/
|
private boolean mWithinDismissTarget = false;
|
|
/**
|
* Whether the first bubble is springing towards the touch point, rather than using the default
|
* behavior of moving directly to the touch point with the rest of the stack following it.
|
*
|
* This happens when the user's finger exits the dismiss area while the stack is magnetized to
|
* the center. Since the touch point differs from the stack location, we need to animate the
|
* stack back to the touch point to avoid a jarring instant location change from the center of
|
* the target to the touch point just outside the target bounds.
|
*
|
* This is reset once the spring animations end, since that means the first bubble has
|
* successfully 'caught up' to the touch.
|
*/
|
private boolean mFirstBubbleSpringingToTouch = false;
|
|
/** Horizontal offset of bubbles in the stack. */
|
private float mStackOffset;
|
/** Diameter of the bubbles themselves. */
|
private int mIndividualBubbleSize;
|
/**
|
* The amount of space to add between the bubbles and certain UI elements, such as the top of
|
* the screen or the IME. This does not apply to the left/right sides of the screen since the
|
* stack goes offscreen intentionally.
|
*/
|
private int mBubblePadding;
|
/** How far offscreen the stack rests. */
|
private int mBubbleOffscreen;
|
/** How far down the screen the stack starts, when there is no pre-existing location. */
|
private int mStackStartingVerticalOffset;
|
/** Height of the status bar. */
|
private float mStatusBarHeight;
|
|
/**
|
* Instantly move the first bubble to the given point, and animate the rest of the stack behind
|
* it with the 'following' effect.
|
*/
|
public void moveFirstBubbleWithStackFollowing(float x, float y) {
|
// If we manually move the bubbles with the IME open, clear the return point since we don't
|
// want the stack to snap away from the new position.
|
mPreImeY = Float.MIN_VALUE;
|
|
moveFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X, x);
|
moveFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y, y);
|
|
// This method is called when the stack is being dragged manually, so we're clearly no
|
// longer flinging.
|
mIsMovingFromFlinging = false;
|
}
|
|
/**
|
* The position of the stack - typically the position of the first bubble; if no bubbles have
|
* been added yet, it will be where the first bubble will go when added.
|
*/
|
public PointF getStackPosition() {
|
return mStackPosition;
|
}
|
|
/** Whether the stack is on the left side of the screen. */
|
public boolean isStackOnLeftSide() {
|
if (mLayout == null || !isStackPositionSet()) {
|
return false;
|
}
|
|
float stackCenter = mStackPosition.x + mIndividualBubbleSize / 2;
|
float screenCenter = mLayout.getWidth() / 2;
|
return stackCenter < screenCenter;
|
}
|
|
/**
|
* Fling stack to given corner, within allowable screen bounds.
|
* Note that we need new SpringForce instances per animation despite identical configs because
|
* SpringAnimation uses SpringForce's internal (changing) velocity while the animation runs.
|
*/
|
public void springStack(float destinationX, float destinationY) {
|
springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X,
|
new SpringForce()
|
.setStiffness(SPRING_AFTER_FLING_STIFFNESS)
|
.setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
|
0 /* startXVelocity */,
|
destinationX);
|
|
springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y,
|
new SpringForce()
|
.setStiffness(SPRING_AFTER_FLING_STIFFNESS)
|
.setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
|
0 /* startYVelocity */,
|
destinationY);
|
}
|
|
/**
|
* Flings the stack starting with the given velocities, springing it to the nearest edge
|
* afterward.
|
*
|
* @return The X value that the stack will end up at after the fling/spring.
|
*/
|
public float flingStackThenSpringToEdge(float x, float velX, float velY) {
|
final boolean stackOnLeftSide = x - mIndividualBubbleSize / 2 < mLayout.getWidth() / 2;
|
|
final boolean stackShouldFlingLeft = stackOnLeftSide
|
? velX < ESCAPE_VELOCITY
|
: velX < -ESCAPE_VELOCITY;
|
|
final RectF stackBounds = getAllowableStackPositionRegion();
|
|
// Target X translation (either the left or right side of the screen).
|
final float destinationRelativeX = stackShouldFlingLeft
|
? stackBounds.left : stackBounds.right;
|
|
// Minimum velocity required for the stack to make it to the targeted side of the screen,
|
// taking friction into account (4.2f is the number that friction scalars are multiplied by
|
// in DynamicAnimation.DragForce). This is an estimate - it could possibly be slightly off,
|
// but the SpringAnimation at the end will ensure that it reaches the destination X
|
// regardless.
|
final float minimumVelocityToReachEdge =
|
(destinationRelativeX - x) * (FLING_FRICTION_X * 4.2f);
|
|
// Use the touch event's velocity if it's sufficient, otherwise use the minimum velocity so
|
// that it'll make it all the way to the side of the screen.
|
final float startXVelocity = stackShouldFlingLeft
|
? Math.min(minimumVelocityToReachEdge, velX)
|
: Math.max(minimumVelocityToReachEdge, velX);
|
|
flingThenSpringFirstBubbleWithStackFollowing(
|
DynamicAnimation.TRANSLATION_X,
|
startXVelocity,
|
FLING_FRICTION_X,
|
new SpringForce()
|
.setStiffness(SPRING_AFTER_FLING_STIFFNESS)
|
.setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
|
destinationRelativeX);
|
|
flingThenSpringFirstBubbleWithStackFollowing(
|
DynamicAnimation.TRANSLATION_Y,
|
velY,
|
FLING_FRICTION_Y,
|
new SpringForce()
|
.setStiffness(SPRING_AFTER_FLING_STIFFNESS)
|
.setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
|
/* destination */ null);
|
|
mLayout.setEndActionForMultipleProperties(
|
() -> {
|
mRestingStackPosition = new PointF();
|
mRestingStackPosition.set(mStackPosition);
|
mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_X);
|
mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_Y);
|
},
|
DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
|
|
// If we're flinging now, there's no more touch event to catch up to.
|
mFirstBubbleSpringingToTouch = false;
|
mIsMovingFromFlinging = true;
|
return destinationRelativeX;
|
}
|
|
/**
|
* Where the stack would be if it were snapped to the nearest horizontal edge (left or right).
|
*/
|
public PointF getStackPositionAlongNearestHorizontalEdge() {
|
final PointF stackPos = getStackPosition();
|
final boolean onLeft = mLayout.isFirstChildXLeftOfCenter(stackPos.x);
|
final RectF bounds = getAllowableStackPositionRegion();
|
|
stackPos.x = onLeft ? bounds.left : bounds.right;
|
return stackPos;
|
}
|
|
/**
|
* Moves the stack in response to rotation. We keep it in the most similar position by keeping
|
* it on the same side, and positioning it the same percentage of the way down the screen
|
* (taking status bar/nav bar into account by using the allowable region's height).
|
*/
|
public void moveStackToSimilarPositionAfterRotation(boolean wasOnLeft, float verticalPercent) {
|
final RectF allowablePos = getAllowableStackPositionRegion();
|
final float allowableRegionHeight = allowablePos.bottom - allowablePos.top;
|
|
final float x = wasOnLeft ? allowablePos.left : allowablePos.right;
|
final float y = (allowableRegionHeight * verticalPercent) + allowablePos.top;
|
|
setStackPosition(new PointF(x, y));
|
}
|
|
/**
|
* Flings the first bubble along the given property's axis, using the provided configuration
|
* values. When the animation ends - either by hitting the min/max, or by friction sufficiently
|
* reducing momentum - a SpringAnimation takes over to snap the bubble to the given final
|
* position.
|
*/
|
protected void flingThenSpringFirstBubbleWithStackFollowing(
|
DynamicAnimation.ViewProperty property,
|
float vel,
|
float friction,
|
SpringForce spring,
|
Float finalPosition) {
|
Log.d(TAG, String.format("Flinging %s.",
|
PhysicsAnimationLayout.getReadablePropertyName(property)));
|
|
StackPositionProperty firstBubbleProperty = new StackPositionProperty(property);
|
final float currentValue = firstBubbleProperty.getValue(this);
|
final RectF bounds = getAllowableStackPositionRegion();
|
final float min =
|
property.equals(DynamicAnimation.TRANSLATION_X)
|
? bounds.left
|
: bounds.top;
|
final float max =
|
property.equals(DynamicAnimation.TRANSLATION_X)
|
? bounds.right
|
: bounds.bottom;
|
|
FlingAnimation flingAnimation = new FlingAnimation(this, firstBubbleProperty);
|
flingAnimation.setFriction(friction)
|
.setStartVelocity(vel)
|
|
// If the bubble's property value starts beyond the desired min/max, use that value
|
// instead so that the animation won't immediately end. If, for example, the user
|
// drags the bubbles into the navigation bar, but then flings them upward, we want
|
// the fling to occur despite temporarily having a value outside of the min/max. If
|
// the bubbles are out of bounds and flung even farther out of bounds, the fling
|
// animation will halt immediately and the SpringAnimation will take over, springing
|
// it in reverse to the (legal) final position.
|
.setMinValue(Math.min(currentValue, min))
|
.setMaxValue(Math.max(currentValue, max))
|
|
.addEndListener((animation, canceled, endValue, endVelocity) -> {
|
if (!canceled) {
|
springFirstBubbleWithStackFollowing(property, spring, endVelocity,
|
finalPosition != null
|
? finalPosition
|
: Math.max(min, Math.min(max, endValue)));
|
}
|
});
|
|
cancelStackPositionAnimation(property);
|
mStackPositionAnimations.put(property, flingAnimation);
|
flingAnimation.start();
|
}
|
|
/**
|
* Cancel any stack position animations that were started by calling
|
* @link #flingThenSpringFirstBubbleWithStackFollowing}, and remove any corresponding end
|
* listeners.
|
*/
|
public void cancelStackPositionAnimations() {
|
cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_X);
|
cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_Y);
|
|
mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_X);
|
mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_Y);
|
}
|
|
/** Save the current IME height so that we know where the stack bounds should be. */
|
public void setImeHeight(int imeHeight) {
|
mImeHeight = imeHeight;
|
}
|
|
/**
|
* Animates the stack either away from the newly visible IME, or back to its original position
|
* due to the IME going away.
|
*/
|
public void animateForImeVisibility(boolean imeVisible) {
|
final float maxBubbleY = getAllowableStackPositionRegion().bottom;
|
float destinationY = Float.MIN_VALUE;
|
|
if (imeVisible) {
|
// Stack is lower than it should be and overlaps the now-visible IME.
|
if (mStackPosition.y > maxBubbleY && mPreImeY == Float.MIN_VALUE) {
|
mPreImeY = mStackPosition.y;
|
destinationY = maxBubbleY;
|
}
|
} else {
|
if (mPreImeY > Float.MIN_VALUE) {
|
destinationY = mPreImeY;
|
mPreImeY = Float.MIN_VALUE;
|
}
|
}
|
|
if (destinationY > Float.MIN_VALUE) {
|
springFirstBubbleWithStackFollowing(
|
DynamicAnimation.TRANSLATION_Y,
|
getSpringForce(DynamicAnimation.TRANSLATION_Y, /* view */ null)
|
.setStiffness(SpringForce.STIFFNESS_LOW),
|
/* startVel */ 0f,
|
destinationY);
|
}
|
}
|
|
/**
|
* Returns the region within which the stack is allowed to rest. This goes slightly off the left
|
* and right sides of the screen, below the status bar/cutout and above the navigation bar.
|
* While the stack is not allowed to rest outside of these bounds, it can temporarily be
|
* animated or dragged beyond them.
|
*/
|
public RectF getAllowableStackPositionRegion() {
|
final WindowInsets insets = mLayout.getRootWindowInsets();
|
final RectF allowableRegion = new RectF();
|
if (insets != null) {
|
allowableRegion.left =
|
-mBubbleOffscreen
|
+ Math.max(
|
insets.getSystemWindowInsetLeft(),
|
insets.getDisplayCutout() != null
|
? insets.getDisplayCutout().getSafeInsetLeft()
|
: 0);
|
allowableRegion.right =
|
mLayout.getWidth()
|
- mIndividualBubbleSize
|
+ mBubbleOffscreen
|
- Math.max(
|
insets.getSystemWindowInsetRight(),
|
insets.getDisplayCutout() != null
|
? insets.getDisplayCutout().getSafeInsetRight()
|
: 0);
|
|
allowableRegion.top =
|
mBubblePadding
|
+ Math.max(
|
mStatusBarHeight,
|
insets.getDisplayCutout() != null
|
? insets.getDisplayCutout().getSafeInsetTop()
|
: 0);
|
allowableRegion.bottom =
|
mLayout.getHeight()
|
- mIndividualBubbleSize
|
- mBubblePadding
|
- (mImeHeight > Float.MIN_VALUE ? mImeHeight + mBubblePadding : 0f)
|
- Math.max(
|
insets.getSystemWindowInsetBottom(),
|
insets.getDisplayCutout() != null
|
? insets.getDisplayCutout().getSafeInsetBottom()
|
: 0);
|
}
|
|
return allowableRegion;
|
}
|
|
/** Moves the stack in response to a touch event. */
|
public void moveStackFromTouch(float x, float y) {
|
|
// If we're springing to the touch point to 'catch up' after dragging out of the dismiss
|
// target, then update the stack position animations instead of moving the bubble directly.
|
if (mFirstBubbleSpringingToTouch) {
|
final SpringAnimation springToTouchX =
|
(SpringAnimation) mStackPositionAnimations.get(DynamicAnimation.TRANSLATION_X);
|
final SpringAnimation springToTouchY =
|
(SpringAnimation) mStackPositionAnimations.get(DynamicAnimation.TRANSLATION_Y);
|
|
// If either animation is still running, we haven't caught up. Update the animations.
|
if (springToTouchX.isRunning() || springToTouchY.isRunning()) {
|
springToTouchX.animateToFinalPosition(x);
|
springToTouchY.animateToFinalPosition(y);
|
} else {
|
// If the animations have finished, the stack is now at the touch point. We can
|
// resume moving the bubble directly.
|
mFirstBubbleSpringingToTouch = false;
|
}
|
}
|
|
if (!mFirstBubbleSpringingToTouch && !mWithinDismissTarget) {
|
moveFirstBubbleWithStackFollowing(x, y);
|
}
|
}
|
|
/**
|
* Demagnetizes the stack, springing it towards the given point. This also sets flags so that
|
* subsequent touch events will update the final position of the demagnetization spring instead
|
* of directly moving the bubbles, until demagnetization is complete.
|
*/
|
public void demagnetizeFromDismissToPoint(float x, float y, float velX, float velY) {
|
mWithinDismissTarget = false;
|
mFirstBubbleSpringingToTouch = true;
|
|
springFirstBubbleWithStackFollowing(
|
DynamicAnimation.TRANSLATION_X,
|
new SpringForce()
|
.setDampingRatio(DEFAULT_BOUNCINESS)
|
.setStiffness(DEFAULT_STIFFNESS),
|
velX, x);
|
|
springFirstBubbleWithStackFollowing(
|
DynamicAnimation.TRANSLATION_Y,
|
new SpringForce()
|
.setDampingRatio(DEFAULT_BOUNCINESS)
|
.setStiffness(DEFAULT_STIFFNESS),
|
velY, y);
|
}
|
|
/**
|
* Spring the stack towards the dismiss target, respecting existing velocity. This also sets
|
* flags so that subsequent touch events will not move the stack until it's demagnetized.
|
*/
|
public void magnetToDismiss(float velX, float velY, float destY, Runnable after) {
|
mWithinDismissTarget = true;
|
mFirstBubbleSpringingToTouch = false;
|
|
animationForChildAtIndex(0)
|
.translationX(mLayout.getWidth() / 2f - mIndividualBubbleSize / 2f)
|
.translationY(destY, after)
|
.withPositionStartVelocities(velX, velY)
|
.withStiffness(SpringForce.STIFFNESS_MEDIUM)
|
.withDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
|
.start();
|
}
|
|
/**
|
* 'Implode' the stack by shrinking the bubbles via chained animations and fading them out.
|
*/
|
public void implodeStack(Runnable after) {
|
// Pop and fade the bubbles sequentially.
|
animationForChildAtIndex(0)
|
.scaleX(0.5f)
|
.scaleY(0.5f)
|
.alpha(0f)
|
.withDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
|
.withStiffness(SpringForce.STIFFNESS_HIGH)
|
.start(() -> {
|
// Run the callback and reset flags. The child translation animations might
|
// still be running, but that's fine. Once the alpha is at 0f they're no longer
|
// visible anyway.
|
after.run();
|
mWithinDismissTarget = false;
|
});
|
}
|
|
/**
|
* Springs the first bubble to the given final position, with the rest of the stack 'following'.
|
*/
|
protected void springFirstBubbleWithStackFollowing(
|
DynamicAnimation.ViewProperty property, SpringForce spring,
|
float vel, float finalPosition) {
|
|
if (mLayout.getChildCount() == 0) {
|
return;
|
}
|
|
Log.d(TAG, String.format("Springing %s to final position %f.",
|
PhysicsAnimationLayout.getReadablePropertyName(property),
|
finalPosition));
|
|
StackPositionProperty firstBubbleProperty = new StackPositionProperty(property);
|
SpringAnimation springAnimation =
|
new SpringAnimation(this, firstBubbleProperty)
|
.setSpring(spring)
|
.setStartVelocity(vel);
|
|
cancelStackPositionAnimation(property);
|
mStackPositionAnimations.put(property, springAnimation);
|
springAnimation.animateToFinalPosition(finalPosition);
|
}
|
|
@Override
|
Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
|
return Sets.newHashSet(
|
DynamicAnimation.TRANSLATION_X, // For positioning.
|
DynamicAnimation.TRANSLATION_Y,
|
DynamicAnimation.ALPHA, // For fading in new bubbles.
|
DynamicAnimation.SCALE_X, // For 'popping in' new bubbles.
|
DynamicAnimation.SCALE_Y);
|
}
|
|
@Override
|
int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) {
|
if (property.equals(DynamicAnimation.TRANSLATION_X)
|
|| property.equals(DynamicAnimation.TRANSLATION_Y)) {
|
return index + 1;
|
} else if (mWithinDismissTarget) {
|
return index + 1; // Chain all animations in dismiss (scale, alpha, etc. are used).
|
} else {
|
return NONE;
|
}
|
}
|
|
|
@Override
|
float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) {
|
if (property.equals(DynamicAnimation.TRANSLATION_X)) {
|
// If we're in the dismiss target, have the bubbles pile on top of each other with no
|
// offset.
|
if (mWithinDismissTarget) {
|
return 0f;
|
} else {
|
// Offset to the left if we're on the left, or the right otherwise.
|
return mLayout.isFirstChildXLeftOfCenter(mStackPosition.x)
|
? -mStackOffset : mStackOffset;
|
}
|
} else {
|
return 0f;
|
}
|
}
|
|
@Override
|
SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) {
|
return new SpringForce()
|
.setDampingRatio(DEFAULT_BOUNCINESS)
|
.setStiffness(mIsMovingFromFlinging ? FLING_FOLLOW_STIFFNESS : DEFAULT_STIFFNESS);
|
}
|
|
@Override
|
void onChildAdded(View child, int index) {
|
if (mLayout.getChildCount() == 1) {
|
// If this is the first child added, position the stack in its starting position.
|
moveStackToStartPosition();
|
} else if (isStackPositionSet() && mLayout.indexOfChild(child) == 0) {
|
// Otherwise, animate the bubble in if it's the newest bubble. If we're adding a bubble
|
// to the back of the stack, it'll be largely invisible so don't bother animating it in.
|
animateInBubble(child);
|
}
|
}
|
|
@Override
|
void onChildRemoved(View child, int index, Runnable finishRemoval) {
|
// Animate the removing view in the opposite direction of the stack.
|
final float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
|
animationForChild(child)
|
.alpha(0f, finishRemoval /* after */)
|
.scaleX(ANIMATE_IN_STARTING_SCALE)
|
.scaleY(ANIMATE_IN_STARTING_SCALE)
|
.translationX(mStackPosition.x - (-xOffset * ANIMATE_TRANSLATION_FACTOR))
|
.start();
|
|
if (mLayout.getChildCount() > 0) {
|
animationForChildAtIndex(0).translationX(mStackPosition.x).start();
|
} else {
|
// Set the start position back to the default since we're out of bubbles. New bubbles
|
// will then animate in from the start position.
|
mStackPosition = getDefaultStartPosition();
|
}
|
}
|
|
@Override
|
void onChildReordered(View child, int oldIndex, int newIndex) {}
|
|
@Override
|
void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
|
Resources res = layout.getResources();
|
mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
|
mIndividualBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
|
mBubblePadding = res.getDimensionPixelSize(R.dimen.bubble_padding);
|
mBubbleOffscreen = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen);
|
mStackStartingVerticalOffset =
|
res.getDimensionPixelSize(R.dimen.bubble_stack_starting_offset_y);
|
mStatusBarHeight =
|
res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
|
}
|
|
/** Moves the stack, without any animation, to the starting position. */
|
private void moveStackToStartPosition() {
|
// Post to ensure that the layout's width and height have been calculated.
|
mLayout.setVisibility(View.INVISIBLE);
|
mLayout.post(() -> {
|
setStackPosition(mRestingStackPosition == null
|
? getDefaultStartPosition()
|
: mRestingStackPosition);
|
mStackMovedToStartPosition = true;
|
mLayout.setVisibility(View.VISIBLE);
|
|
// Animate in the top bubble now that we're visible.
|
if (mLayout.getChildCount() > 0) {
|
animateInBubble(mLayout.getChildAt(0));
|
}
|
});
|
}
|
|
/**
|
* Moves the first bubble instantly to the given X or Y translation, and instructs subsequent
|
* bubbles to animate 'following' to the new location.
|
*/
|
private void moveFirstBubbleWithStackFollowing(
|
DynamicAnimation.ViewProperty property, float value) {
|
|
// Update the canonical stack position.
|
if (property.equals(DynamicAnimation.TRANSLATION_X)) {
|
mStackPosition.x = value;
|
} else if (property.equals(DynamicAnimation.TRANSLATION_Y)) {
|
mStackPosition.y = value;
|
}
|
|
if (mLayout.getChildCount() > 0) {
|
property.setValue(mLayout.getChildAt(0), value);
|
if (mLayout.getChildCount() > 1) {
|
animationForChildAtIndex(1)
|
.property(property, value + getOffsetForChainedPropertyAnimation(property))
|
.start();
|
}
|
}
|
}
|
|
/** Moves the stack to a position instantly, with no animation. */
|
private void setStackPosition(PointF pos) {
|
Log.d(TAG, String.format("Setting position to (%f, %f).", pos.x, pos.y));
|
mStackPosition.set(pos.x, pos.y);
|
|
// If we're not the active controller, we don't want to physically move the bubble views.
|
if (isActiveController()) {
|
mLayout.cancelAllAnimations();
|
cancelStackPositionAnimations();
|
|
// Since we're not using the chained animations, apply the offsets manually.
|
final float xOffset = getOffsetForChainedPropertyAnimation(
|
DynamicAnimation.TRANSLATION_X);
|
final float yOffset = getOffsetForChainedPropertyAnimation(
|
DynamicAnimation.TRANSLATION_Y);
|
for (int i = 0; i < mLayout.getChildCount(); i++) {
|
mLayout.getChildAt(i).setTranslationX(pos.x + (i * xOffset));
|
mLayout.getChildAt(i).setTranslationY(pos.y + (i * yOffset));
|
}
|
}
|
}
|
|
/** Returns the default stack position, which is on the top right. */
|
private PointF getDefaultStartPosition() {
|
return new PointF(
|
getAllowableStackPositionRegion().right,
|
getAllowableStackPositionRegion().top + mStackStartingVerticalOffset);
|
}
|
|
private boolean isStackPositionSet() {
|
return mStackMovedToStartPosition;
|
}
|
|
/** Animates in the given bubble. */
|
private void animateInBubble(View child) {
|
if (!isActiveController()) {
|
return;
|
}
|
|
child.setTranslationY(mStackPosition.y);
|
|
float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
|
animationForChild(child)
|
.scaleX(ANIMATE_IN_STARTING_SCALE /* from */, 1f /* to */)
|
.scaleY(ANIMATE_IN_STARTING_SCALE /* from */, 1f /* to */)
|
.alpha(0f /* from */, 1f /* to */)
|
.translationX(
|
mStackPosition.x - ANIMATE_TRANSLATION_FACTOR * xOffset /* from */,
|
mStackPosition.x /* to */)
|
.start();
|
}
|
|
/**
|
* Cancels any outstanding first bubble property animations that are running. This does not
|
* affect the SpringAnimations controlling the individual bubbles' 'following' effect - it only
|
* cancels animations started from {@link #springFirstBubbleWithStackFollowing} and
|
* {@link #flingThenSpringFirstBubbleWithStackFollowing}.
|
*/
|
private void cancelStackPositionAnimation(DynamicAnimation.ViewProperty property) {
|
if (mStackPositionAnimations.containsKey(property)) {
|
mStackPositionAnimations.get(property).cancel();
|
}
|
}
|
|
/**
|
* FloatProperty that uses {@link #moveFirstBubbleWithStackFollowing} to set the first bubble's
|
* translation and animate the rest of the stack with it. A DynamicAnimation can animate this
|
* property directly to move the first bubble and cause the stack to 'follow' to the new
|
* location.
|
*
|
* This could also be achieved by simply animating the first bubble view and adding an update
|
* listener to dispatch movement to the rest of the stack. However, this would require
|
* duplication of logic in that update handler - it's simpler to keep all logic contained in the
|
* {@link #moveFirstBubbleWithStackFollowing} method.
|
*/
|
private class StackPositionProperty
|
extends FloatPropertyCompat<StackAnimationController> {
|
private final DynamicAnimation.ViewProperty mProperty;
|
|
private StackPositionProperty(DynamicAnimation.ViewProperty property) {
|
super(property.toString());
|
mProperty = property;
|
}
|
|
@Override
|
public float getValue(StackAnimationController controller) {
|
return mLayout.getChildCount() > 0 ? mProperty.getValue(mLayout.getChildAt(0)) : 0;
|
}
|
|
@Override
|
public void setValue(StackAnimationController controller, float value) {
|
moveFirstBubbleWithStackFollowing(mProperty, value);
|
}
|
}
|
}
|