Snap for 11654651 from 1bc33421de to 24Q3-release

Change-Id: I182405b7193aa209728cc0e581e8275da7c0b84a
This commit is contained in:
Android Build Coastguard Worker
2024-04-01 23:20:46 +00:00
15 changed files with 561 additions and 18 deletions
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 2024 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
<item android:alpha="0.35" android:color="?androidprv:attr/materialColorPrimaryContainer" />
</selector>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 2024 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.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:shape="rectangle">
<corners android:radius="@dimen/bubblebar_drop_target_corner_radius" />
<solid android:color="@color/bubblebar_drop_target_bg_color" />
<stroke
android:width="1dp"
android:color="?androidprv:attr/materialColorPrimaryContainer" />
</shape>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 2024 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.
-->
<View xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="@dimen/bubblebar_size"
android:layout_height="@dimen/bubblebar_size"
android:background="@drawable/bg_bubble_bar_drop_target"
android:elevation="@dimen/bubblebar_elevation" />
+6
View File
@@ -417,6 +417,7 @@
<!-- Container size with pointer included: bubblebar_size + bubblebar_pointer_size -->
<dimen name="bubblebar_size_with_pointer">80dp</dimen>
<dimen name="bubblebar_elevation">1dp</dimen>
<dimen name="bubblebar_drag_elevation">2dp</dimen>
<dimen name="bubblebar_hotseat_adjustment_threshold">90dp</dimen>
<dimen name="bubblebar_icon_size">50dp</dimen>
@@ -432,6 +433,11 @@
<dimen name="bubblebar_dismiss_target_icon_size">24dp</dimen>
<dimen name="bubblebar_dismiss_target_bottom_margin">50dp</dimen>
<dimen name="bubblebar_dismiss_floating_gradient_height">548dp</dimen>
<dimen name="bubblebar_dismiss_zone_width">192dp</dimen>
<dimen name="bubblebar_dismiss_zone_height">242dp</dimen>
<!-- Bubble bar drop target -->
<dimen name="bubblebar_drop_target_corner_radius">36dp</dimen>
<!-- Launcher splash screen -->
<!-- Note: keep this value in sync with the WindowManager/Shell dimens.xml -->
@@ -99,6 +99,7 @@ import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.AutohideSu
import com.android.launcher3.taskbar.TaskbarTranslationController.TransitionCallback;
import com.android.launcher3.taskbar.allapps.TaskbarAllAppsController;
import com.android.launcher3.taskbar.bubbles.BubbleBarController;
import com.android.launcher3.taskbar.bubbles.BubbleBarPinController;
import com.android.launcher3.taskbar.bubbles.BubbleBarView;
import com.android.launcher3.taskbar.bubbles.BubbleBarViewController;
import com.android.launcher3.taskbar.bubbles.BubbleControllers;
@@ -255,7 +256,10 @@ public class TaskbarActivityContext extends BaseTaskbarContext {
new BubbleStashController(this),
new BubbleStashedHandleViewController(this, bubbleHandleView),
new BubbleDragController(this),
new BubbleDismissController(this, mDragLayer)));
new BubbleDismissController(this, mDragLayer),
new BubbleBarPinController(this, mDragLayer,
() -> getDeviceProfile().getDisplayInfo().currentSize)
));
}
// Construct controllers.
@@ -15,6 +15,7 @@
*/
package com.android.launcher3.taskbar.bubbles
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
@@ -27,12 +28,10 @@ import com.android.launcher3.R
import com.android.launcher3.Utilities
import com.android.launcher3.Utilities.mapToRange
import com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound
import com.android.launcher3.taskbar.TaskbarActivityContext
import com.android.wm.shell.common.TriangleShape
/** Drawable for the background of the bubble bar. */
class BubbleBarBackground(context: TaskbarActivityContext, private val backgroundHeight: Float) :
Drawable() {
class BubbleBarBackground(context: Context, private val backgroundHeight: Float) : Drawable() {
private val DARK_THEME_SHADOW_ALPHA = 51f
private val LIGHT_THEME_SHADOW_ALPHA = 25f
@@ -46,6 +45,7 @@ class BubbleBarBackground(context: TaskbarActivityContext, private val backgroun
var arrowPositionX: Float = 0f
private set
private var showingArrow: Boolean = false
private var arrowDrawable: ShapeDrawable
@@ -0,0 +1,104 @@
/*
* Copyright (C) 2024 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.launcher3.taskbar.bubbles
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Point
import android.graphics.RectF
import android.view.Gravity.BOTTOM
import android.view.Gravity.LEFT
import android.view.Gravity.RIGHT
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import androidx.core.view.updateLayoutParams
import com.android.launcher3.R
import com.android.wm.shell.common.bubbles.BaseBubblePinController
import com.android.wm.shell.common.bubbles.BubbleBarLocation
/**
* Controller to manage pinning bubble bar to left or right when dragging starts from the bubble bar
*/
class BubbleBarPinController(
private val context: Context,
private val container: FrameLayout,
private val screenSizeProvider: () -> Point
) : BaseBubblePinController() {
private lateinit var bubbleBarViewController: BubbleBarViewController
private lateinit var bubbleStashController: BubbleStashController
private var dropTargetView: View? = null
fun init(bubbleControllers: BubbleControllers) {
bubbleBarViewController = bubbleControllers.bubbleBarViewController
bubbleStashController = bubbleControllers.bubbleStashController
}
override fun getScreenCenterX(): Int {
return screenSizeProvider.invoke().x / 2
}
override fun getExclusionRect(): RectF {
val rect =
RectF(
0f,
0f,
context.resources.getDimension(R.dimen.bubblebar_dismiss_zone_width),
context.resources.getDimension(R.dimen.bubblebar_dismiss_zone_height)
)
val screenSize = screenSizeProvider.invoke()
val middleX = screenSize.x / 2
// Center it around the bottom center of the screen
rect.offsetTo(middleX - rect.width() / 2, screenSize.y - rect.height())
return rect
}
override fun createDropTargetView(): View {
return LayoutInflater.from(context)
.inflate(R.layout.bubble_bar_drop_target, container, false)
.also { view ->
dropTargetView = view
container.addView(view)
}
}
override fun getDropTargetView(): View? {
return dropTargetView
}
override fun removeDropTargetView(view: View) {
container.removeView(view)
dropTargetView = null
}
@SuppressLint("RtlHardcoded")
override fun updateLocation(location: BubbleBarLocation) {
val onLeft = location.isOnLeft(container.isLayoutRtl)
val bounds = bubbleBarViewController.bubbleBarBounds
val horizontalMargin = bubbleBarViewController.horizontalMargin
dropTargetView?.updateLayoutParams<FrameLayout.LayoutParams> {
width = bounds.width()
height = bounds.height()
gravity = BOTTOM or (if (onLeft) LEFT else RIGHT)
leftMargin = horizontalMargin
rightMargin = horizontalMargin
bottomMargin = -bubbleStashController.bubbleBarTranslationY.toInt()
}
}
}
@@ -39,8 +39,6 @@ import androidx.dynamicanimation.animation.SpringForce;
import com.android.launcher3.R;
import com.android.launcher3.anim.SpringAnimationBuilder;
import com.android.launcher3.taskbar.TaskbarActivityContext;
import com.android.launcher3.views.ActivityContext;
import com.android.wm.shell.common.bubbles.BubbleBarLocation;
import java.util.List;
@@ -159,8 +157,6 @@ public class BubbleBarView extends FrameLayout {
public BubbleBarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
TaskbarActivityContext activityContext = ActivityContext.lookupContext(context);
setAlpha(0);
setVisibility(INVISIBLE);
mIconOverlapAmount = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_overlap);
@@ -171,7 +167,7 @@ public class BubbleBarView extends FrameLayout {
setClipToPadding(false);
mBubbleBarBackground = new BubbleBarBackground(activityContext,
mBubbleBarBackground = new BubbleBarBackground(context,
getResources().getDimensionPixelSize(R.dimen.bubblebar_size));
setBackgroundDrawable(mBubbleBarBackground);
@@ -379,6 +375,36 @@ public class BubbleBarView extends FrameLayout {
return mRelativePivotY;
}
/** Prepares for animating a bubble while being stashed. */
public void prepareForAnimatingBubbleWhileStashed(String bubbleKey) {
// we're about to animate the new bubble in. the new bubble has already been added to this
// view, but we're currently stashed, so before we can start the animation we need make
// everything else in the bubble bar invisible, except for the bubble that's being animated.
setBackground(null);
for (int i = 0; i < getChildCount(); i++) {
final BubbleView view = (BubbleView) getChildAt(i);
final String key = view.getBubble().getKey();
if (!bubbleKey.equals(key)) {
view.setVisibility(INVISIBLE);
}
}
setVisibility(VISIBLE);
setAlpha(1);
setTranslationY(0);
setScaleX(1);
setScaleY(1);
}
/** Resets the state after the bubble animation completed. */
public void onAnimatingBubbleCompleted() {
setBackground(mBubbleBarBackground);
for (int i = 0; i < getChildCount(); i++) {
final BubbleView view = (BubbleView) getChildAt(i);
view.setVisibility(VISIBLE);
view.setAlpha(1f);
}
}
// TODO: (b/280605790) animate it
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
@@ -34,6 +34,7 @@ import com.android.launcher3.taskbar.TaskbarActivityContext;
import com.android.launcher3.taskbar.TaskbarControllers;
import com.android.launcher3.taskbar.TaskbarInsetsController;
import com.android.launcher3.taskbar.TaskbarStashController;
import com.android.launcher3.taskbar.bubbles.animation.BubbleBarViewAnimator;
import com.android.launcher3.util.MultiPropertyFactory;
import com.android.launcher3.util.MultiValueAlpha;
import com.android.quickstep.SystemUiProxy;
@@ -81,6 +82,8 @@ public class BubbleBarViewController {
private boolean mHiddenForNoBubbles = true;
private boolean mShouldShowEducation;
private BubbleBarViewAnimator mBubbleBarViewAnimator;
public BubbleBarViewController(TaskbarActivityContext activity, BubbleBarView barView) {
mActivity = activity;
mBarView = barView;
@@ -113,6 +116,8 @@ public class BubbleBarViewController {
mBarView.addOnLayoutChangeListener((view, i, i1, i2, i3, i4, i5, i6, i7) ->
mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
);
mBubbleBarViewAnimator = new BubbleBarViewAnimator(mBarView, mBubbleStashController);
}
private void onBubbleClicked(View v) {
@@ -316,6 +321,12 @@ public class BubbleBarViewController {
new FrameLayout.LayoutParams(mIconSize, mIconSize, Gravity.LEFT));
b.getView().setOnClickListener(mBubbleClickListener);
mBubbleDragController.setupBubbleView(b.getView());
boolean isStashedOrGone =
mBubbleStashController.isStashed() || mBarView.getVisibility() != VISIBLE;
if (b instanceof BubbleBarBubble && isStashedOrGone) {
mBubbleBarViewAnimator.animateBubbleInForStashed((BubbleBarBubble) b);
}
} else {
Log.w(TAG, "addBubble, bubble was null!");
}
@@ -29,6 +29,7 @@ public class BubbleControllers {
public final BubbleStashedHandleViewController bubbleStashedHandleViewController;
public final BubbleDragController bubbleDragController;
public final BubbleDismissController bubbleDismissController;
public final BubbleBarPinController bubbleBarPinController;
private final RunnableList mPostInitRunnables = new RunnableList();
@@ -43,13 +44,15 @@ public class BubbleControllers {
BubbleStashController bubbleStashController,
BubbleStashedHandleViewController bubbleStashedHandleViewController,
BubbleDragController bubbleDragController,
BubbleDismissController bubbleDismissController) {
BubbleDismissController bubbleDismissController,
BubbleBarPinController bubbleBarPinController) {
this.bubbleBarController = bubbleBarController;
this.bubbleBarViewController = bubbleBarViewController;
this.bubbleStashController = bubbleStashController;
this.bubbleStashedHandleViewController = bubbleStashedHandleViewController;
this.bubbleDragController = bubbleDragController;
this.bubbleDismissController = bubbleDismissController;
this.bubbleBarPinController = bubbleBarPinController;
}
/**
@@ -64,6 +67,7 @@ public class BubbleControllers {
bubbleStashController.init(taskbarControllers, this);
bubbleDragController.init(/* bubbleControllers = */ this);
bubbleDismissController.init(/* bubbleControllers = */ this);
bubbleBarPinController.init(this);
mPostInitRunnables.executeAllAndDestroy();
}
@@ -67,6 +67,9 @@ public class BubbleDismissController {
@Nullable
private BubbleDragAnimator mAnimator;
@Nullable
private Listener mListener;
public BubbleDismissController(TaskbarActivityContext activity, TaskbarDragLayer dragLayer) {
mActivity = activity;
mDragLayer = dragLayer;
@@ -81,6 +84,13 @@ public class BubbleDismissController {
mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
}
/**
* Set listener to be notified of dismiss events
*/
public void setListener(@Nullable Listener listener) {
mListener = listener;
}
/**
* Setup the dismiss view and magnetized object that will be attracted to magnetic target.
* Should be called before handling events or showing/hiding dismiss view.
@@ -189,6 +199,9 @@ public class BubbleDismissController {
@NonNull MagnetizedObject<?> draggedObject) {
if (mAnimator == null) return;
mAnimator.animateDismissCaptured();
if (mListener != null) {
mListener.onStuckToDismissChanged(true /* stuck */);
}
}
@Override
@@ -197,6 +210,9 @@ public class BubbleDismissController {
float velX, float velY, boolean wasFlungOut) {
if (mAnimator == null) return;
mAnimator.animateDismissReleased();
if (mListener != null) {
mListener.onStuckToDismissChanged(false /* stuck */);
}
}
@Override
@@ -206,4 +222,10 @@ public class BubbleDismissController {
}
});
}
/** Interface to receive updates about the dismiss state */
public interface Listener {
/** Called when view is stuck or unstuck from dismiss target */
void onStuckToDismissChanged(boolean stuck);
}
}
@@ -25,10 +25,11 @@ import android.view.ViewConfiguration;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.R;
import com.android.launcher3.taskbar.TaskbarActivityContext;
/**
* Controls bubble bar drag to dismiss interaction.
* Controls bubble bar drag interactions.
* Interacts with {@link BubbleDismissController}, used by {@link BubbleBarViewController}.
* Supported interactions:
* - Drag a single bubble view into dismiss target to remove it.
@@ -39,6 +40,7 @@ public class BubbleDragController {
private final TaskbarActivityContext mActivity;
private BubbleBarViewController mBubbleBarViewController;
private BubbleDismissController mBubbleDismissController;
private BubbleBarPinController mBubbleBarPinController;
public BubbleDragController(TaskbarActivityContext activity) {
mActivity = activity;
@@ -52,6 +54,12 @@ public class BubbleDragController {
public void init(@NonNull BubbleControllers bubbleControllers) {
mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
mBubbleDismissController = bubbleControllers.bubbleDismissController;
mBubbleBarPinController = bubbleControllers.bubbleBarPinController;
mBubbleBarPinController.setListener(location -> {
// TODO(b/330585397): update bubble bar location in shell
});
mBubbleDismissController.setListener(
stuck -> mBubbleBarPinController.setDropTargetHidden(stuck));
}
/**
@@ -88,6 +96,10 @@ public class BubbleDragController {
@SuppressLint("ClickableViewAccessibility")
public void setupBubbleBarView(@NonNull BubbleBarView bubbleBarView) {
PointF initialRelativePivot = new PointF();
final int restingElevation = bubbleBarView.getResources().getDimensionPixelSize(
R.dimen.bubblebar_elevation);
final int dragElevation = bubbleBarView.getResources().getDimensionPixelSize(
R.dimen.bubblebar_drag_elevation);
bubbleBarView.setOnTouchListener(new BubbleTouchListener() {
@Override
protected boolean onTouchDown(@NonNull View view, @NonNull MotionEvent event) {
@@ -102,12 +114,31 @@ public class BubbleDragController {
// By default the bubble bar view pivot is in bottom right corner, while dragging
// it should be centered in order to align it with the dismiss target view
bubbleBarView.setRelativePivot(/* x = */ 0.5f, /* y = */ 0.5f);
bubbleBarView.setElevation(dragElevation);
mBubbleBarPinController.onDragStart(
bubbleBarView.getBubbleBarLocation().isOnLeft(bubbleBarView.isLayoutRtl()));
}
@Override
protected void onDragUpdate(float x, float y) {
mBubbleBarPinController.onDragUpdate(x, y);
}
@Override
protected void onDragRelease() {
mBubbleBarPinController.onDragEnd();
}
@Override
protected void onDragDismiss() {
mBubbleBarPinController.onDragEnd();
}
@Override
void onDragEnd() {
// Restoring the initial pivot for the bubble bar view
bubbleBarView.setRelativePivot(initialRelativePivot.x, initialRelativePivot.y);
bubbleBarView.setElevation(restingElevation);
}
});
}
@@ -169,6 +200,13 @@ public class BubbleDragController {
*/
abstract void onDragStart();
/**
* Called when bubble is dragged to new coordinates.
* Not called while bubble is stuck to the dismiss target.
*/
protected void onDragUpdate(float x, float y) {
}
/**
* Called when the dragging interaction has ended and all the animations have completed
*/
@@ -232,8 +270,10 @@ public class BubbleDragController {
* @param event the motion event
*/
protected void onTouchMove(@NonNull View view, @NonNull MotionEvent event) {
final float dx = event.getRawX() - mTouchDownLocation.x;
final float dy = event.getRawY() - mTouchDownLocation.y;
float rawX = event.getRawX();
float rawY = event.getRawY();
final float dx = rawX - mTouchDownLocation.x;
final float dy = rawY - mTouchDownLocation.y;
switch (mState) {
case TOUCHED:
final boolean movedOut = Math.hypot(dx, dy) > mTouchSlop;
@@ -244,7 +284,7 @@ public class BubbleDragController {
}
break;
case DRAGGING:
drag(view, event, dx, dy);
drag(view, event, dx, dy, rawX, rawY);
break;
}
}
@@ -293,10 +333,12 @@ public class BubbleDragController {
mBubbleDismissController.showDismissView();
}
private void drag(@NonNull View view, @NonNull MotionEvent event, float dx, float dy) {
private void drag(@NonNull View view, @NonNull MotionEvent event, float dx, float dy,
float x, float y) {
if (mBubbleDismissController.handleTouchEvent(event)) return;
view.setTranslationX(mViewInitialPosition.x + dx);
view.setTranslationY(mViewInitialPosition.y + dy);
onDragUpdate(x, y);
}
private void stopDragging(@NonNull View view, @NonNull MotionEvent event) {
@@ -145,7 +145,7 @@ public class BubbleView extends ConstraintLayout {
}
/** Sets the bubble being rendered in this view. */
void setBubble(BubbleBarBubble bubble) {
public void setBubble(BubbleBarBubble bubble) {
mBubble = bubble;
mBubbleIcon.setImageBitmap(bubble.getIcon());
mAppIcon.setImageBitmap(bubble.getBadge());
@@ -159,7 +159,7 @@ public class BubbleView extends ConstraintLayout {
* the list of bubbles. It doesn't show an app icon because it is part of system UI / doesn't
* come from an app.
*/
void setOverflow(BubbleBarOverflow overflow, Bitmap bitmap) {
public void setOverflow(BubbleBarOverflow overflow, Bitmap bitmap) {
mBubble = overflow;
mBubbleIcon.setImageBitmap(bitmap);
mAppIcon.setVisibility(GONE); // Overflow doesn't show the app badge
@@ -168,7 +168,7 @@ public class BubbleView extends ConstraintLayout {
/** Returns the bubble being rendered in this view. */
@Nullable
BubbleBarItem getBubble() {
public BubbleBarItem getBubble() {
return mBubble;
}
@@ -0,0 +1,118 @@
/*
* Copyright (C) 2024 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.launcher3.taskbar.bubbles.animation
import android.view.View
import android.view.View.VISIBLE
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.dynamicanimation.animation.SpringForce
import com.android.launcher3.taskbar.bubbles.BubbleBarBubble
import com.android.launcher3.taskbar.bubbles.BubbleBarView
import com.android.launcher3.taskbar.bubbles.BubbleStashController
import com.android.launcher3.taskbar.bubbles.BubbleView
import com.android.wm.shell.shared.animation.PhysicsAnimator
/** Handles animations for bubble bar bubbles. */
class BubbleBarViewAnimator
@JvmOverloads
constructor(
private val bubbleBarView: BubbleBarView,
private val bubbleStashController: BubbleStashController,
private val scheduler: Scheduler = HandlerScheduler(bubbleBarView)
) {
private companion object {
/** The time to show the flyout. */
const val FLYOUT_DELAY_MS: Long = 2500
/** The translation Y the new bubble will animate to. */
const val BUBBLE_ANIMATION_TRANSLATION_Y = -50f
}
/** An interface for scheduling jobs. */
interface Scheduler {
/** Schedule the given [block] to run. */
fun post(block: () -> Unit)
/** Schedule the given [block] to start with a delay of [delayMillis]. */
fun postDelayed(delayMillis: Long, block: () -> Unit)
}
/** A [Scheduler] that uses a Handler to run jobs. */
private class HandlerScheduler(private val view: View) : Scheduler {
override fun post(block: () -> Unit) {
view.post(block)
}
override fun postDelayed(delayMillis: Long, block: () -> Unit) {
view.postDelayed(block, delayMillis)
}
}
private val springConfig =
PhysicsAnimator.SpringConfig(
stiffness = SpringForce.STIFFNESS_LOW,
dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY
)
/** Animates a bubble for the state where the bubble bar is stashed. */
fun animateBubbleInForStashed(b: BubbleBarBubble) {
val bubbleView = b.view
val animator = PhysicsAnimator.getInstance(bubbleView)
if (animator.isRunning()) animator.cancel()
// the animation of a new bubble is divided into 2 parts. The first part shows the bubble
// and the second part hides it after a delay.
val showAnimation = buildShowAnimation(bubbleView, b.key, animator)
val hideAnimation = buildHideAnimation(animator)
scheduler.post(showAnimation)
scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
}
/** Returns a lambda that starts the animation that shows the new bubble. */
private fun buildShowAnimation(
bubbleView: BubbleView,
key: String,
animator: PhysicsAnimator<BubbleView>
): () -> Unit = {
bubbleBarView.prepareForAnimatingBubbleWhileStashed(key)
animator.setDefaultSpringConfig(springConfig)
animator
.spring(DynamicAnimation.ALPHA, 1f)
.spring(DynamicAnimation.TRANSLATION_Y, BUBBLE_ANIMATION_TRANSLATION_Y)
bubbleView.alpha = 0f
bubbleView.visibility = VISIBLE
animator.start()
}
/** Returns a lambda that starts the animation that hides the new bubble. */
private fun buildHideAnimation(animator: PhysicsAnimator<BubbleView>): () -> Unit = {
animator.setDefaultSpringConfig(springConfig)
animator
.spring(DynamicAnimation.ALPHA, 0f)
.spring(DynamicAnimation.TRANSLATION_Y, 0f)
.addEndListener { _, _, _, canceled, _, _, allRelevantPropertyAnimsEnded ->
if (!canceled && allRelevantPropertyAnimsEnded) {
if (bubbleStashController.isStashed) {
bubbleBarView.alpha = 0f
}
bubbleBarView.onAnimatingBubbleCompleted()
}
}
animator.start()
}
}
@@ -0,0 +1,142 @@
/*
* Copyright (C) 2024 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.launcher3.taskbar.bubbles.animation
import android.content.Context
import android.graphics.Color
import android.graphics.Path
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.widget.FrameLayout
import androidx.core.graphics.drawable.toBitmap
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.R
import com.android.launcher3.taskbar.bubbles.BubbleBarBubble
import com.android.launcher3.taskbar.bubbles.BubbleBarOverflow
import com.android.launcher3.taskbar.bubbles.BubbleBarView
import com.android.launcher3.taskbar.bubbles.BubbleStashController
import com.android.launcher3.taskbar.bubbles.BubbleView
import com.android.wm.shell.common.bubbles.BubbleInfo
import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
@SmallTest
@RunWith(AndroidJUnit4::class)
class BubbleBarViewAnimatorTest {
private val context = ApplicationProvider.getApplicationContext<Context>()
private val animatorScheduler = TestBubbleBarViewAnimatorScheduler()
@Before
fun setUp() {
PhysicsAnimatorTestUtils.prepareForTest()
}
@Test
fun animateBubbleInForStashed() {
lateinit var overflowView: BubbleView
lateinit var bubbleView: BubbleView
lateinit var bubble: BubbleBarBubble
val bubbleBarView = BubbleBarView(context)
InstrumentationRegistry.getInstrumentation().runOnMainSync {
bubbleBarView.layoutParams = FrameLayout.LayoutParams(0, 0)
val inflater = LayoutInflater.from(context)
val bitmap = ColorDrawable(Color.WHITE).toBitmap(width = 20, height = 20)
overflowView =
inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView
overflowView.setOverflow(BubbleBarOverflow(overflowView), bitmap)
bubbleBarView.addView(overflowView)
val bubbleInfo = BubbleInfo("key", 0, null, null, 0, context.packageName, null, false)
bubbleView =
inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView
bubble =
BubbleBarBubble(bubbleInfo, bubbleView, bitmap, bitmap, Color.WHITE, Path(), "")
bubbleView.setBubble(bubble)
bubbleBarView.addView(bubbleView)
}
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
val bubbleStashController = mock<BubbleStashController>()
whenever(bubbleStashController.isStashed).thenReturn(true)
val animator =
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
InstrumentationRegistry.getInstrumentation().runOnMainSync {
animator.animateBubbleInForStashed(bubble)
}
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
assertThat(overflowView.visibility).isEqualTo(INVISIBLE)
assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
assertThat(bubbleView.visibility).isEqualTo(VISIBLE)
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(
DynamicAnimation.ALPHA,
DynamicAnimation.TRANSLATION_Y
)
assertThat(bubbleView.alpha).isEqualTo(1)
assertThat(bubbleView.translationY).isEqualTo(-50)
assertThat(animatorScheduler.delayedBlock).isNotNull()
InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(
DynamicAnimation.ALPHA,
DynamicAnimation.TRANSLATION_Y
)
assertThat(bubbleView.alpha).isEqualTo(1)
assertThat(bubbleView.visibility).isEqualTo(VISIBLE)
assertThat(bubbleView.translationY).isEqualTo(0)
assertThat(bubbleBarView.alpha).isEqualTo(0)
assertThat(overflowView.alpha).isEqualTo(1)
assertThat(overflowView.visibility).isEqualTo(VISIBLE)
}
private class TestBubbleBarViewAnimatorScheduler : BubbleBarViewAnimator.Scheduler {
var delayedBlock: (() -> Unit)? = null
private set
override fun post(block: () -> Unit) {
block.invoke()
}
override fun postDelayed(delayMillis: Long, block: () -> Unit) {
check(delayedBlock == null) { "there is already a pending block waiting to run" }
delayedBlock = block
}
}
}