diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt index 43e21f4085..39d1ed70d2 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt @@ -34,4 +34,8 @@ data class BubbleBarBubble( ) : BubbleBarItem(info.key, view) /** Represents the overflow bubble in the bubble bar. */ -data class BubbleBarOverflow(override var view: BubbleView) : BubbleBarItem("Overflow", view) +data class BubbleBarOverflow(override var view: BubbleView) : BubbleBarItem(KEY, view) { + companion object { + const val KEY = "Overflow" + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java index c7c63e8f86..50cafe0550 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java @@ -44,6 +44,7 @@ import androidx.dynamicanimation.animation.SpringForce; import com.android.launcher3.R; import com.android.launcher3.anim.SpringAnimationBuilder; +import com.android.launcher3.taskbar.bubbles.animation.BubbleAnimator; import com.android.launcher3.util.DisplayController; import com.android.wm.shell.Flags; import com.android.wm.shell.common.bubbles.BubbleBarLocation; @@ -101,8 +102,6 @@ public class BubbleBarView extends FrameLayout { // During fade in animation we shift the bubble bar 1/60th of the screen width private static final float FADE_IN_ANIM_POSITION_SHIFT = 1 / 60f; - private static final int SCALE_IN_ANIMATION_DURATION_MS = 250; - /** * Custom property to set alpha value for the bar view while a bubble is being dragged. * Skips applying alpha to the dragged bubble. @@ -161,11 +160,12 @@ public class BubbleBarView extends FrameLayout { // collapsed state and 1 to the fully expanded state. private final ValueAnimator mWidthAnimator = ValueAnimator.ofFloat(0, 1); - /** An animator used for scaling in a new bubble to the bubble bar while expanded. */ + /** An animator used for animating individual bubbles in the bubble bar while expanded. */ @Nullable - private ValueAnimator mNewBubbleScaleInAnimator = null; + private BubbleAnimator mBubbleAnimator = null; @Nullable private ValueAnimator mScalePaddingAnimator; + @Nullable private Animator mBubbleBarLocationAnimator = null; @@ -670,38 +670,37 @@ public class BubbleBarView extends FrameLayout { bubble.setScaleX(0f); bubble.setScaleY(0f); addView(bubble, 0, lp); - createNewBubbleScaleInAnimator(bubble); - mNewBubbleScaleInAnimator.start(); + + mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing, + getChildCount(), mBubbleBarLocation.isOnLeft(isLayoutRtl())); + BubbleAnimator.Listener listener = new BubbleAnimator.Listener() { + + @Override + public void onAnimationEnd() { + updateWidth(); + mBubbleAnimator = null; + } + + @Override + public void onAnimationCancel() { + bubble.setScaleX(1); + bubble.setScaleY(1); + } + + @Override + public void onAnimationUpdate(float animatedFraction) { + bubble.setScaleX(animatedFraction); + bubble.setScaleY(animatedFraction); + updateBubblesLayoutProperties(mBubbleBarLocation); + invalidate(); + } + }; + mBubbleAnimator.animateNewBubble(indexOfChild(mSelectedBubbleView), listener); } else { addView(bubble, 0, lp); } } - private void createNewBubbleScaleInAnimator(View bubble) { - mNewBubbleScaleInAnimator = ValueAnimator.ofFloat(0, 1); - mNewBubbleScaleInAnimator.setDuration(SCALE_IN_ANIMATION_DURATION_MS); - mNewBubbleScaleInAnimator.addUpdateListener(animation -> { - float animatedFraction = animation.getAnimatedFraction(); - bubble.setScaleX(animatedFraction); - bubble.setScaleY(animatedFraction); - updateBubblesLayoutProperties(mBubbleBarLocation); - invalidate(); - }); - mNewBubbleScaleInAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationCancel(Animator animation) { - bubble.setScaleX(1); - bubble.setScaleY(1); - } - - @Override - public void onAnimationEnd(Animator animation) { - updateWidth(); - mNewBubbleScaleInAnimator = null; - } - }); - } - // TODO: (b/280605790) animate it @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { @@ -716,6 +715,50 @@ public class BubbleBarView extends FrameLayout { updateContentDescription(); } + /** Removes the given bubble from the bubble bar. */ + public void removeBubble(View bubble) { + if (isExpanded()) { + // TODO b/347062801 - animate the bubble bar if the last bubble is removed + int bubbleCount = getChildCount(); + mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing, + bubbleCount, mBubbleBarLocation.isOnLeft(isLayoutRtl())); + BubbleAnimator.Listener listener = new BubbleAnimator.Listener() { + + @Override + public void onAnimationEnd() { + removeView(bubble); + mBubbleAnimator = null; + } + + @Override + public void onAnimationCancel() { + bubble.setScaleX(0); + bubble.setScaleY(0); + } + + @Override + public void onAnimationUpdate(float animatedFraction) { + bubble.setScaleX(1 - animatedFraction); + bubble.setScaleY(1 - animatedFraction); + updateBubblesLayoutProperties(mBubbleBarLocation); + invalidate(); + } + }; + int bubbleIndex = indexOfChild(bubble); + BubbleView lastBubble = (BubbleView) getChildAt(bubbleCount - 1); + String lastBubbleKey = lastBubble.getBubble().getKey(); + boolean removingLastBubble = + BubbleBarOverflow.KEY.equals(lastBubbleKey) + ? bubbleIndex == bubbleCount - 2 + : bubbleIndex == bubbleCount - 1; + mBubbleAnimator.animateRemovedBubble( + indexOfChild(bubble), indexOfChild(mSelectedBubbleView), removingLastBubble, + listener); + } else { + removeView(bubble); + } + } + // TODO: (b/283309949) animate it @Override public void removeView(View view) { @@ -781,9 +824,14 @@ public class BubbleBarView extends FrameLayout { bv.setDragTranslationX(0f); bv.setOffsetX(0f); - bv.setScaleX(mIconScale); - bv.setScaleY(mIconScale); + if (mBubbleAnimator == null || !mBubbleAnimator.isRunning()) { + // if the bubble animator is running don't set scale here, it will be set by the + // animator + bv.setScaleX(mIconScale); + bv.setScaleY(mIconScale); + } bv.setTranslationY(ty); + // the position of the bubble when the bar is fully expanded final float expandedX = getExpandedBubbleTranslationX(i, bubbleCount, onLeft); // the position of the bubble when the bar is fully collapsed @@ -861,9 +909,8 @@ public class BubbleBarView extends FrameLayout { } final float iconAndSpacing = getScaledIconSize() + mExpandedBarIconsSpacing; float translationX; - if (mNewBubbleScaleInAnimator != null && mNewBubbleScaleInAnimator.isRunning()) { - translationX = getExpandedBubbleTranslationXDuringScaleAnimation( - bubbleIndex, bubbleCount, onLeft); + if (mBubbleAnimator != null && mBubbleAnimator.isRunning()) { + return mBubbleAnimator.getExpandedBubbleTranslationX(bubbleIndex) + mBubbleBarPadding; } else if (onLeft) { translationX = mBubbleBarPadding + (bubbleCount - bubbleIndex - 1) * iconAndSpacing; } else { @@ -872,51 +919,6 @@ public class BubbleBarView extends FrameLayout { return translationX - getScaleIconShift(); } - /** - * Returns the translation X for the bubble at index {@code bubbleIndex} when the bubble bar is - * expanded and a new bubble is animating in. - * - *
This method assumes that the animation is running so callers are expected to verify that - * before calling it. - */ - private float getExpandedBubbleTranslationXDuringScaleAnimation( - int bubbleIndex, int bubbleCount, boolean onLeft) { - // when the new bubble scale animation is running, a new bubble is animating in while the - // bubble bar is expanded, so we have at least 2 bubbles in the bubble bar - the expanded - // one, and the new one animating in. - - if (mNewBubbleScaleInAnimator == null) { - // callers of this method are expected to verify that the animation is running, but the - // compiler doesn't know that. - return 0; - } - final float iconAndSpacing = getScaledIconSize() + mExpandedBarIconsSpacing; - final float newBubbleScale = mNewBubbleScaleInAnimator.getAnimatedFraction(); - // the new bubble is scaling in from the center, so we need to adjust its translation so - // that the distance to the adjacent bubble scales at the same rate. - final float pivotAdjustment = -(1 - newBubbleScale) * getScaledIconSize() / 2f; - - if (onLeft) { - if (bubbleIndex == 0) { - // this is the animating bubble. use scaled spacing between it and the bubble to - // its left - return (bubbleCount - 1) * getScaledIconSize() - + (bubbleCount - 2) * mExpandedBarIconsSpacing - + newBubbleScale * mExpandedBarIconsSpacing - + pivotAdjustment; - } - // when the bubble bar is on the left, only the translation of the right-most bubble - // is affected by the scale animation. - return (bubbleCount - bubbleIndex - 1) * iconAndSpacing; - } else if (bubbleIndex == 0) { - // the bubble bar is on the right, and this is the animating bubble. it only needs - // to be adjusted for the scaling pivot. - return pivotAdjustment; - } else { - return iconAndSpacing * (bubbleIndex - 1 + newBubbleScale); - } - } - private float getCollapsedBubbleTranslationX(int bubbleIndex, int bubbleCount, boolean onLeft) { if (bubbleIndex < 0 || bubbleIndex >= bubbleCount) { @@ -979,9 +981,11 @@ public class BubbleBarView extends FrameLayout { BubbleView previouslySelectedBubble = mSelectedBubbleView; mSelectedBubbleView = view; mBubbleBarBackground.showArrow(view != null); - // TODO: (b/283309949) remove animation should be implemented first, so than arrow - // animation is adjusted, skip animation for now - updateArrowForSelected(previouslySelectedBubble != null); + + // if bubbles are being animated, the arrow position will be set as part of the animation + if (mBubbleAnimator == null) { + updateArrowForSelected(previouslySelectedBubble != null); + } } /** @@ -1036,6 +1040,9 @@ public class BubbleBarView extends FrameLayout { } private float arrowPositionForSelectedWhenExpanded(BubbleBarLocation bubbleBarLocation) { + if (mBubbleAnimator != null && mBubbleAnimator.isRunning()) { + return mBubbleAnimator.getArrowPosition() + mBubbleBarPadding; + } final int index = indexOfChild(mSelectedBubbleView); final float selectedBubbleTranslationX = getExpandedBubbleTranslationX( index, getChildCount(), bubbleBarLocation.isOnLeft(isLayoutRtl())); @@ -1101,20 +1108,14 @@ public class BubbleBarView extends FrameLayout { */ public float expandedWidth() { final int childCount = getChildCount(); - // spaces amount is less than child count by 1, or 0 if no child views - final float totalSpace; - final float totalIconSize; - if (mNewBubbleScaleInAnimator != null && mNewBubbleScaleInAnimator.isRunning()) { - // when this animation is running, a new bubble is animating in while the bubble bar is - // expanded, so we have at least 2 bubbles in the bubble bar. - final float newBubbleScale = mNewBubbleScaleInAnimator.getAnimatedFraction(); - totalSpace = (childCount - 2 + newBubbleScale) * mExpandedBarIconsSpacing; - totalIconSize = (childCount - 1 + newBubbleScale) * getScaledIconSize(); - } else { - totalSpace = Math.max(childCount - 1, 0) * mExpandedBarIconsSpacing; - totalIconSize = childCount * getScaledIconSize(); + final float horizontalPadding = 2 * mBubbleBarPadding; + if (mBubbleAnimator != null && mBubbleAnimator.isRunning()) { + return mBubbleAnimator.getExpandedWidth() + horizontalPadding; } - return totalIconSize + totalSpace + 2 * mBubbleBarPadding; + // spaces amount is less than child count by 1, or 0 if no child views + final float totalSpace = Math.max(childCount - 1, 0) * mExpandedBarIconsSpacing; + final float totalIconSize = childCount * getScaledIconSize(); + return totalIconSize + totalSpace + horizontalPadding; } private float collapsedWidth() { @@ -1165,7 +1166,6 @@ public class BubbleBarView extends FrameLayout { return mIsAnimatingNewBubble; } - private boolean hasOverview() { // Overview is always the last bubble View lastChild = getChildAt(getChildCount() - 1); diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java index eec095df56..c93b7ec3a4 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java @@ -388,7 +388,7 @@ public class BubbleBarViewController { */ public void removeBubble(BubbleBarItem b) { if (b != null) { - mBarView.removeView(b.getView()); + mBarView.removeBubble(b.getView()); } else { Log.w(TAG, "removeBubble, bubble was null!"); } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt new file mode 100644 index 0000000000..76727438ec --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt @@ -0,0 +1,297 @@ +/* + * 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 androidx.core.animation.Animator +import androidx.core.animation.ValueAnimator + +/** + * Animates individual bubbles within the bubble bar while the bubble bar is expanded. + * + * This class should only be kept for the duration of the animation and a new instance should be + * created for each animation. + */ +class BubbleAnimator( + private val iconSize: Float, + private val expandedBarIconSpacing: Float, + private val bubbleCount: Int, + private val onLeft: Boolean, +) { + + companion object { + const val ANIMATION_DURATION_MS = 250L + } + + private var state: State = State.Idle + private lateinit var animator: ValueAnimator + + fun animateNewBubble(selectedBubbleIndex: Int, listener: Listener) { + animator = createAnimator(listener) + state = State.AddingBubble(selectedBubbleIndex) + animator.start() + } + + fun animateRemovedBubble( + bubbleIndex: Int, + selectedBubbleIndex: Int, + removingLastBubble: Boolean, + listener: Listener + ) { + animator = createAnimator(listener) + state = State.RemovingBubble(bubbleIndex, selectedBubbleIndex, removingLastBubble) + animator.start() + } + + private fun createAnimator(listener: Listener): ValueAnimator { + val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(ANIMATION_DURATION_MS) + animator.addUpdateListener { animation -> + val animatedFraction = (animation as ValueAnimator).animatedFraction + listener.onAnimationUpdate(animatedFraction) + } + animator.addListener( + object : Animator.AnimatorListener { + + override fun onAnimationCancel(animation: Animator) { + listener.onAnimationCancel() + } + + override fun onAnimationEnd(animation: Animator) { + state = State.Idle + listener.onAnimationEnd() + } + + override fun onAnimationRepeat(animation: Animator) {} + + override fun onAnimationStart(animation: Animator) {} + } + ) + return animator + } + + /** + * The translation X of the bubble at index [bubbleIndex] according to the progress of the + * animation. + * + * Callers should verify that the animation is running before calling this. + * + * @see isRunning + */ + fun getExpandedBubbleTranslationX(bubbleIndex: Int): Float { + return when (val state = state) { + State.Idle -> 0f + is State.AddingBubble -> + getExpandedBubbleTranslationXWhileScalingBubble( + bubbleIndex = bubbleIndex, + scalingBubbleIndex = 0, + bubbleScale = animator.animatedFraction + ) + is State.RemovingBubble -> + getExpandedBubbleTranslationXWhileScalingBubble( + bubbleIndex = bubbleIndex, + scalingBubbleIndex = state.bubbleIndex, + bubbleScale = 1 - animator.animatedFraction + ) + } + } + + /** + * The expanded width of the bubble bar according to the progress of the animation. + * + * Callers should verify that the animation is running before calling this. + * + * @see isRunning + */ + fun getExpandedWidth(): Float { + val bubbleScale = + when (state) { + State.Idle -> 0f + is State.AddingBubble -> animator.animatedFraction + is State.RemovingBubble -> 1 - animator.animatedFraction + } + // When this animator is running the bubble bar is expanded so it's safe to assume that we + // have at least 2 bubbles, but should update the logic to support optional overflow. + // If we're removing the last bubble, the entire bar should animate and we shouldn't get + // here. + val totalSpace = (bubbleCount - 2 + bubbleScale) * expandedBarIconSpacing + val totalIconSize = (bubbleCount - 1 + bubbleScale) * iconSize + return totalIconSize + totalSpace + } + + /** + * Returns the arrow position according to the progress of the animation and, if the selected + * bubble is being removed, accounting to the newly selected bubble. + * + * Callers should verify that the animation is running before calling this. + * + * @see isRunning + */ + fun getArrowPosition(): Float { + return when (val state = state) { + State.Idle -> 0f + is State.AddingBubble -> { + val tx = + getExpandedBubbleTranslationXWhileScalingBubble( + bubbleIndex = state.selectedBubbleIndex, + scalingBubbleIndex = 0, + bubbleScale = animator.animatedFraction + ) + tx + iconSize / 2f + } + is State.RemovingBubble -> getArrowPositionWhenRemovingBubble(state) + } + } + + private fun getArrowPositionWhenRemovingBubble(state: State.RemovingBubble): Float { + return if (state.selectedBubbleIndex != state.bubbleIndex) { + // if we're not removing the selected bubble, the selected bubble doesn't change so just + // return the translation X of the selected bubble and add half icon + val tx = + getExpandedBubbleTranslationXWhileScalingBubble( + bubbleIndex = state.selectedBubbleIndex, + scalingBubbleIndex = state.bubbleIndex, + bubbleScale = 1 - animator.animatedFraction + ) + tx + iconSize / 2f + } else { + // we're removing the selected bubble so the arrow needs to point to a different bubble. + // if we're removing the last bubble the newly selected bubble will be the second to + // last. otherwise, it'll be the next bubble (closer to the overflow) + val iconAndSpacing = iconSize + expandedBarIconSpacing + if (state.removingLastBubble) { + if (onLeft) { + // the newly selected bubble is the bubble to the right. at the end of the + // animation all the bubbles will have shifted left, so the arrow stays at the + // same distance from the left edge of bar + (bubbleCount - state.bubbleIndex - 1) * iconAndSpacing + iconSize / 2f + } else { + // the newly selected bubble is the bubble to the left. at the end of the + // animation all the bubbles will have shifted right, and the arrow would + // eventually be closer to the left edge of the bar by iconAndSpacing + val initialTx = state.bubbleIndex * iconAndSpacing + iconSize / 2f + initialTx - animator.animatedFraction * iconAndSpacing + } + } else { + if (onLeft) { + // the newly selected bubble is to the left, and bubbles are shifting left, so + // move the arrow closer to the left edge of the bar by iconAndSpacing + val initialTx = + (bubbleCount - state.bubbleIndex - 1) * iconAndSpacing + iconSize / 2f + initialTx - animator.animatedFraction * iconAndSpacing + } else { + // the newly selected bubble is to the right, and bubbles are shifting right, so + // the arrow stays at the same distance from the left edge of the bar + state.bubbleIndex * iconAndSpacing + iconSize / 2f + } + } + } + } + + /** + * Returns the translation X for the bubble at index {@code bubbleIndex} when the bubble bar is + * expanded and a bubble is animating in or out. + * + * @param bubbleIndex the index of the bubble for which the translation is requested + * @param scalingBubbleIndex the index of the bubble that is animating + * @param bubbleScale the current scale of the animating bubble + */ + private fun getExpandedBubbleTranslationXWhileScalingBubble( + bubbleIndex: Int, + scalingBubbleIndex: Int, + bubbleScale: Float + ): Float { + val iconAndSpacing = iconSize + expandedBarIconSpacing + // the bubble is scaling from the center, so we need to adjust its translation so + // that the distance to the adjacent bubble scales at the same rate. + val pivotAdjustment = -(1 - bubbleScale) * iconSize / 2f + + return if (onLeft) { + when { + bubbleIndex < scalingBubbleIndex -> + // the bar is on the left and the current bubble is to the right of the scaling + // bubble so account for its scale + (bubbleCount - bubbleIndex - 2 + bubbleScale) * iconAndSpacing + bubbleIndex == scalingBubbleIndex -> { + // the bar is on the left and this is the scaling bubble + val totalIconSize = (bubbleCount - bubbleIndex - 1) * iconSize + // don't count the spacing between the scaling bubble and the bubble on the left + // because we need to scale that space + val totalSpacing = (bubbleCount - bubbleIndex - 2) * expandedBarIconSpacing + val scaledSpace = bubbleScale * expandedBarIconSpacing + totalIconSize + totalSpacing + scaledSpace + pivotAdjustment + } + else -> + // the bar is on the left and the scaling bubble is on the right. the current + // bubble is unaffected by the scaling bubble + (bubbleCount - bubbleIndex - 1) * iconAndSpacing + } + } else { + when { + bubbleIndex < scalingBubbleIndex -> + // the bar is on the right and the scaling bubble is on the right. the current + // bubble is unaffected by the scaling bubble + iconAndSpacing * bubbleIndex + bubbleIndex == scalingBubbleIndex -> + // the bar is on the right, and this is the animating bubble. it only needs to + // be adjusted for the scaling pivot. + iconAndSpacing * bubbleIndex + pivotAdjustment + else -> + // the bar is on the right and the scaling bubble is on the left so account for + // its scale + iconAndSpacing * (bubbleIndex - 1 + bubbleScale) + } + } + } + + val isRunning: Boolean + get() = state != State.Idle + + /** The state of the animation. */ + sealed interface State { + + /** The animation is not running. */ + data object Idle : State + + /** A new bubble is being added to the bubble bar. */ + data class AddingBubble(val selectedBubbleIndex: Int) : State + + /** A bubble is being removed from the bubble bar. */ + data class RemovingBubble( + /** The index of the bubble being removed. */ + val bubbleIndex: Int, + /** The index of the selected bubble. */ + val selectedBubbleIndex: Int, + /** Whether the bubble being removed is also the last bubble. */ + val removingLastBubble: Boolean + ) : State + } + + /** Callbacks for the animation. */ + interface Listener { + + /** + * Notifies the listener of an animation update event, where `animatedFraction` represents + * the progress of the animation starting from 0 and ending at 1. + */ + fun onAnimationUpdate(animatedFraction: Float) + + /** Notifies the listener that the animation was canceled. */ + fun onAnimationCancel() + + /** Notifies that listener that the animation ended. */ + fun onAnimationEnd() + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt new file mode 100644 index 0000000000..20bd617173 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt @@ -0,0 +1,91 @@ +/* + * 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 androidx.core.animation.AnimatorTestRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleAnimatorTest { + + @get:Rule val animatorTestRule = AnimatorTestRule() + + private lateinit var bubbleAnimator: BubbleAnimator + + @Test + fun animateNewBubble_isRunning() { + bubbleAnimator = + BubbleAnimator( + iconSize = 40f, + expandedBarIconSpacing = 10f, + bubbleCount = 5, + onLeft = false + ) + val listener = TestBubbleAnimatorListener() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleAnimator.animateNewBubble(selectedBubbleIndex = 2, listener) + } + + assertThat(bubbleAnimator.isRunning).isTrue() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(250) + } + assertThat(bubbleAnimator.isRunning).isFalse() + } + + @Test + fun animateRemovedBubble_isRunning() { + bubbleAnimator = + BubbleAnimator( + iconSize = 40f, + expandedBarIconSpacing = 10f, + bubbleCount = 5, + onLeft = false + ) + val listener = TestBubbleAnimatorListener() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleAnimator.animateRemovedBubble( + bubbleIndex = 2, + selectedBubbleIndex = 3, + removingLastBubble = false, + listener + ) + } + + assertThat(bubbleAnimator.isRunning).isTrue() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(250) + } + assertThat(bubbleAnimator.isRunning).isFalse() + } + + private class TestBubbleAnimatorListener : BubbleAnimator.Listener { + + override fun onAnimationUpdate(animatedFraction: Float) {} + + override fun onAnimationCancel() {} + + override fun onAnimationEnd() {} + } +}