Adds the new dismiss target, with fling/magnet to dismiss.
Test: atest SystemUITests Bug: 123541855 Change-Id: Id2a149f8551eb58b240ece35569ca394c06e811d
This commit is contained in:
27
packages/SystemUI/res/drawable/bubble_dismiss_circle.xml
Normal file
27
packages/SystemUI/res/drawable/bubble_dismiss_circle.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
<!--
|
||||
The transparent circle outline that encircles the bubbles when they're in the dismiss target.
|
||||
-->
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#66FFFFFF" />
|
||||
|
||||
</shape>
|
||||
26
packages/SystemUI/res/drawable/bubble_dismiss_icon.xml
Normal file
26
packages/SystemUI/res/drawable/bubble_dismiss_icon.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
<!-- The 'X' bubble dismiss icon. This is just ic_close with a stroke. -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24.0dp"
|
||||
android:height="24.0dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:pathData="M19.000000,6.400000l-1.400000,-1.400000 -5.600000,5.600000 -5.600000,-5.600000 -1.400000,1.400000 5.600000,5.600000 -5.600000,5.600000 1.400000,1.400000 5.600000,-5.600000 5.600000,5.600000 1.400000,-1.400000 -5.600000,-5.600000z"
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:strokeColor="#FF000000"/>
|
||||
</vector>
|
||||
66
packages/SystemUI/res/layout/bubble_dismiss_target.xml
Normal file
66
packages/SystemUI/res/layout/bubble_dismiss_target.xml
Normal file
@@ -0,0 +1,66 @@
|
||||
<!--
|
||||
~ 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
|
||||
-->
|
||||
<!-- Bubble dismiss target consisting of an X icon and the text 'Dismiss'. -->
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/pip_dismiss_gradient_height"
|
||||
android:layout_gravity="bottom|center_horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/bubble_dismiss_icon_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:paddingBottom="@dimen/bubble_dismiss_target_padding_y"
|
||||
android:paddingTop="@dimen/bubble_dismiss_target_padding_y"
|
||||
android:paddingLeft="@dimen/bubble_dismiss_target_padding_x"
|
||||
android:paddingRight="@dimen/bubble_dismiss_target_padding_x"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/bubble_dismiss_close_icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/bubble_dismiss_icon" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bubble_dismiss_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="9dp"
|
||||
android:layout_marginBottom="9dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Body1"
|
||||
android:textColor="@android:color/white"
|
||||
android:shadowColor="@android:color/black"
|
||||
android:shadowDx="-1"
|
||||
android:shadowDy="1"
|
||||
android:shadowRadius="0.01"
|
||||
android:text="@string/bubble_dismiss_text" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/bubble_dismiss_circle"
|
||||
android:layout_width="@dimen/bubble_dismiss_encircle_size"
|
||||
android:layout_height="@dimen/bubble_dismiss_encircle_size"
|
||||
android:layout_gravity="center"
|
||||
android:alpha="0"
|
||||
android:background="@drawable/bubble_dismiss_circle" />
|
||||
</FrameLayout>
|
||||
@@ -1095,6 +1095,8 @@
|
||||
<dimen name="bubble_padding">8dp</dimen>
|
||||
<!-- Size of individual bubbles. -->
|
||||
<dimen name="individual_bubble_size">52dp</dimen>
|
||||
<!-- Size of the circle around the bubbles when they're in the dismiss target. -->
|
||||
<dimen name="bubble_dismiss_encircle_size">56dp</dimen>
|
||||
<!-- How much to inset the icon in the circle -->
|
||||
<dimen name="bubble_icon_inset">16dp</dimen>
|
||||
<!-- Padding around the view displayed when the bubble is expanded -->
|
||||
@@ -1131,7 +1133,12 @@
|
||||
<dimen name="bubble_header_icon_size">48dp</dimen>
|
||||
<!-- Space between the pointer triangle and the bubble expanded view -->
|
||||
<dimen name="bubble_pointer_margin">8dp</dimen>
|
||||
|
||||
<!-- Height of the permission prompt shown with bubbles -->
|
||||
<dimen name="bubble_permission_height">120dp</dimen>
|
||||
<!-- Padding applied to the bubble dismiss target. Touches in this padding cause the bubbles to
|
||||
snap to the dismiss target. -->
|
||||
<dimen name="bubble_dismiss_target_padding_x">40dp</dimen>
|
||||
<dimen name="bubble_dismiss_target_padding_y">20dp</dimen>
|
||||
<!-- Size of the RAT type for CellularTile -->
|
||||
<dimen name="celltile_rat_type_size">10sp</dimen>
|
||||
</resources>
|
||||
|
||||
@@ -2442,4 +2442,6 @@
|
||||
<string name="bubble_accessibility_action_move_bottom_left">Move bottom left</string>
|
||||
<!-- Action in accessibility menu to move the stack of bubbles to the bottom right of the screen. [CHAR LIMIT=30]-->
|
||||
<string name="bubble_accessibility_action_move_bottom_right">Move bottom right</string>
|
||||
<!-- Text used for the bubble dismiss area. Bubbles dragged to, or flung towards, this area will go away. [CHAR LIMIT=20] -->
|
||||
<string name="bubble_dismiss_text">Dismiss</string>
|
||||
</resources>
|
||||
|
||||
@@ -108,7 +108,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
|
||||
static final int DISMISS_ACCESSIBILITY_ACTION = 6;
|
||||
static final int DISMISS_NO_LONGER_BUBBLE = 7;
|
||||
|
||||
static final int MAX_BUBBLES = 5; // TODO: actually enforce this
|
||||
public static final int MAX_BUBBLES = 5; // TODO: actually enforce this
|
||||
|
||||
// Enables some subset of notifs to automatically become bubbles
|
||||
private static final boolean DEBUG_ENABLE_AUTO_BUBBLE = false;
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.dynamicanimation.animation.DynamicAnimation;
|
||||
import androidx.dynamicanimation.animation.SpringAnimation;
|
||||
import androidx.dynamicanimation.animation.SpringForce;
|
||||
|
||||
import com.android.systemui.R;
|
||||
|
||||
/** Dismiss view that contains a scrim gradient, as well as a dismiss icon, text, and circle. */
|
||||
public class BubbleDismissView extends FrameLayout {
|
||||
/** Duration for animations involving the dismiss target text/icon/gradient. */
|
||||
private static final int DISMISS_TARGET_ANIMATION_BASE_DURATION = 150;
|
||||
|
||||
private View mDismissGradient;
|
||||
|
||||
private LinearLayout mDismissTarget;
|
||||
private ImageView mDismissIcon;
|
||||
private TextView mDismissText;
|
||||
private View mDismissCircle;
|
||||
|
||||
private SpringAnimation mDismissTargetAlphaSpring;
|
||||
private SpringAnimation mDismissTargetVerticalSpring;
|
||||
|
||||
public BubbleDismissView(Context context) {
|
||||
super(context);
|
||||
setVisibility(GONE);
|
||||
|
||||
mDismissGradient = new FrameLayout(mContext);
|
||||
|
||||
FrameLayout.LayoutParams gradientParams =
|
||||
new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT);
|
||||
gradientParams.gravity = Gravity.BOTTOM;
|
||||
mDismissGradient.setLayoutParams(gradientParams);
|
||||
|
||||
Drawable gradient = mContext.getResources().getDrawable(R.drawable.pip_dismiss_scrim);
|
||||
gradient.setAlpha((int) (255 * 0.85f));
|
||||
mDismissGradient.setBackground(gradient);
|
||||
|
||||
mDismissGradient.setVisibility(GONE);
|
||||
addView(mDismissGradient);
|
||||
|
||||
LayoutInflater.from(context).inflate(R.layout.bubble_dismiss_target, this, true);
|
||||
mDismissTarget = findViewById(R.id.bubble_dismiss_icon_container);
|
||||
mDismissIcon = findViewById(R.id.bubble_dismiss_close_icon);
|
||||
mDismissText = findViewById(R.id.bubble_dismiss_text);
|
||||
mDismissCircle = findViewById(R.id.bubble_dismiss_circle);
|
||||
|
||||
// Set up the basic target area animations. These are very simple animations that don't need
|
||||
// fancy interpolators.
|
||||
final AccelerateDecelerateInterpolator interpolator =
|
||||
new AccelerateDecelerateInterpolator();
|
||||
mDismissGradient.animate()
|
||||
.setDuration(DISMISS_TARGET_ANIMATION_BASE_DURATION)
|
||||
.setInterpolator(interpolator);
|
||||
mDismissText.animate()
|
||||
.setDuration(DISMISS_TARGET_ANIMATION_BASE_DURATION)
|
||||
.setInterpolator(interpolator);
|
||||
mDismissIcon.animate()
|
||||
.setDuration(DISMISS_TARGET_ANIMATION_BASE_DURATION)
|
||||
.setInterpolator(interpolator);
|
||||
mDismissCircle.animate()
|
||||
.setDuration(DISMISS_TARGET_ANIMATION_BASE_DURATION / 2)
|
||||
.setInterpolator(interpolator);
|
||||
|
||||
mDismissTargetAlphaSpring =
|
||||
new SpringAnimation(mDismissTarget, DynamicAnimation.ALPHA)
|
||||
.setSpring(new SpringForce()
|
||||
.setStiffness(SpringForce.STIFFNESS_LOW)
|
||||
.setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
|
||||
mDismissTargetVerticalSpring =
|
||||
new SpringAnimation(mDismissTarget, DynamicAnimation.TRANSLATION_Y)
|
||||
.setSpring(new SpringForce()
|
||||
.setStiffness(SpringForce.STIFFNESS_MEDIUM)
|
||||
.setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
|
||||
|
||||
mDismissTargetAlphaSpring.addEndListener((anim, canceled, alpha, velocity) -> {
|
||||
// Since DynamicAnimations end when they're 'nearly' done, we can't rely on alpha being
|
||||
// exactly zero when this listener is triggered. However, if it's less than 50% we can
|
||||
// safely assume it was animating out rather than in.
|
||||
if (alpha < 0.5f) {
|
||||
// If the alpha spring was animating the view out, set it to GONE when it's done.
|
||||
setVisibility(GONE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Springs in the dismiss target and fades in the gradient. */
|
||||
void springIn() {
|
||||
setVisibility(View.VISIBLE);
|
||||
|
||||
// Fade in the dismiss target (icon + text).
|
||||
mDismissTarget.setAlpha(0f);
|
||||
mDismissTargetAlphaSpring.animateToFinalPosition(1f);
|
||||
|
||||
// Spring up the dismiss target (icon + text).
|
||||
mDismissTarget.setTranslationY(mDismissTarget.getHeight() / 2f);
|
||||
mDismissTargetVerticalSpring.animateToFinalPosition(0);
|
||||
|
||||
// Fade in the gradient.
|
||||
mDismissGradient.setVisibility(VISIBLE);
|
||||
mDismissGradient.animate().alpha(1f);
|
||||
|
||||
// Make sure the dismiss elements are in the separated position (in case we hid the target
|
||||
// while they were condensed to cover the bubbles being in the target).
|
||||
mDismissIcon.setAlpha(1f);
|
||||
mDismissIcon.setScaleX(1f);
|
||||
mDismissIcon.setScaleY(1f);
|
||||
mDismissIcon.setTranslationX(0f);
|
||||
mDismissText.setAlpha(1f);
|
||||
mDismissText.setTranslationX(0f);
|
||||
}
|
||||
|
||||
/** Springs out the dismiss target and fades out the gradient. */
|
||||
void springOut() {
|
||||
// Fade out the target.
|
||||
mDismissTargetAlphaSpring.animateToFinalPosition(0f);
|
||||
|
||||
// Spring the target down a bit.
|
||||
mDismissTargetVerticalSpring.animateToFinalPosition(mDismissTarget.getHeight() / 2f);
|
||||
|
||||
// Fade out the gradient and then set it to GONE so it's not in the SBV hierarchy.
|
||||
mDismissGradient.animate().alpha(0f).withEndAction(
|
||||
() -> mDismissGradient.setVisibility(GONE));
|
||||
|
||||
// Pop out the dismiss circle.
|
||||
mDismissCircle.animate().alpha(0f).scaleX(1.2f).scaleY(1.2f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encircles the center of the dismiss target, pulling the X towards the center and hiding the
|
||||
* text.
|
||||
*/
|
||||
void animateEncircleCenterWithX(boolean encircle) {
|
||||
// Pull the text towards the center if we're encircling (it'll be faded out, leaving only
|
||||
// the X icon over the bubbles), or back to normal if we're un-encircling.
|
||||
final float textTranslation = encircle
|
||||
? -mDismissIcon.getWidth() / 4f
|
||||
: 0f;
|
||||
|
||||
// Center the icon if we're encircling, or put it back to normal if not.
|
||||
final float iconTranslation = encircle
|
||||
? mDismissTarget.getWidth() / 2f
|
||||
- mDismissIcon.getWidth() / 2f
|
||||
- mDismissIcon.getLeft()
|
||||
: 0f;
|
||||
|
||||
// Fade in/out the text and translate it.
|
||||
mDismissText.animate()
|
||||
.alpha(encircle ? 0f : 1f)
|
||||
.translationX(textTranslation);
|
||||
|
||||
mDismissIcon.animate()
|
||||
.setDuration(150)
|
||||
.translationX(iconTranslation);
|
||||
|
||||
// Fade out the gradient if we're encircling (the bubbles will 'absorb' it by darkening
|
||||
// themselves).
|
||||
mDismissGradient.animate()
|
||||
.alpha(encircle ? 0f : 1f);
|
||||
|
||||
// Prepare the circle to be 'dropped in'.
|
||||
if (encircle) {
|
||||
mDismissCircle.setAlpha(0f);
|
||||
mDismissCircle.setScaleX(1.2f);
|
||||
mDismissCircle.setScaleY(1.2f);
|
||||
}
|
||||
|
||||
// Drop in the circle, or pull it back up.
|
||||
mDismissCircle.animate()
|
||||
.alpha(encircle ? 1f : 0f)
|
||||
.scaleX(encircle ? 1f : 0f)
|
||||
.scaleY(encircle ? 1f : 0f);
|
||||
}
|
||||
|
||||
/** Animates the circle and the centered icon out. */
|
||||
void animateEncirclingCircleDisappearance() {
|
||||
// Pop out the dismiss icon and circle.
|
||||
mDismissIcon.animate()
|
||||
.setDuration(50)
|
||||
.scaleX(0.9f)
|
||||
.scaleY(0.9f)
|
||||
.alpha(0f);
|
||||
mDismissCircle.animate()
|
||||
.scaleX(0.9f)
|
||||
.scaleY(0.9f)
|
||||
.alpha(0f);
|
||||
}
|
||||
|
||||
/** Returns the Y value of the center of the dismiss target. */
|
||||
float getDismissTargetCenterY() {
|
||||
return getTop() + mDismissTarget.getTop() + mDismissTarget.getHeight() / 2f;
|
||||
}
|
||||
|
||||
/** Returns the dismiss target, which contains the text/icon and any added padding. */
|
||||
View getDismissTarget() {
|
||||
return mDismissTarget;
|
||||
}
|
||||
}
|
||||
@@ -19,12 +19,18 @@ package com.android.systemui.bubbles;
|
||||
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.annotation.NonNull;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.ColorMatrix;
|
||||
import android.graphics.ColorMatrixColorFilter;
|
||||
import android.graphics.Outline;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.Rect;
|
||||
@@ -32,6 +38,8 @@ import android.graphics.RectF;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.os.Bundle;
|
||||
import android.os.VibrationEffect;
|
||||
import android.os.Vibrator;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
import android.util.Log;
|
||||
import android.util.StatsLog;
|
||||
@@ -84,6 +92,9 @@ public class BubbleStackView extends FrameLayout {
|
||||
/** Max width of the flyout, in terms of percent of the screen width. */
|
||||
private static final float FLYOUT_MAX_WIDTH_PERCENT = .6f;
|
||||
|
||||
/** Percent to darken the bubbles when they're in the dismiss target. */
|
||||
private static final float DARKEN_PERCENT = 0.3f;
|
||||
|
||||
/** How long to wait, in milliseconds, before hiding the flyout. */
|
||||
@VisibleForTesting
|
||||
static final int FLYOUT_HIDE_AFTER = 5000;
|
||||
@@ -131,6 +142,10 @@ public class BubbleStackView extends FrameLayout {
|
||||
private final SpringAnimation mExpandedViewYAnim;
|
||||
private final BubbleData mBubbleData;
|
||||
|
||||
private final Vibrator mVibrator;
|
||||
private final ValueAnimator mDesaturateAndDarkenAnimator;
|
||||
private final Paint mDesaturateAndDarkenPaint = new Paint();
|
||||
|
||||
private PhysicsAnimationLayout mBubbleContainer;
|
||||
private StackAnimationController mStackAnimationController;
|
||||
private ExpandedAnimationController mExpandedAnimationController;
|
||||
@@ -183,6 +198,20 @@ public class BubbleStackView extends FrameLayout {
|
||||
|
||||
private boolean mViewUpdatedRequested = false;
|
||||
private boolean mIsExpansionAnimating = false;
|
||||
private boolean mShowingDismiss = false;
|
||||
|
||||
/**
|
||||
* Whether the user is currently dragging their finger within the dismiss target. In this state
|
||||
* the stack will be magnetized to the center of the target, so we shouldn't move it until the
|
||||
* touch exits the dismiss target area.
|
||||
*/
|
||||
private boolean mDraggingInDismissTarget = false;
|
||||
|
||||
/** Whether the stack is magneting towards the dismiss target. */
|
||||
private boolean mAnimatingMagnet = false;
|
||||
|
||||
/** The view to desaturate/darken when magneted to the dismiss target. */
|
||||
private View mDesaturateAndDarkenTargetView;
|
||||
|
||||
private LayoutInflater mInflater;
|
||||
|
||||
@@ -222,6 +251,8 @@ public class BubbleStackView extends FrameLayout {
|
||||
|
||||
@NonNull private final SurfaceSynchronizer mSurfaceSynchronizer;
|
||||
|
||||
private BubbleDismissView mDismissContainer;
|
||||
private Runnable mAfterMagnet;
|
||||
|
||||
public BubbleStackView(Context context, BubbleData data,
|
||||
@Nullable SurfaceSynchronizer synchronizer) {
|
||||
@@ -253,6 +284,8 @@ public class BubbleStackView extends FrameLayout {
|
||||
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
|
||||
wm.getDefaultDisplay().getSize(mDisplaySize);
|
||||
|
||||
mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
|
||||
|
||||
int padding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
|
||||
int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
|
||||
|
||||
@@ -286,6 +319,13 @@ public class BubbleStackView extends FrameLayout {
|
||||
addView(mFlyoutContainer);
|
||||
setupFlyout();
|
||||
|
||||
mDismissContainer = new BubbleDismissView(mContext);
|
||||
mDismissContainer.setLayoutParams(new FrameLayout.LayoutParams(
|
||||
MATCH_PARENT,
|
||||
getResources().getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height),
|
||||
Gravity.BOTTOM));
|
||||
addView(mDismissContainer);
|
||||
|
||||
mExpandedViewXAnim =
|
||||
new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_X);
|
||||
mExpandedViewXAnim.setSpring(
|
||||
@@ -342,6 +382,29 @@ public class BubbleStackView extends FrameLayout {
|
||||
|
||||
// This must be a separate OnDrawListener since it should be called for every draw.
|
||||
getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater);
|
||||
|
||||
final ColorMatrix animatedMatrix = new ColorMatrix();
|
||||
final ColorMatrix darkenMatrix = new ColorMatrix();
|
||||
|
||||
mDesaturateAndDarkenAnimator = ValueAnimator.ofFloat(1f, 0f);
|
||||
mDesaturateAndDarkenAnimator.addUpdateListener(animation -> {
|
||||
final float animatedValue = (float) animation.getAnimatedValue();
|
||||
animatedMatrix.setSaturation(animatedValue);
|
||||
|
||||
final float animatedDarkenValue = (1f - animatedValue) * DARKEN_PERCENT;
|
||||
darkenMatrix.setScale(
|
||||
1f - animatedDarkenValue /* red */,
|
||||
1f - animatedDarkenValue /* green */,
|
||||
1f - animatedDarkenValue /* blue */,
|
||||
1f /* alpha */);
|
||||
|
||||
// Concat the matrices so that the animatedMatrix both desaturates and darkens.
|
||||
animatedMatrix.postConcat(darkenMatrix);
|
||||
|
||||
// Update the paint and apply it to the bubble container.
|
||||
mDesaturateAndDarkenPaint.setColorFilter(new ColorMatrixColorFilter(animatedMatrix));
|
||||
mDesaturateAndDarkenTargetView.setLayerPaint(mDesaturateAndDarkenPaint);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -838,23 +901,22 @@ public class BubbleStackView extends FrameLayout {
|
||||
}
|
||||
|
||||
mExpandedAnimationController.dragBubbleOut(bubble, x, y);
|
||||
springInDismissTarget();
|
||||
}
|
||||
|
||||
/** Called when a drag operation on an individual bubble has finished. */
|
||||
public void onBubbleDragFinish(
|
||||
View bubble, float x, float y, float velX, float velY, boolean dismissed) {
|
||||
View bubble, float x, float y, float velX, float velY) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onBubbleDragFinish: bubble=" + bubble + ", dismissed=" + dismissed);
|
||||
Log.d(TAG, "onBubbleDragFinish: bubble=" + bubble);
|
||||
}
|
||||
|
||||
if (!mIsExpanded || mIsExpansionAnimating) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dismissed) {
|
||||
mExpandedAnimationController.prepareForDismissalWithVelocity(bubble, velX, velY);
|
||||
} else {
|
||||
mExpandedAnimationController.snapBubbleBack(bubble, velX, velY);
|
||||
}
|
||||
mExpandedAnimationController.snapBubbleBack(bubble, velX, velY);
|
||||
springOutDismissTargetAndHideCircle();
|
||||
}
|
||||
|
||||
void onDragStart() {
|
||||
@@ -870,6 +932,7 @@ public class BubbleStackView extends FrameLayout {
|
||||
hideFlyoutImmediate();
|
||||
|
||||
mIsDragging = true;
|
||||
mDraggingInDismissTarget = false;
|
||||
}
|
||||
|
||||
void onDragged(float x, float y) {
|
||||
@@ -877,7 +940,8 @@ public class BubbleStackView extends FrameLayout {
|
||||
return;
|
||||
}
|
||||
|
||||
mStackAnimationController.moveFirstBubbleWithStackFollowing(x, y);
|
||||
springInDismissTarget();
|
||||
mStackAnimationController.moveStackFromTouch(x, y);
|
||||
}
|
||||
|
||||
void onDragFinish(float x, float y, float velX, float velY) {
|
||||
@@ -894,10 +958,171 @@ public class BubbleStackView extends FrameLayout {
|
||||
mStackAnimationController.flingStackThenSpringToEdge(x, velX, velY);
|
||||
logBubbleEvent(null /* no bubble associated with bubble stack move */,
|
||||
StatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED);
|
||||
|
||||
springOutDismissTargetAndHideCircle();
|
||||
}
|
||||
|
||||
void onDragFinishAsDismiss() {
|
||||
mIsDragging = false;
|
||||
/** Prepares and starts the desaturate/darken animation on the bubble stack. */
|
||||
private void animateDesaturateAndDarken(View targetView, boolean desaturateAndDarken) {
|
||||
mDesaturateAndDarkenTargetView = targetView;
|
||||
|
||||
if (desaturateAndDarken) {
|
||||
// Use the animated paint for the bubbles.
|
||||
mDesaturateAndDarkenTargetView.setLayerType(
|
||||
View.LAYER_TYPE_HARDWARE, mDesaturateAndDarkenPaint);
|
||||
mDesaturateAndDarkenAnimator.removeAllListeners();
|
||||
mDesaturateAndDarkenAnimator.start();
|
||||
} else {
|
||||
mDesaturateAndDarkenAnimator.removeAllListeners();
|
||||
mDesaturateAndDarkenAnimator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
super.onAnimationEnd(animation);
|
||||
// Stop using the animated paint.
|
||||
resetDesaturationAndDarken();
|
||||
}
|
||||
});
|
||||
mDesaturateAndDarkenAnimator.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
private void resetDesaturationAndDarken() {
|
||||
mDesaturateAndDarkenAnimator.removeAllListeners();
|
||||
mDesaturateAndDarkenAnimator.cancel();
|
||||
mDesaturateAndDarkenTargetView.setLayerType(View.LAYER_TYPE_NONE, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Magnets the stack to the target, while also transforming the target to encircle the stack and
|
||||
* desaturating/darkening the bubbles.
|
||||
*/
|
||||
void animateMagnetToDismissTarget(
|
||||
View magnetView, boolean toTarget, float x, float y, float velX, float velY) {
|
||||
mDraggingInDismissTarget = toTarget;
|
||||
|
||||
if (toTarget) {
|
||||
// The Y-value for the bubble stack to be positioned in the center of the dismiss target
|
||||
final float destY = mDismissContainer.getDismissTargetCenterY() - mBubbleSize / 2f;
|
||||
|
||||
mAnimatingMagnet = true;
|
||||
|
||||
final Runnable afterMagnet = () -> {
|
||||
mAnimatingMagnet = false;
|
||||
if (mAfterMagnet != null) {
|
||||
mAfterMagnet.run();
|
||||
}
|
||||
};
|
||||
|
||||
if (magnetView == this) {
|
||||
mStackAnimationController.magnetToDismiss(velX, velY, destY, afterMagnet);
|
||||
animateDesaturateAndDarken(mBubbleContainer, true);
|
||||
} else {
|
||||
mExpandedAnimationController.magnetBubbleToDismiss(
|
||||
magnetView, velX, velY, destY, afterMagnet);
|
||||
|
||||
animateDesaturateAndDarken(magnetView, true);
|
||||
}
|
||||
|
||||
mDismissContainer.animateEncircleCenterWithX(true);
|
||||
|
||||
} else {
|
||||
mAnimatingMagnet = false;
|
||||
|
||||
if (magnetView == this) {
|
||||
mStackAnimationController.demagnetizeFromDismissToPoint(x, y, velX, velY);
|
||||
animateDesaturateAndDarken(mBubbleContainer, false);
|
||||
} else {
|
||||
mExpandedAnimationController.demagnetizeBubbleTo(x, y, velX, velY);
|
||||
animateDesaturateAndDarken(magnetView, false);
|
||||
}
|
||||
|
||||
mDismissContainer.animateEncircleCenterWithX(false);
|
||||
}
|
||||
|
||||
mVibrator.vibrate(VibrationEffect.get(toTarget
|
||||
? VibrationEffect.EFFECT_CLICK
|
||||
: VibrationEffect.EFFECT_TICK));
|
||||
}
|
||||
|
||||
/**
|
||||
* Magnets the stack to the dismiss target if it's not already there. Then, dismiss the stack
|
||||
* using the 'implode' animation and animate out the target.
|
||||
*/
|
||||
void magnetToStackIfNeededThenAnimateDismissal(
|
||||
View touchedView, float velX, float velY, Runnable after) {
|
||||
final Runnable animateDismissal = () -> {
|
||||
mAfterMagnet = null;
|
||||
|
||||
mVibrator.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_CLICK));
|
||||
mDismissContainer.animateEncirclingCircleDisappearance();
|
||||
|
||||
// 'Implode' the stack and then hide the dismiss target.
|
||||
if (touchedView == this) {
|
||||
mStackAnimationController.implodeStack(
|
||||
() -> {
|
||||
mAnimatingMagnet = false;
|
||||
mShowingDismiss = false;
|
||||
mDraggingInDismissTarget = false;
|
||||
after.run();
|
||||
resetDesaturationAndDarken();
|
||||
});
|
||||
} else {
|
||||
mExpandedAnimationController.dismissDraggedOutBubble(() -> {
|
||||
mAnimatingMagnet = false;
|
||||
mShowingDismiss = false;
|
||||
mDraggingInDismissTarget = false;
|
||||
resetDesaturationAndDarken();
|
||||
after.run();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (mAnimatingMagnet) {
|
||||
// If the magnet animation is currently playing, dismiss the stack after it's done. This
|
||||
// happens if the stack is flung towards the target.
|
||||
mAfterMagnet = animateDismissal;
|
||||
} else if (mDraggingInDismissTarget) {
|
||||
// If we're in the dismiss target, but not animating, we already magneted - dismiss
|
||||
// immediately.
|
||||
animateDismissal.run();
|
||||
} else {
|
||||
// Otherwise, we need to start the magnet animation and then dismiss afterward.
|
||||
animateMagnetToDismissTarget(touchedView, true, -1 /* x */, -1 /* y */, velX, velY);
|
||||
mAfterMagnet = animateDismissal;
|
||||
}
|
||||
}
|
||||
|
||||
/** Animates in the dismiss target, including the gradient behind it. */
|
||||
private void springInDismissTarget() {
|
||||
if (mShowingDismiss) {
|
||||
return;
|
||||
}
|
||||
|
||||
mShowingDismiss = true;
|
||||
|
||||
// Show the dismiss container and bring it to the front so the bubbles will go behind it.
|
||||
mDismissContainer.springIn();
|
||||
mDismissContainer.bringToFront();
|
||||
mDismissContainer.setZ(Short.MAX_VALUE - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Animates the dismiss target out, as well as the circle that encircles the bubbles, if they
|
||||
* were dragged into the target and encircled.
|
||||
*/
|
||||
private void springOutDismissTargetAndHideCircle() {
|
||||
if (!mShowingDismiss) {
|
||||
return;
|
||||
}
|
||||
|
||||
mDismissContainer.springOut();
|
||||
mShowingDismiss = false;
|
||||
}
|
||||
|
||||
|
||||
/** Whether the location of the given MotionEvent is within the dismiss target area. */
|
||||
public boolean isInDismissTarget(MotionEvent ev) {
|
||||
return isIntersecting(mDismissContainer.getDismissTarget(), ev.getRawX(), ev.getRawY());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1066,7 +1291,7 @@ public class BubbleStackView extends FrameLayout {
|
||||
private void setupFlyout() {
|
||||
// Retrieve the styled floating background color.
|
||||
TypedArray ta = mContext.obtainStyledAttributes(
|
||||
new int[] {android.R.attr.colorBackgroundFloating});
|
||||
new int[]{android.R.attr.colorBackgroundFloating});
|
||||
final int floatingBackgroundColor = ta.getColor(0, Color.WHITE);
|
||||
ta.recycle();
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
package com.android.systemui.bubbles;
|
||||
|
||||
import static com.android.systemui.pip.phone.PipDismissViewController.SHOW_TARGET_DELAY;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.PointF;
|
||||
import android.os.Handler;
|
||||
@@ -27,17 +25,35 @@ import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
|
||||
import com.android.systemui.Dependency;
|
||||
import com.android.systemui.pip.phone.PipDismissViewController;
|
||||
|
||||
/**
|
||||
* Handles interpreting touches on a {@link BubbleStackView}. This includes expanding, collapsing,
|
||||
* dismissing, and flings.
|
||||
*/
|
||||
class BubbleTouchHandler implements View.OnTouchListener {
|
||||
/** Velocity required to dismiss a bubble without dragging it into the dismiss target. */
|
||||
private static final float DISMISS_MIN_VELOCITY = 4000f;
|
||||
/** Velocity required to dismiss the stack without dragging it into the dismiss target. */
|
||||
private static final float STACK_DISMISS_MIN_VELOCITY = 4000f;
|
||||
|
||||
/**
|
||||
* Velocity required to dismiss an individual bubble without dragging it into the dismiss
|
||||
* target.
|
||||
*
|
||||
* This is higher than the stack dismiss velocity since unlike the stack, a downward fling could
|
||||
* also be an attempted gesture to return the bubble to the row of expanded bubbles, which would
|
||||
* usually be below the dragged bubble. By increasing the required velocity, it's less likely
|
||||
* that the user is trying to drop it back into the row vs. fling it away.
|
||||
*/
|
||||
private static final float INDIVIDUAL_BUBBLE_DISMISS_MIN_VELOCITY = 6000f;
|
||||
|
||||
private static final String TAG = "BubbleTouchHandler";
|
||||
/**
|
||||
* When the stack is flung towards the bottom of the screen, it'll be dismissed if it's flung
|
||||
* towards the center of the screen (where the dismiss target is). This value is the width of
|
||||
* the target area to be considered 'towards the target'. For example 50% means that the stack
|
||||
* needs to be flung towards the middle 50%, and the 25% on the left and right sides won't
|
||||
* count.
|
||||
*/
|
||||
private static final float DISMISS_FLING_TARGET_WIDTH_PERCENT = 0.5f;
|
||||
|
||||
private final PointF mTouchDown = new PointF();
|
||||
private final PointF mViewPositionOnTouchDown = new PointF();
|
||||
@@ -45,7 +61,6 @@ class BubbleTouchHandler implements View.OnTouchListener {
|
||||
private final BubbleData mBubbleData;
|
||||
|
||||
private BubbleController mController = Dependency.get(BubbleController.class);
|
||||
private PipDismissViewController mDismissViewController;
|
||||
|
||||
private boolean mMovedEnough;
|
||||
private int mTouchSlopSquared;
|
||||
@@ -53,12 +68,6 @@ class BubbleTouchHandler implements View.OnTouchListener {
|
||||
|
||||
private boolean mInDismissTarget;
|
||||
private Handler mHandler = new Handler();
|
||||
private Runnable mShowDismissAffordance = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mDismissViewController.showDismissTarget();
|
||||
}
|
||||
};
|
||||
|
||||
/** View that was initially touched, when we received the first ACTION_DOWN event. */
|
||||
private View mTouchedView;
|
||||
@@ -67,7 +76,6 @@ class BubbleTouchHandler implements View.OnTouchListener {
|
||||
BubbleData bubbleData, Context context) {
|
||||
final int touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
|
||||
mTouchSlopSquared = touchSlop * touchSlop;
|
||||
mDismissViewController = new PipDismissViewController(context);
|
||||
mBubbleData = bubbleData;
|
||||
mStack = stackView;
|
||||
}
|
||||
@@ -104,11 +112,6 @@ class BubbleTouchHandler implements View.OnTouchListener {
|
||||
|
||||
mTouchDown.set(rawX, rawY);
|
||||
|
||||
if (!isFlyout) {
|
||||
mDismissViewController.createDismissTarget();
|
||||
mHandler.postDelayed(mShowDismissAffordance, SHOW_TARGET_DELAY);
|
||||
}
|
||||
|
||||
if (isStack) {
|
||||
mViewPositionOnTouchDown.set(mStack.getStackPosition());
|
||||
mStack.onDragStart();
|
||||
@@ -140,9 +143,18 @@ class BubbleTouchHandler implements View.OnTouchListener {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO - when we're in the target stick to it / animate in some way?
|
||||
mInDismissTarget = mDismissViewController.updateTarget(
|
||||
isStack ? mStack.getBubbleAt(0) : mTouchedView);
|
||||
final boolean currentlyInDismissTarget = mStack.isInDismissTarget(event);
|
||||
if (currentlyInDismissTarget != mInDismissTarget) {
|
||||
mInDismissTarget = currentlyInDismissTarget;
|
||||
|
||||
mVelocityTracker.computeCurrentVelocity(/* maxVelocity */ 1000);
|
||||
final float velX = mVelocityTracker.getXVelocity();
|
||||
final float velY = mVelocityTracker.getYVelocity();
|
||||
|
||||
// If the touch event is within the dismiss target, magnet the stack to it.
|
||||
mStack.animateMagnetToDismissTarget(
|
||||
mTouchedView, mInDismissTarget, viewX, viewY, velX, velY);
|
||||
}
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
@@ -151,28 +163,40 @@ class BubbleTouchHandler implements View.OnTouchListener {
|
||||
|
||||
case MotionEvent.ACTION_UP:
|
||||
trackMovement(event);
|
||||
if (mInDismissTarget && isStack) {
|
||||
mController.dismissStack(BubbleController.DISMISS_USER_GESTURE);
|
||||
mStack.onDragFinishAsDismiss();
|
||||
mVelocityTracker.computeCurrentVelocity(/* maxVelocity */ 1000);
|
||||
final float velX = mVelocityTracker.getXVelocity();
|
||||
final float velY = mVelocityTracker.getYVelocity();
|
||||
|
||||
final boolean shouldDismiss =
|
||||
isStack
|
||||
? mInDismissTarget
|
||||
|| isFastFlingTowardsDismissTarget(rawX, rawY, velX, velY)
|
||||
: mInDismissTarget
|
||||
|| velY > INDIVIDUAL_BUBBLE_DISMISS_MIN_VELOCITY;
|
||||
|
||||
if (shouldDismiss) {
|
||||
final String individualBubbleKey =
|
||||
isStack ? null : ((BubbleView) mTouchedView).getKey();
|
||||
mStack.magnetToStackIfNeededThenAnimateDismissal(mTouchedView, velX, velY,
|
||||
() -> {
|
||||
if (isStack) {
|
||||
mController.dismissStack(BubbleController.DISMISS_USER_GESTURE);
|
||||
} else {
|
||||
mController.removeBubble(
|
||||
individualBubbleKey,
|
||||
BubbleController.DISMISS_USER_GESTURE);
|
||||
}
|
||||
});
|
||||
} else if (isFlyout) {
|
||||
// TODO(b/129768381): Expand if tapped, dismiss if swiped away.
|
||||
if (!mBubbleData.isExpanded() && !mMovedEnough) {
|
||||
mBubbleData.setExpanded(true);
|
||||
}
|
||||
} else if (mMovedEnough) {
|
||||
mVelocityTracker.computeCurrentVelocity(/* maxVelocity */ 1000);
|
||||
final float velX = mVelocityTracker.getXVelocity();
|
||||
final float velY = mVelocityTracker.getYVelocity();
|
||||
if (isStack) {
|
||||
mStack.onDragFinish(viewX, viewY, velX, velY);
|
||||
} else {
|
||||
final boolean dismissed = mInDismissTarget || velY > DISMISS_MIN_VELOCITY;
|
||||
mStack.onBubbleDragFinish(
|
||||
mTouchedView, viewX, viewY, velX, velY, /* dismissed */ dismissed);
|
||||
if (dismissed) {
|
||||
mController.removeBubble(((BubbleView) mTouchedView).getKey(),
|
||||
BubbleController.DISMISS_USER_GESTURE);
|
||||
}
|
||||
mStack.onBubbleDragFinish(mTouchedView, viewX, viewY, velX, velY);
|
||||
}
|
||||
} else if (mTouchedView == mStack.getExpandedBubbleView()) {
|
||||
mBubbleData.setExpanded(false);
|
||||
@@ -191,9 +215,38 @@ class BubbleTouchHandler implements View.OnTouchListener {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the given touch data represents a powerful fling towards the bottom-center of the
|
||||
* screen (the dismiss target).
|
||||
*/
|
||||
private boolean isFastFlingTowardsDismissTarget(
|
||||
float rawX, float rawY, float velX, float velY) {
|
||||
// Not a fling downward towards the target if velocity is zero or negative.
|
||||
if (velY <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
float bottomOfScreenInterceptX = rawX;
|
||||
|
||||
// Only do math if the X velocity is non-zero, otherwise X won't change.
|
||||
if (velX != 0) {
|
||||
// Rise over run...
|
||||
final float slope = velY / velX;
|
||||
// ...y = mx + b, b = y / mx...
|
||||
final float yIntercept = rawY - slope * rawX;
|
||||
// ...calculate the x value when y = bottom of the screen.
|
||||
bottomOfScreenInterceptX = (mStack.getHeight() - yIntercept) / slope;
|
||||
}
|
||||
|
||||
final float dismissTargetWidth =
|
||||
mStack.getWidth() * DISMISS_FLING_TARGET_WIDTH_PERCENT;
|
||||
return velY > STACK_DISMISS_MIN_VELOCITY
|
||||
&& bottomOfScreenInterceptX > dismissTargetWidth / 2f
|
||||
&& bottomOfScreenInterceptX < mStack.getWidth() - dismissTargetWidth / 2f;
|
||||
}
|
||||
|
||||
/** Clears all touch-related state. */
|
||||
private void resetForNextGesture() {
|
||||
cleanUpDismissTarget();
|
||||
if (mVelocityTracker != null) {
|
||||
mVelocityTracker.recycle();
|
||||
mVelocityTracker = null;
|
||||
@@ -203,15 +256,6 @@ class BubbleTouchHandler implements View.OnTouchListener {
|
||||
mInDismissTarget = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the dismiss target and cancels any pending callbacks to show it.
|
||||
*/
|
||||
private void cleanUpDismissTarget() {
|
||||
mHandler.removeCallbacks(mShowDismissAffordance);
|
||||
mDismissViewController.destroyDismissTarget();
|
||||
}
|
||||
|
||||
|
||||
private void trackMovement(MotionEvent event) {
|
||||
if (mVelocityTracker == null) {
|
||||
mVelocityTracker = VelocityTracker.obtain();
|
||||
|
||||
@@ -64,6 +64,20 @@ public class ExpandedAnimationController
|
||||
/** Size of dismiss target at bottom of screen. */
|
||||
private float mPipDismissHeight;
|
||||
|
||||
/** Whether the dragged-out bubble is in the dismiss target. */
|
||||
private boolean mIndividualBubbleWithinDismissTarget = false;
|
||||
|
||||
/**
|
||||
* Whether the dragged out bubble is springing towards the touch point, rather than using the
|
||||
* default behavior of moving directly to the touch point.
|
||||
*
|
||||
* This happens when the user's finger exits the dismiss area while the bubble is magnetized to
|
||||
* the center. Since the touch point differs from the bubble location, we need to animate the
|
||||
* bubble 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.
|
||||
*/
|
||||
private boolean mSpringingBubbleToTouch = false;
|
||||
|
||||
public ExpandedAnimationController(Point displaySize) {
|
||||
mDisplaySize = displaySize;
|
||||
}
|
||||
@@ -151,8 +165,23 @@ public class ExpandedAnimationController
|
||||
* bubble is dragged back into the row.
|
||||
*/
|
||||
public void dragBubbleOut(View bubbleView, float x, float y) {
|
||||
bubbleView.setTranslationX(x);
|
||||
bubbleView.setTranslationY(y);
|
||||
if (mSpringingBubbleToTouch) {
|
||||
if (mLayout.arePropertiesAnimatingOnView(
|
||||
bubbleView, DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y)) {
|
||||
animationForChild(mBubbleDraggingOut)
|
||||
.translationX(x)
|
||||
.translationY(y)
|
||||
.withStiffness(SpringForce.STIFFNESS_HIGH)
|
||||
.start();
|
||||
} else {
|
||||
mSpringingBubbleToTouch = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!mSpringingBubbleToTouch && !mIndividualBubbleWithinDismissTarget) {
|
||||
bubbleView.setTranslationX(x);
|
||||
bubbleView.setTranslationY(y);
|
||||
}
|
||||
|
||||
final boolean draggedOutEnough =
|
||||
y > getExpandedY() + mBubbleSizePx || y < getExpandedY() - mBubbleSizePx;
|
||||
@@ -164,6 +193,53 @@ public class ExpandedAnimationController
|
||||
}
|
||||
}
|
||||
|
||||
/** Plays a dismiss animation on the dragged out bubble. */
|
||||
public void dismissDraggedOutBubble(Runnable after) {
|
||||
mIndividualBubbleWithinDismissTarget = false;
|
||||
|
||||
// Fill the space from the soon to be dismissed bubble.
|
||||
animateStackByBubbleWidthsStartingFrom(
|
||||
/* numBubbleWidths */ -1,
|
||||
/* startIndex */ mLayout.indexOfChild(mBubbleDraggingOut) + 1);
|
||||
|
||||
animationForChild(mBubbleDraggingOut)
|
||||
.withStiffness(SpringForce.STIFFNESS_HIGH)
|
||||
.scaleX(1.1f)
|
||||
.scaleY(1.1f)
|
||||
.alpha(0f, after)
|
||||
.start();
|
||||
}
|
||||
|
||||
/** Magnets the given bubble to the dismiss target. */
|
||||
public void magnetBubbleToDismiss(
|
||||
View bubbleView, float velX, float velY, float destY, Runnable after) {
|
||||
mIndividualBubbleWithinDismissTarget = true;
|
||||
mSpringingBubbleToTouch = false;
|
||||
animationForChild(bubbleView)
|
||||
.withStiffness(SpringForce.STIFFNESS_MEDIUM)
|
||||
.withDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
|
||||
.withPositionStartVelocities(velX, velY)
|
||||
.translationX(mLayout.getWidth() / 2f - mBubbleSizePx / 2f)
|
||||
.translationY(destY, after)
|
||||
.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Springs the dragged-out bubble towards the given coordinates and sets flags to have touch
|
||||
* events update the spring's final position until it's settled.
|
||||
*/
|
||||
public void demagnetizeBubbleTo(float x, float y, float velX, float velY) {
|
||||
mIndividualBubbleWithinDismissTarget = false;
|
||||
mSpringingBubbleToTouch = true;
|
||||
|
||||
animationForChild(mBubbleDraggingOut)
|
||||
.translationX(x)
|
||||
.translationY(y)
|
||||
.withPositionStartVelocities(velX, velY)
|
||||
.withStiffness(SpringForce.STIFFNESS_HIGH)
|
||||
.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Snaps a bubble back to its position within the bubble row, and animates the rest of the
|
||||
* bubbles to accommodate it if it was previously dragged out past the threshold.
|
||||
@@ -274,28 +350,21 @@ public class ExpandedAnimationController
|
||||
|
||||
@Override
|
||||
void onChildRemoved(View child, int index, Runnable finishRemoval) {
|
||||
// Bubble pops out to the top.
|
||||
// TODO: Reverse this when bubbles are at the bottom.
|
||||
|
||||
final PhysicsAnimationLayout.PhysicsPropertyAnimator animator = animationForChild(child);
|
||||
animator.alpha(0f, finishRemoval /* endAction */);
|
||||
|
||||
// If we're removing the dragged-out bubble, that means it got dismissed.
|
||||
if (child.equals(mBubbleDraggingOut)) {
|
||||
animator.position(
|
||||
mLayout.getWidth() / 2f - mBubbleSizePx / 2f,
|
||||
mLayout.getHeight() + mBubbleSizePx)
|
||||
.withPositionStartVelocities(mBubbleDraggingOutVelX, mBubbleDraggingOutVelY)
|
||||
.scaleX(ANIMATE_SCALE_PERCENT)
|
||||
.scaleY(ANIMATE_SCALE_PERCENT);
|
||||
|
||||
mBubbleDraggingOut = null;
|
||||
finishRemoval.run();
|
||||
} else {
|
||||
animator.translationY(getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR);
|
||||
animator.alpha(0f, finishRemoval /* endAction */)
|
||||
.withStiffness(SpringForce.STIFFNESS_HIGH)
|
||||
.withDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
|
||||
.scaleX(1.1f)
|
||||
.scaleY(1.1f)
|
||||
.start();
|
||||
}
|
||||
|
||||
animator.start();
|
||||
|
||||
// Animate all the other bubbles to their new positions sans this bubble.
|
||||
animateBubblesAfterIndexToCorrectX(index);
|
||||
}
|
||||
|
||||
@@ -290,6 +290,10 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
final Runnable checkIfAllFinished = () -> {
|
||||
if (!arePropertiesAnimating(properties)) {
|
||||
action.run();
|
||||
|
||||
for (DynamicAnimation.ViewProperty property : properties) {
|
||||
removeEndActionForProperty(property);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -379,10 +383,21 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
/** Checks whether any animations of the given properties are still running. */
|
||||
public boolean arePropertiesAnimating(DynamicAnimation.ViewProperty... properties) {
|
||||
for (int i = 0; i < getChildCount(); i++) {
|
||||
for (DynamicAnimation.ViewProperty property : properties) {
|
||||
if (getAnimationAtIndex(property, i).isRunning()) {
|
||||
return true;
|
||||
}
|
||||
if (arePropertiesAnimatingOnView(getChildAt(i), properties)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Checks whether any animations of the given properties are running on the given view. */
|
||||
public boolean arePropertiesAnimatingOnView(
|
||||
View view, DynamicAnimation.ViewProperty... properties) {
|
||||
for (DynamicAnimation.ViewProperty property : properties) {
|
||||
final SpringAnimation animation = getAnimationFromView(property, view);
|
||||
if (animation != null && animation.isRunning()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,7 +571,11 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
DynamicAnimation anim, boolean canceled, float value, float velocity) {
|
||||
if (!arePropertiesAnimating(mProperty)) {
|
||||
if (mEndActionForProperty.containsKey(mProperty)) {
|
||||
mEndActionForProperty.get(mProperty).run();
|
||||
final Runnable callback = mEndActionForProperty.get(mProperty);
|
||||
|
||||
if (callback != null) {
|
||||
callback.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -578,6 +597,12 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
/** Start delay to use when start is called. */
|
||||
private long mStartDelay = 0;
|
||||
|
||||
/** Damping ratio to use for the animations. */
|
||||
private float mDampingRatio = -1;
|
||||
|
||||
/** Stiffness to use for the animations. */
|
||||
private float mStiffness = -1;
|
||||
|
||||
/** End actions to call when animations for the given property complete. */
|
||||
private Map<DynamicAnimation.ViewProperty, Runnable[]> mEndActionsForProperty =
|
||||
new HashMap<>();
|
||||
@@ -686,6 +711,24 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the damping ratio to use for this animation. If not supplied, will default to the
|
||||
* value from {@link PhysicsAnimationController#getSpringForce}.
|
||||
*/
|
||||
public PhysicsPropertyAnimator withDampingRatio(float dampingRatio) {
|
||||
mDampingRatio = dampingRatio;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the stiffness to use for this animation. If not supplied, will default to the
|
||||
* value from {@link PhysicsAnimationController#getSpringForce}.
|
||||
*/
|
||||
public PhysicsPropertyAnimator withStiffness(float stiffness) {
|
||||
mStiffness = stiffness;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the start velocities to use for TRANSLATION_X and TRANSLATION_Y animations. This
|
||||
* overrides any value set via {@link #withStartVelocity(float)} for those properties.
|
||||
@@ -711,12 +754,14 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
|
||||
// If there are end actions, set an end listener on the layout for all the properties
|
||||
// we're about to animate.
|
||||
if (after != null) {
|
||||
if (after != null && after.length > 0) {
|
||||
final DynamicAnimation.ViewProperty[] propertiesArray =
|
||||
properties.toArray(new DynamicAnimation.ViewProperty[0]);
|
||||
for (Runnable callback : after) {
|
||||
setEndActionForMultipleProperties(callback, propertiesArray);
|
||||
}
|
||||
setEndActionForMultipleProperties(() -> {
|
||||
for (Runnable callback : after) {
|
||||
callback.run();
|
||||
}
|
||||
}, propertiesArray);
|
||||
}
|
||||
|
||||
// If we used position-specific end actions, we'll need to listen for both TRANSLATION_X
|
||||
@@ -746,12 +791,15 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
|
||||
// Actually start the animations.
|
||||
for (DynamicAnimation.ViewProperty property : properties) {
|
||||
final SpringForce defaultSpringForce = mController.getSpringForce(property, mView);
|
||||
animateValueForChild(
|
||||
property,
|
||||
mView,
|
||||
mAnimatedProperties.get(property),
|
||||
mPositionStartVelocities.getOrDefault(property, mDefaultStartVelocity),
|
||||
mStartDelay,
|
||||
mStiffness >= 0 ? mStiffness : defaultSpringForce.getStiffness(),
|
||||
mDampingRatio >= 0 ? mDampingRatio : defaultSpringForce.getDampingRatio(),
|
||||
mEndActionsForProperty.get(property));
|
||||
}
|
||||
|
||||
@@ -760,6 +808,8 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
mPositionStartVelocities.clear();
|
||||
mDefaultStartVelocity = 0;
|
||||
mStartDelay = 0;
|
||||
mStiffness = -1;
|
||||
mDampingRatio = -1;
|
||||
mEndActionsForProperty.clear();
|
||||
}
|
||||
|
||||
@@ -778,6 +828,8 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
float value,
|
||||
float startVel,
|
||||
long startDelay,
|
||||
float stiffness,
|
||||
float dampingRatio,
|
||||
Runnable[] afterCallbacks) {
|
||||
if (view != null) {
|
||||
final SpringAnimation animation =
|
||||
@@ -795,6 +847,9 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
});
|
||||
}
|
||||
|
||||
animation.getSpring().setStiffness(stiffness);
|
||||
animation.getSpring().setDampingRatio(dampingRatio);
|
||||
|
||||
if (startVel > 0) {
|
||||
animation.setStartVelocity(startVel);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ import androidx.dynamicanimation.animation.SpringAnimation;
|
||||
import androidx.dynamicanimation.animation.SpringForce;
|
||||
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.bubbles.BubbleController;
|
||||
|
||||
import com.google.android.collect.Sets;
|
||||
|
||||
@@ -116,6 +115,25 @@ public class StackAnimationController extends
|
||||
*/
|
||||
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. */
|
||||
@@ -445,6 +463,120 @@ public class StackAnimationController extends
|
||||
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(
|
||||
@@ -459,7 +591,9 @@ public class StackAnimationController extends
|
||||
int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) {
|
||||
if (property.equals(DynamicAnimation.TRANSLATION_X)
|
||||
|| property.equals(DynamicAnimation.TRANSLATION_Y)) {
|
||||
return index + 1; // Just chain them linearly.
|
||||
return index + 1;
|
||||
} else if (mWithinDismissTarget) {
|
||||
return index + 1; // Chain all animations in dismiss (scale, alpha, etc. are used).
|
||||
} else {
|
||||
return NONE;
|
||||
}
|
||||
@@ -469,9 +603,15 @@ public class StackAnimationController extends
|
||||
@Override
|
||||
float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) {
|
||||
if (property.equals(DynamicAnimation.TRANSLATION_X)) {
|
||||
// Offset to the left if we're on the left, or the right otherwise.
|
||||
return mLayout.isFirstChildXLeftOfCenter(mStackPosition.x)
|
||||
? -mStackOffset : mStackOffset;
|
||||
// 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;
|
||||
}
|
||||
@@ -480,11 +620,8 @@ public class StackAnimationController extends
|
||||
@Override
|
||||
SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) {
|
||||
return new SpringForce()
|
||||
.setDampingRatio(BubbleController.getBubbleBounciness(
|
||||
mLayout.getContext(), DEFAULT_BOUNCINESS))
|
||||
.setStiffness(BubbleController.getBubbleStiffness(
|
||||
mLayout.getContext(),
|
||||
mIsMovingFromFlinging ? FLING_FOLLOW_STIFFNESS : DEFAULT_STIFFNESS));
|
||||
.setDampingRatio(DEFAULT_BOUNCINESS)
|
||||
.setStiffness(mIsMovingFromFlinging ? FLING_FOLLOW_STIFFNESS : DEFAULT_STIFFNESS);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -593,32 +730,6 @@ public class StackAnimationController extends
|
||||
.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Springs the first bubble to the given final position, with the rest of the stack 'following'.
|
||||
*/
|
||||
private 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels any outstanding first bubble property animations that are running. This does not
|
||||
* affect the SpringAnimations controlling the individual bubbles' 'following' effect - it only
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
package com.android.systemui.bubbles.animation;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Point;
|
||||
@@ -69,14 +71,14 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
|
||||
|
||||
testBubblesInCorrectExpandedPositions();
|
||||
Mockito.verify(afterExpand).run();
|
||||
verify(afterExpand).run();
|
||||
|
||||
Runnable afterCollapse = Mockito.mock(Runnable.class);
|
||||
mExpandedController.collapseBackToStack(afterCollapse);
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
|
||||
|
||||
testStackedAtPosition(mExpansionPoint.x, mExpansionPoint.y, -1);
|
||||
Mockito.verify(afterExpand).run();
|
||||
verify(afterExpand).run();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -140,6 +142,78 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC
|
||||
testBubblesInCorrectExpandedPositions();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMagnetToDismiss_dismiss() throws InterruptedException {
|
||||
expand();
|
||||
|
||||
final View draggedOutView = mViews.get(0);
|
||||
final Runnable after = Mockito.mock(Runnable.class);
|
||||
|
||||
mExpandedController.prepareForBubbleDrag(draggedOutView);
|
||||
mExpandedController.dragBubbleOut(draggedOutView, 25, 25);
|
||||
|
||||
// Magnet to dismiss, verify the bubble is at the dismiss target and the callback was
|
||||
// called.
|
||||
mExpandedController.magnetBubbleToDismiss(
|
||||
mViews.get(0), 100 /* velX */, 100 /* velY */, 1000 /* destY */, after);
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
|
||||
verify(after).run();
|
||||
assertEquals(1000, mViews.get(0).getTranslationY(), .1f);
|
||||
|
||||
// Dismiss the now-magneted bubble, verify that the callback was called.
|
||||
final Runnable afterDismiss = Mockito.mock(Runnable.class);
|
||||
mExpandedController.dismissDraggedOutBubble(afterDismiss);
|
||||
waitForPropertyAnimations(DynamicAnimation.ALPHA);
|
||||
verify(after).run();
|
||||
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
|
||||
|
||||
assertEquals(mBubblePadding, mViews.get(1).getTranslationX(), 1f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMagnetToDismiss_demagnetizeThenDrag() throws InterruptedException {
|
||||
expand();
|
||||
|
||||
final View draggedOutView = mViews.get(0);
|
||||
final Runnable after = Mockito.mock(Runnable.class);
|
||||
|
||||
mExpandedController.prepareForBubbleDrag(draggedOutView);
|
||||
mExpandedController.dragBubbleOut(draggedOutView, 25, 25);
|
||||
|
||||
// Magnet to dismiss, verify the bubble is at the dismiss target and the callback was
|
||||
// called.
|
||||
mExpandedController.magnetBubbleToDismiss(
|
||||
draggedOutView, 100 /* velX */, 100 /* velY */, 1000 /* destY */, after);
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
|
||||
verify(after).run();
|
||||
assertEquals(1000, mViews.get(0).getTranslationY(), .1f);
|
||||
|
||||
// Demagnetize the bubble towards (25, 25).
|
||||
mExpandedController.demagnetizeBubbleTo(25 /* x */, 25 /* y */, 100, 100);
|
||||
|
||||
// Start dragging towards (20, 20).
|
||||
mExpandedController.dragBubbleOut(draggedOutView, 20, 20);
|
||||
|
||||
// Since we just demagnetized, the bubble shouldn't be at (20, 20), it should be animating
|
||||
// towards it.
|
||||
assertNotEquals(20, draggedOutView.getTranslationX());
|
||||
assertNotEquals(20, draggedOutView.getTranslationY());
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
|
||||
|
||||
// Waiting for the animations should result in the bubble ending at (20, 20) since the
|
||||
// animation end value was updated.
|
||||
assertEquals(20, draggedOutView.getTranslationX(), 1f);
|
||||
assertEquals(20, draggedOutView.getTranslationY(), 1f);
|
||||
|
||||
// Drag to (30, 30).
|
||||
mExpandedController.dragBubbleOut(draggedOutView, 30, 30);
|
||||
|
||||
// It should go there instantly since the animations finished.
|
||||
assertEquals(30, draggedOutView.getTranslationX(), 1f);
|
||||
assertEquals(30, draggedOutView.getTranslationY(), 1f);
|
||||
}
|
||||
|
||||
/** Expand the stack and wait for animations to finish. */
|
||||
private void expand() throws InterruptedException {
|
||||
mExpandedController.expandFromStack(mExpansionPoint, Mockito.mock(Runnable.class));
|
||||
|
||||
@@ -195,9 +195,11 @@ public class PhysicsAnimationLayoutTestCase extends SysuiTestCase {
|
||||
|
||||
@Override
|
||||
protected void animateValueForChild(DynamicAnimation.ViewProperty property, View view,
|
||||
float value, float startVel, long startDelay, Runnable[] afterCallbacks) {
|
||||
float value, float startVel, long startDelay, float stiffness,
|
||||
float dampingRatio, Runnable[] afterCallbacks) {
|
||||
mMainThreadHandler.post(() -> super.animateValueForChild(
|
||||
property, view, value, startVel, startDelay, afterCallbacks));
|
||||
property, view, value, startVel, startDelay, stiffness, dampingRatio,
|
||||
afterCallbacks));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
package com.android.systemui.bubbles.animation;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.graphics.PointF;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
@@ -33,6 +35,7 @@ import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.Spy;
|
||||
|
||||
@SmallTest
|
||||
@@ -223,6 +226,59 @@ public class StackAnimationControllerTest extends PhysicsAnimationLayoutTestCase
|
||||
assertEquals(prevStackPos, mStackController.getStackPosition());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMagnetToDismiss_dismiss() throws InterruptedException {
|
||||
final Runnable after = Mockito.mock(Runnable.class);
|
||||
|
||||
// Magnet to dismiss, verify the stack is at the dismiss target and the callback was
|
||||
// called.
|
||||
mStackController.magnetToDismiss(100 /* velX */, 100 /* velY */, 1000 /* destY */, after);
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
|
||||
verify(after).run();
|
||||
assertEquals(1000, mViews.get(0).getTranslationY(), .1f);
|
||||
|
||||
// Dismiss the stack, verify that the callback was called.
|
||||
final Runnable afterImplode = Mockito.mock(Runnable.class);
|
||||
mStackController.implodeStack(afterImplode);
|
||||
waitForPropertyAnimations(
|
||||
DynamicAnimation.ALPHA, DynamicAnimation.SCALE_X, DynamicAnimation.SCALE_Y);
|
||||
verify(after).run();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMagnetToDismiss_demagnetizeThenDrag() throws InterruptedException {
|
||||
final Runnable after = Mockito.mock(Runnable.class);
|
||||
|
||||
// Magnet to dismiss, verify the stack is at the dismiss target and the callback was
|
||||
// called.
|
||||
mStackController.magnetToDismiss(100 /* velX */, 100 /* velY */, 1000 /* destY */, after);
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
|
||||
verify(after).run();
|
||||
|
||||
assertEquals(1000, mViews.get(0).getTranslationY(), .1f);
|
||||
|
||||
// Demagnetize towards (25, 25) and then send a touch event.
|
||||
mStackController.demagnetizeFromDismissToPoint(25, 25, 0, 0);
|
||||
waitForLayoutMessageQueue();
|
||||
mStackController.moveStackFromTouch(20, 20);
|
||||
|
||||
// Since the stack is demagnetizing, it shouldn't be at the stack position yet.
|
||||
assertNotEquals(20, mStackController.getStackPosition().x, 1f);
|
||||
assertNotEquals(20, mStackController.getStackPosition().y, 1f);
|
||||
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
|
||||
|
||||
// Once the animation is done it should end at the touch position coordinates.
|
||||
assertEquals(20, mStackController.getStackPosition().x, 1f);
|
||||
assertEquals(20, mStackController.getStackPosition().y, 1f);
|
||||
|
||||
mStackController.moveStackFromTouch(30, 30);
|
||||
|
||||
// Touches after the animation are done should change the stack position instantly.
|
||||
assertEquals(30, mStackController.getStackPosition().x, 1f);
|
||||
assertEquals(30, mStackController.getStackPosition().y, 1f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks every child view to make sure it's stacked at the given coordinates, off to the left
|
||||
* or right side depending on offset multiplier.
|
||||
@@ -249,5 +305,13 @@ public class StackAnimationControllerTest extends PhysicsAnimationLayoutTestCase
|
||||
super.flingThenSpringFirstBubbleWithStackFollowing(
|
||||
property, vel, friction, spring, finalPosition));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void springFirstBubbleWithStackFollowing(DynamicAnimation.ViewProperty property,
|
||||
SpringForce spring, float vel, float finalPosition) {
|
||||
mMainThreadHandler.post(() ->
|
||||
super.springFirstBubbleWithStackFollowing(
|
||||
property, spring, vel, finalPosition));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user