diff options
Diffstat (limited to 'libs')
7 files changed, 569 insertions, 417 deletions
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetControllerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetControllerTest.kt deleted file mode 100644 index 2ac77917a348..000000000000 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetControllerTest.kt +++ /dev/null @@ -1,180 +0,0 @@ -/* - * 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.wm.shell.bubbles.bar - -import android.content.Context -import android.graphics.Insets -import android.graphics.Rect -import android.view.View -import android.view.WindowManager -import android.widget.FrameLayout -import androidx.core.animation.AnimatorTestRule -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.internal.protolog.common.ProtoLog -import com.android.wm.shell.R -import com.android.wm.shell.bubbles.BubblePositioner -import com.android.wm.shell.bubbles.DeviceConfig -import com.android.wm.shell.bubbles.bar.BubbleBarDropTargetController.Companion.DROP_TARGET_ALPHA_IN_DURATION -import com.android.wm.shell.bubbles.bar.BubbleBarDropTargetController.Companion.DROP_TARGET_ALPHA_OUT_DURATION -import com.android.wm.shell.bubbles.bar.BubbleBarDropTargetController.Companion.DROP_TARGET_SCALE -import com.android.wm.shell.common.bubbles.BubbleBarLocation -import com.google.common.truth.Truth.assertThat -import org.junit.Before -import org.junit.ClassRule -import org.junit.Test -import org.junit.runner.RunWith - -/** Tests for [BubbleBarDropTargetController] */ -@SmallTest -@RunWith(AndroidJUnit4::class) -class BubbleBarDropTargetControllerTest { - - companion object { - @JvmField @ClassRule val animatorTestRule: AnimatorTestRule = AnimatorTestRule() - } - - private val context = ApplicationProvider.getApplicationContext<Context>() - private lateinit var controller: BubbleBarDropTargetController - private lateinit var positioner: BubblePositioner - private lateinit var container: FrameLayout - - @Before - fun setUp() { - ProtoLog.REQUIRE_PROTOLOGTOOL = false - container = FrameLayout(context) - val windowManager = context.getSystemService(WindowManager::class.java) - positioner = BubblePositioner(context, windowManager) - positioner.setShowingInBubbleBar(true) - val deviceConfig = - DeviceConfig( - windowBounds = Rect(0, 0, 2000, 2600), - isLargeScreen = true, - isSmallTablet = false, - isLandscape = true, - isRtl = false, - insets = Insets.of(10, 20, 30, 40) - ) - positioner.update(deviceConfig) - positioner.bubbleBarBounds = Rect(1800, 2400, 1970, 2560) - - controller = BubbleBarDropTargetController(context, container, positioner) - } - - @Test - fun show_moveLeftToRight_isVisibleWithExpectedBounds() { - val expectedBoundsOnLeft = getExpectedDropTargetBounds(onLeft = true) - val expectedBoundsOnRight = getExpectedDropTargetBounds(onLeft = false) - - runOnMainSync { controller.show(BubbleBarLocation.LEFT) } - waitForAnimateIn() - val viewOnLeft = getDropTargetView() - assertThat(viewOnLeft).isNotNull() - assertThat(viewOnLeft!!.alpha).isEqualTo(1f) - assertThat(viewOnLeft.layoutParams.width).isEqualTo(expectedBoundsOnLeft.width()) - assertThat(viewOnLeft.layoutParams.height).isEqualTo(expectedBoundsOnLeft.height()) - assertThat(viewOnLeft.x).isEqualTo(expectedBoundsOnLeft.left) - assertThat(viewOnLeft.y).isEqualTo(expectedBoundsOnLeft.top) - - runOnMainSync { controller.show(BubbleBarLocation.RIGHT) } - waitForAnimateOut() - waitForAnimateIn() - val viewOnRight = getDropTargetView() - assertThat(viewOnRight).isNotNull() - assertThat(viewOnRight!!.alpha).isEqualTo(1f) - assertThat(viewOnRight.layoutParams.width).isEqualTo(expectedBoundsOnRight.width()) - assertThat(viewOnRight.layoutParams.height).isEqualTo(expectedBoundsOnRight.height()) - assertThat(viewOnRight.x).isEqualTo(expectedBoundsOnRight.left) - assertThat(viewOnRight.y).isEqualTo(expectedBoundsOnRight.top) - } - - @Test - fun toggleSetHidden_dropTargetShown_updatesAlpha() { - runOnMainSync { controller.show(BubbleBarLocation.RIGHT) } - waitForAnimateIn() - val view = getDropTargetView() - assertThat(view).isNotNull() - assertThat(view!!.alpha).isEqualTo(1f) - - runOnMainSync { controller.setHidden(true) } - waitForAnimateOut() - val hiddenView = getDropTargetView() - assertThat(hiddenView).isNotNull() - assertThat(hiddenView!!.alpha).isEqualTo(0f) - - runOnMainSync { controller.setHidden(false) } - waitForAnimateIn() - val shownView = getDropTargetView() - assertThat(shownView).isNotNull() - assertThat(shownView!!.alpha).isEqualTo(1f) - } - - @Test - fun toggleSetHidden_dropTargetNotShown_viewNotCreated() { - runOnMainSync { controller.setHidden(true) } - waitForAnimateOut() - assertThat(getDropTargetView()).isNull() - runOnMainSync { controller.setHidden(false) } - waitForAnimateIn() - assertThat(getDropTargetView()).isNull() - } - - @Test - fun dismiss_dropTargetShown_viewRemoved() { - runOnMainSync { controller.show(BubbleBarLocation.LEFT) } - waitForAnimateIn() - assertThat(getDropTargetView()).isNotNull() - runOnMainSync { controller.dismiss() } - waitForAnimateOut() - assertThat(getDropTargetView()).isNull() - } - - @Test - fun dismiss_dropTargetNotShown_doesNothing() { - runOnMainSync { controller.dismiss() } - waitForAnimateOut() - assertThat(getDropTargetView()).isNull() - } - - private fun getDropTargetView(): View? = container.findViewById(R.id.bubble_bar_drop_target) - - private fun getExpectedDropTargetBounds(onLeft: Boolean): Rect { - val rect = Rect() - positioner.getBubbleBarExpandedViewBounds(onLeft, false /* isOveflowExpanded */, rect) - // Scale the rect to expected size, but keep the center point the same - val centerX = rect.centerX() - val centerY = rect.centerY() - rect.scale(DROP_TARGET_SCALE) - rect.offset(centerX - rect.centerX(), centerY - rect.centerY()) - return rect - } - - private fun runOnMainSync(runnable: Runnable) { - InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable) - } - - private fun waitForAnimateIn() { - // Advance animator for on-device test - runOnMainSync { animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_IN_DURATION) } - } - - private fun waitForAnimateOut() { - // Advance animator for on-device test - runOnMainSync { animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_OUT_DURATION) } - } -} diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt new file mode 100644 index 000000000000..e1bf40ca19dc --- /dev/null +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt @@ -0,0 +1,263 @@ +/* + * 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.wm.shell.bubbles.bar + +import android.content.Context +import android.graphics.Insets +import android.graphics.PointF +import android.graphics.Rect +import android.view.View +import android.view.WindowManager +import android.widget.FrameLayout +import androidx.core.animation.AnimatorTestRule +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.internal.protolog.common.ProtoLog +import com.android.wm.shell.R +import com.android.wm.shell.bubbles.BubblePositioner +import com.android.wm.shell.bubbles.DeviceConfig +import com.android.wm.shell.bubbles.bar.BubbleExpandedViewPinController.Companion.DROP_TARGET_SCALE +import com.android.wm.shell.common.bubbles.BaseBubblePinController +import com.android.wm.shell.common.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_IN_DURATION +import com.android.wm.shell.common.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_OUT_DURATION +import com.android.wm.shell.common.bubbles.BubbleBarLocation +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.ClassRule +import org.junit.Test +import org.junit.runner.RunWith + +/** Tests for [BubbleExpandedViewPinController] */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleExpandedViewPinControllerTest { + + companion object { + @JvmField @ClassRule val animatorTestRule: AnimatorTestRule = AnimatorTestRule() + + const val SCREEN_WIDTH = 2000 + const val SCREEN_HEIGHT = 1000 + + const val BUBBLE_BAR_WIDTH = 100 + const val BUBBLE_BAR_HEIGHT = 50 + } + + private val context = ApplicationProvider.getApplicationContext<Context>() + private lateinit var positioner: BubblePositioner + private lateinit var container: FrameLayout + + private lateinit var controller: BubbleExpandedViewPinController + private lateinit var testListener: TestLocationChangeListener + + private val pointOnLeft = PointF(100f, 100f) + private val pointOnRight = PointF(1900f, 500f) + + @Before + fun setUp() { + ProtoLog.REQUIRE_PROTOLOGTOOL = false + container = FrameLayout(context) + val windowManager = context.getSystemService(WindowManager::class.java) + positioner = BubblePositioner(context, windowManager) + positioner.setShowingInBubbleBar(true) + val deviceConfig = + DeviceConfig( + windowBounds = Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT), + isLargeScreen = true, + isSmallTablet = false, + isLandscape = true, + isRtl = false, + insets = Insets.of(10, 20, 30, 40) + ) + positioner.update(deviceConfig) + positioner.bubbleBarBounds = + Rect( + SCREEN_WIDTH - deviceConfig.insets.right - BUBBLE_BAR_WIDTH, + SCREEN_HEIGHT - deviceConfig.insets.bottom - BUBBLE_BAR_HEIGHT, + SCREEN_WIDTH - deviceConfig.insets.right, + SCREEN_HEIGHT - deviceConfig.insets.bottom + ) + + controller = BubbleExpandedViewPinController(context, container, positioner) + testListener = TestLocationChangeListener() + controller.setListener(testListener) + } + + @After + fun tearDown() { + runOnMainSync { controller.onDragEnd() } + waitForAnimateOut() + } + + @Test + fun onDragUpdate_stayOnSameSide() { + runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnRight.x, pointOnRight.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).isEmpty() + } + + @Test + fun onDragUpdate_toLeft() { + runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + + val expectedDropTargetBounds = getExpectedDropTargetBounds(onLeft = true) + assertThat(dropTargetView!!.layoutParams.width).isEqualTo(expectedDropTargetBounds.width()) + assertThat(dropTargetView!!.layoutParams.height) + .isEqualTo(expectedDropTargetBounds.height()) + + assertThat(testListener.locationChanges).containsExactly(BubbleBarLocation.LEFT) + } + + @Test + fun onDragUpdate_toLeftAndBackToRight() { + runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + + runOnMainSync { controller.onDragUpdate(pointOnRight.x, pointOnRight.y) } + // We have to wait for existing drop target to animate out and new to animate in + waitForAnimateOut() + waitForAnimateIn() + + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + + val expectedDropTargetBounds = getExpectedDropTargetBounds(onLeft = false) + assertThat(dropTargetView!!.layoutParams.width).isEqualTo(expectedDropTargetBounds.width()) + assertThat(dropTargetView!!.layoutParams.height) + .isEqualTo(expectedDropTargetBounds.height()) + + assertThat(testListener.locationChanges) + .containsExactly(BubbleBarLocation.LEFT, BubbleBarLocation.RIGHT) + } + + @Test + fun onDragUpdate_toLeftInExclusionRect() { + runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + // Exclusion rect is around the bottom center area of the screen + controller.onDragUpdate(SCREEN_WIDTH / 2f - 50, SCREEN_HEIGHT - 100f) + } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + assertThat(testListener.locationChanges).isEmpty() + } + + @Test + fun toggleSetDropTargetHidden_dropTargetExists() { + runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + + runOnMainSync { controller.setDropTargetHidden(true) } + waitForAnimateOut() + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(0f) + + runOnMainSync { controller.setDropTargetHidden(false) } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + assertThat(dropTargetView!!.alpha).isEqualTo(1f) + } + + @Test + fun toggleSetDropTargetHidden_noDropTarget() { + runOnMainSync { controller.setDropTargetHidden(true) } + waitForAnimateOut() + assertThat(dropTargetView).isNull() + + runOnMainSync { controller.setDropTargetHidden(false) } + waitForAnimateIn() + assertThat(dropTargetView).isNull() + } + + @Test + fun onDragEnd_dropTargetExists() { + runOnMainSync { + controller.onDragStart(initialLocationOnLeft = false) + controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) + } + waitForAnimateIn() + assertThat(dropTargetView).isNotNull() + + runOnMainSync { controller.onDragEnd() } + waitForAnimateOut() + assertThat(dropTargetView).isNull() + } + + @Test + fun onDragEnd_noDropTarget() { + runOnMainSync { controller.onDragEnd() } + waitForAnimateOut() + assertThat(dropTargetView).isNull() + } + + private val dropTargetView: View? + get() = container.findViewById(R.id.bubble_bar_drop_target) + + private fun getExpectedDropTargetBounds(onLeft: Boolean): Rect { + val rect = Rect() + positioner.getBubbleBarExpandedViewBounds(onLeft, false /* isOveflowExpanded */, rect) + // Scale the rect to expected size, but keep the center point the same + val centerX = rect.centerX() + val centerY = rect.centerY() + rect.scale(DROP_TARGET_SCALE) + rect.offset(centerX - rect.centerX(), centerY - rect.centerY()) + return rect + } + + private fun runOnMainSync(runnable: Runnable) { + InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable) + } + + private fun waitForAnimateIn() { + // Advance animator for on-device test + runOnMainSync { animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_IN_DURATION) } + } + + private fun waitForAnimateOut() { + // Advance animator for on-device test + runOnMainSync { animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_OUT_DURATION) } + } + + internal class TestLocationChangeListener : BaseBubblePinController.LocationChangeListener { + val locationChanges = mutableListOf<BubbleBarLocation>() + override fun onChange(location: BubbleBarLocation) { + locationChanges.add(location) + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetController.kt deleted file mode 100644 index f6b4653b8162..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetController.kt +++ /dev/null @@ -1,151 +0,0 @@ -/* - * 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.wm.shell.bubbles.bar - -import android.content.Context -import android.graphics.Rect -import android.view.LayoutInflater -import android.view.View -import android.widget.FrameLayout -import android.widget.FrameLayout.LayoutParams -import androidx.annotation.VisibleForTesting -import androidx.core.animation.Animator -import androidx.core.animation.AnimatorListenerAdapter -import androidx.core.animation.ObjectAnimator -import com.android.wm.shell.R -import com.android.wm.shell.bubbles.BubblePositioner -import com.android.wm.shell.common.bubbles.BubbleBarLocation - -/** Controller to show/hide drop target when bubble bar expanded view is being dragged */ -class BubbleBarDropTargetController( - val context: Context, - val container: FrameLayout, - val positioner: BubblePositioner -) { - - private var dropTargetView: View? = null - private var animator: ObjectAnimator? = null - private val tempRect: Rect by lazy(LazyThreadSafetyMode.NONE) { Rect() } - - /** - * Show drop target at [location] with animation. - * - * If the drop target is currently visible, animates it out first, before showing it at the - * supplied location. - */ - fun show(location: BubbleBarLocation) { - val targetView = dropTargetView ?: createView().also { dropTargetView = it } - if (targetView.alpha > 0) { - targetView.animateOut { - targetView.updateBounds(location) - targetView.animateIn() - } - } else { - targetView.updateBounds(location) - targetView.animateIn() - } - } - - /** - * Set the view hidden or not - * - * Requires the drop target to be first shown by calling [animateIn]. Otherwise does not do - * anything. - */ - fun setHidden(hidden: Boolean) { - val targetView = dropTargetView ?: return - if (hidden) { - targetView.animateOut() - } else { - targetView.animateIn() - } - } - - /** Remove the drop target if it is was shown. */ - fun dismiss() { - dropTargetView?.animateOut { - dropTargetView?.let { container.removeView(it) } - dropTargetView = null - } - } - - private fun createView(): View { - return LayoutInflater.from(context) - .inflate(R.layout.bubble_bar_drop_target, container, false /* attachToRoot */) - .also { view: View -> - view.alpha = 0f - // Add at index 0 to ensure it does not cover the bubble - container.addView(view, 0) - } - } - - private fun getBounds(onLeft: Boolean, out: Rect) { - positioner.getBubbleBarExpandedViewBounds(onLeft, false /* isOverflowExpanded */, out) - val centerX = out.centerX() - val centerY = out.centerY() - out.scale(DROP_TARGET_SCALE) - // Move rect center back to the same position as before scale - out.offset(centerX - out.centerX(), centerY - out.centerY()) - } - - private fun View.updateBounds(location: BubbleBarLocation) { - getBounds(location.isOnLeft(isLayoutRtl), tempRect) - val lp = layoutParams as LayoutParams - lp.width = tempRect.width() - lp.height = tempRect.height() - layoutParams = lp - x = tempRect.left.toFloat() - y = tempRect.top.toFloat() - } - - private fun View.animateIn() { - animator?.cancel() - animator = - ObjectAnimator.ofFloat(this, View.ALPHA, 1f) - .setDuration(DROP_TARGET_ALPHA_IN_DURATION) - .addEndAction { animator = null } - animator?.start() - } - - private fun View.animateOut(endAction: Runnable? = null) { - animator?.cancel() - animator = - ObjectAnimator.ofFloat(this, View.ALPHA, 0f) - .setDuration(DROP_TARGET_ALPHA_OUT_DURATION) - .addEndAction { - endAction?.run() - animator = null - } - animator?.start() - } - - private fun <T : Animator> T.addEndAction(runnable: Runnable): T { - addListener( - object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - runnable.run() - } - } - ) - return this - } - - companion object { - @VisibleForTesting const val DROP_TARGET_ALPHA_IN_DURATION = 150L - @VisibleForTesting const val DROP_TARGET_ALPHA_OUT_DURATION = 100L - @VisibleForTesting const val DROP_TARGET_SCALE = 0.9f - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt index ad97a2411ae0..fe9c4d4c9094 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt @@ -17,12 +17,9 @@ package com.android.wm.shell.bubbles.bar import android.annotation.SuppressLint -import android.graphics.RectF import android.view.MotionEvent import android.view.View -import com.android.wm.shell.R import com.android.wm.shell.bubbles.BubblePositioner -import com.android.wm.shell.common.bubbles.BubbleBarLocation import com.android.wm.shell.common.bubbles.DismissView import com.android.wm.shell.common.bubbles.RelativeTouchListener import com.android.wm.shell.common.magnetictarget.MagnetizedObject @@ -34,6 +31,7 @@ class BubbleBarExpandedViewDragController( private val dismissView: DismissView, private val animationHelper: BubbleBarAnimationHelper, private val bubblePositioner: BubblePositioner, + private val pinController: BubbleExpandedViewPinController, private val dragListener: DragListener ) { @@ -45,8 +43,6 @@ class BubbleBarExpandedViewDragController( private val magnetizedExpandedView: MagnetizedObject<BubbleBarExpandedView> = MagnetizedObject.magnetizeView(expandedView) private val magnetizedDismissTarget: MagnetizedObject.MagneticTarget - private val dismissZoneHeight: Int - private val dismissZoneWidth: Int init { magnetizedExpandedView.magnetListener = MagnetListener() @@ -78,33 +74,11 @@ class BubbleBarExpandedViewDragController( } return@setOnTouchListener dragMotionEventHandler.onTouch(view, event) || magnetConsumed } - - dismissZoneHeight = - dismissView.resources.getDimensionPixelSize(R.dimen.bubble_bar_dismiss_zone_height) - dismissZoneWidth = - dismissView.resources.getDimensionPixelSize(R.dimen.bubble_bar_dismiss_zone_width) } - /** Listener to receive callback about dragging events */ + /** Listener to get notified about drag events */ interface DragListener { /** - * Bubble bar [BubbleBarLocation] has changed as a result of dragging the expanded view. - * - * Triggered when drag gesture passes the middle of the screen and before touch up. Can be - * triggered multiple times per gesture. - * - * @param location new location of the bubble bar as a result of the ongoing drag operation - */ - fun onLocationChanged(location: BubbleBarLocation) - - /** - * Called when bubble bar is moved into or out of the dismiss target - * - * @param isStuck `true` if view is dragged inside dismiss target - */ - fun onStuckToDismissChanged(isStuck: Boolean) - - /** * Bubble bar was released * * @param inDismiss `true` if view was release in dismiss target @@ -115,25 +89,11 @@ class BubbleBarExpandedViewDragController( private inner class HandleDragListener : RelativeTouchListener() { private var isMoving = false - private var screenCenterX: Int = -1 - private var isOnLeft = false - private val dismissZone = RectF() override fun onDown(v: View, ev: MotionEvent): Boolean { // While animating, don't allow new touch events if (expandedView.isAnimating) return false - isOnLeft = bubblePositioner.isBubbleBarOnLeft - - val screenRect = bubblePositioner.screenRect - screenCenterX = screenRect.centerX() - val screenBottom = screenRect.bottom - - dismissZone.set( - screenCenterX - dismissZoneWidth / 2f, - (screenBottom - dismissZoneHeight).toFloat(), - screenCenterX + dismissZoneHeight / 2f, - screenBottom.toFloat() - ) + pinController.onDragStart(bubblePositioner.isBubbleBarOnLeft) return true } @@ -152,19 +112,7 @@ class BubbleBarExpandedViewDragController( expandedView.translationX = expandedViewInitialTranslationX + dx expandedView.translationY = expandedViewInitialTranslationY + dy dismissView.show() - - // Check if we are in the zone around dismiss view where drag can only lead to dismiss - if (dismissZone.contains(ev.rawX, ev.rawY)) { - return - } - - if (isOnLeft && ev.rawX > screenCenterX) { - isOnLeft = false - dragListener.onLocationChanged(BubbleBarLocation.RIGHT) - } else if (!isOnLeft && ev.rawX < screenCenterX) { - isOnLeft = true - dragListener.onLocationChanged(BubbleBarLocation.LEFT) - } + pinController.onDragUpdate(ev.rawX, ev.rawY) } override fun onUp( @@ -188,6 +136,7 @@ class BubbleBarExpandedViewDragController( private fun finishDrag() { if (!isStuckToDismiss) { animationHelper.animateToRestPosition() + pinController.onDragEnd() dragListener.onReleased(inDismiss = false) dismissView.hide() } @@ -201,7 +150,7 @@ class BubbleBarExpandedViewDragController( draggedObject: MagnetizedObject<*> ) { isStuckToDismiss = true - dragListener.onStuckToDismissChanged(isStuck = true) + pinController.setDropTargetHidden(true) } override fun onUnstuckFromTarget( @@ -213,7 +162,7 @@ class BubbleBarExpandedViewDragController( ) { isStuckToDismiss = false animationHelper.animateUnstuckFromDismissView(target) - dragListener.onStuckToDismissChanged(isStuck = false) + pinController.setDropTargetHidden(false) } override fun onReleasedInTarget( @@ -221,6 +170,7 @@ class BubbleBarExpandedViewDragController( draggedObject: MagnetizedObject<*> ) { dragListener.onReleased(inDismiss = true) + pinController.onDragEnd() dismissView.hide() } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index 88ccc9267c41..62cc4da3193e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -33,8 +33,6 @@ import android.view.ViewTreeObserver; import android.view.WindowManager; import android.widget.FrameLayout; -import androidx.annotation.NonNull; - import com.android.wm.shell.bubbles.Bubble; import com.android.wm.shell.bubbles.BubbleController; import com.android.wm.shell.bubbles.BubbleData; @@ -44,7 +42,6 @@ import com.android.wm.shell.bubbles.BubbleViewProvider; import com.android.wm.shell.bubbles.DeviceConfig; import com.android.wm.shell.bubbles.DismissViewUtils; import com.android.wm.shell.bubbles.bar.BubbleBarExpandedViewDragController.DragListener; -import com.android.wm.shell.common.bubbles.BubbleBarLocation; import com.android.wm.shell.common.bubbles.DismissView; import kotlin.Unit; @@ -71,7 +68,7 @@ public class BubbleBarLayerView extends FrameLayout private final BubbleBarAnimationHelper mAnimationHelper; private final BubbleEducationViewController mEducationViewController; private final View mScrimView; - private final BubbleBarDropTargetController mDropTargetController; + private final BubbleExpandedViewPinController mBubbleExpandedViewPinController; @Nullable private BubbleViewProvider mExpandedBubble; @@ -116,7 +113,9 @@ public class BubbleBarLayerView extends FrameLayout setUpDismissView(); - mDropTargetController = new BubbleBarDropTargetController(context, this, mPositioner); + mBubbleExpandedViewPinController = new BubbleExpandedViewPinController( + context, this, mPositioner); + mBubbleExpandedViewPinController.setListener(mBubbleController::setBubbleBarLocation); setOnClickListener(view -> hideMenuOrCollapse()); } @@ -207,12 +206,17 @@ public class BubbleBarLayerView extends FrameLayout } }); - DragListener dragListener = createDragListener(); + DragListener dragListener = inDismiss -> { + if (inDismiss && mExpandedBubble != null) { + mBubbleController.dismissBubble(mExpandedBubble.getKey(), DISMISS_USER_GESTURE); + } + }; mDragController = new BubbleBarExpandedViewDragController( mExpandedView, mDismissView, mAnimationHelper, mPositioner, + mBubbleExpandedViewPinController, dragListener); addView(mExpandedView, new LayoutParams(width, height, Gravity.LEFT)); @@ -377,26 +381,4 @@ public class BubbleBarLayerView extends FrameLayout } } - private DragListener createDragListener() { - return new DragListener() { - @Override - public void onLocationChanged(@NonNull BubbleBarLocation location) { - mBubbleController.setBubbleBarLocation(location); - mDropTargetController.show(location); - } - - @Override - public void onStuckToDismissChanged(boolean isStuck) { - mDropTargetController.setHidden(isStuck); - } - - @Override - public void onReleased(boolean inDismiss) { - mDropTargetController.dismiss(); - if (inDismiss && mExpandedBubble != null) { - mBubbleController.dismissBubble(mExpandedBubble.getKey(), DISMISS_USER_GESTURE); - } - } - }; - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinController.kt new file mode 100644 index 000000000000..5d391eca070c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinController.kt @@ -0,0 +1,109 @@ +/* + * 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.wm.shell.bubbles.bar + +import android.content.Context +import android.graphics.Rect +import android.graphics.RectF +import android.view.LayoutInflater +import android.view.View +import android.widget.FrameLayout +import androidx.annotation.VisibleForTesting +import androidx.core.view.updateLayoutParams +import com.android.wm.shell.R +import com.android.wm.shell.bubbles.BubblePositioner +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 + * expanded view + */ +class BubbleExpandedViewPinController( + private val context: Context, + private val container: FrameLayout, + private val positioner: BubblePositioner +) : BaseBubblePinController() { + + private var dropTargetView: View? = null + private val tempRect: Rect by lazy(LazyThreadSafetyMode.NONE) { Rect() } + + override fun getScreenCenterX(): Int { + return positioner.screenRect.centerX() + } + + override fun getExclusionRect(): RectF { + val rect = + RectF( + 0f, + 0f, + context.resources.getDimension(R.dimen.bubble_bar_dismiss_zone_width), + context.resources.getDimension(R.dimen.bubble_bar_dismiss_zone_height) + ) + + val screenRect = positioner.screenRect + // Center it around the bottom center of the screen + rect.offsetTo( + screenRect.exactCenterX() - rect.width() / 2f, + screenRect.bottom - rect.height() + ) + return rect + } + + override fun createDropTargetView(): View { + return LayoutInflater.from(context) + .inflate(R.layout.bubble_bar_drop_target, container, false /* attachToRoot */) + .also { view: View -> + dropTargetView = view + // Add at index 0 to ensure it does not cover the bubble + container.addView(view, 0) + } + } + + override fun getDropTargetView(): View? { + return dropTargetView + } + + override fun removeDropTargetView(view: View) { + container.removeView(view) + dropTargetView = null + } + + override fun updateLocation(location: BubbleBarLocation) { + val view = dropTargetView ?: return + getBounds(location.isOnLeft(view.isLayoutRtl), tempRect) + view.updateLayoutParams<FrameLayout.LayoutParams> { + width = tempRect.width() + height = tempRect.height() + } + view.x = tempRect.left.toFloat() + view.y = tempRect.top.toFloat() + } + + private fun getBounds(onLeft: Boolean, out: Rect) { + positioner.getBubbleBarExpandedViewBounds(onLeft, false /* isOverflowExpanded */, out) + val centerX = out.centerX() + val centerY = out.centerY() + out.scale(DROP_TARGET_SCALE) + // Move rect center back to the same position as before scale + out.offset(centerX - out.centerX(), centerY - out.centerY()) + } + + companion object { + @VisibleForTesting const val DROP_TARGET_SCALE = 0.9f + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BaseBubblePinController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BaseBubblePinController.kt new file mode 100644 index 000000000000..630ad6e7cafe --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BaseBubblePinController.kt @@ -0,0 +1,179 @@ +/* + * 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.wm.shell.common.bubbles + +import android.graphics.RectF +import android.view.View +import androidx.annotation.VisibleForTesting +import androidx.core.animation.Animator +import androidx.core.animation.AnimatorListenerAdapter +import androidx.core.animation.ObjectAnimator +import com.android.wm.shell.common.bubbles.BaseBubblePinController.LocationChangeListener +import com.android.wm.shell.common.bubbles.BubbleBarLocation.LEFT +import com.android.wm.shell.common.bubbles.BubbleBarLocation.RIGHT + +/** + * Base class for common logic shared between different bubble views to support pinning bubble bar + * to left or right edge of screen. + * + * Handles drag events and allows a [LocationChangeListener] to be registered that is notified when + * location of the bubble bar should change. + * + * Shows a drop target when releasing a view would update the [BubbleBarLocation]. + */ +abstract class BaseBubblePinController { + + private var onLeft = false + private var dismissZone: RectF? = null + private var screenCenterX = 0 + private var listener: LocationChangeListener? = null + private var dropTargetAnimator: ObjectAnimator? = null + + /** + * Signal the controller that dragging interaction has started. + * + * @param initialLocationOnLeft side of the screen where bubble bar is pinned to + */ + fun onDragStart(initialLocationOnLeft: Boolean) { + onLeft = initialLocationOnLeft + dismissZone = getExclusionRect() + screenCenterX = getScreenCenterX() + } + + /** View has moved to [x] and [y] screen coordinates */ + fun onDragUpdate(x: Float, y: Float) { + if (dismissZone?.contains(x, y) == true) return + + if (onLeft && x > screenCenterX) { + onLeft = false + onLocationChange(RIGHT) + } else if (!onLeft && x < screenCenterX) { + onLeft = true + onLocationChange(LEFT) + } + } + + /** Temporarily hide the drop target view */ + fun setDropTargetHidden(hidden: Boolean) { + val targetView = getDropTargetView() ?: return + if (hidden) { + targetView.animateOut() + } else { + targetView.animateIn() + } + } + + /** Signal the controller that dragging interaction has finished. */ + fun onDragEnd() { + getDropTargetView()?.let { view -> view.animateOut { removeDropTargetView(view) } } + dismissZone = null + } + + /** + * [LocationChangeListener] that is notified when dragging interaction has resulted in bubble + * bar to be pinned on the other edge + */ + fun setListener(listener: LocationChangeListener?) { + this.listener = listener + } + + /** Get screen center coordinate on the x axis. */ + protected abstract fun getScreenCenterX(): Int + + /** Optional exclusion rect where drag interactions are not processed */ + protected abstract fun getExclusionRect(): RectF? + + /** Create the drop target view and attach it to the parent */ + protected abstract fun createDropTargetView(): View + + /** Get the drop target view if it exists */ + protected abstract fun getDropTargetView(): View? + + /** Remove the drop target view */ + protected abstract fun removeDropTargetView(view: View) + + /** Update size and location of the drop target view */ + protected abstract fun updateLocation(location: BubbleBarLocation) + + private fun onLocationChange(location: BubbleBarLocation) { + showDropTarget(location) + listener?.onChange(location) + } + + private fun showDropTarget(location: BubbleBarLocation) { + val targetView = getDropTargetView() ?: createDropTargetView().apply { alpha = 0f } + if (targetView.alpha > 0) { + targetView.animateOut { + updateLocation(location) + targetView.animateIn() + } + } else { + updateLocation(location) + targetView.animateIn() + } + } + + private fun View.animateIn() { + dropTargetAnimator?.cancel() + dropTargetAnimator = + ObjectAnimator.ofFloat(this, View.ALPHA, 1f) + .setDuration(DROP_TARGET_ALPHA_IN_DURATION) + .addEndAction { dropTargetAnimator = null } + dropTargetAnimator?.start() + } + + private fun View.animateOut(endAction: Runnable? = null) { + dropTargetAnimator?.cancel() + dropTargetAnimator = + ObjectAnimator.ofFloat(this, View.ALPHA, 0f) + .setDuration(DROP_TARGET_ALPHA_OUT_DURATION) + .addEndAction { + endAction?.run() + dropTargetAnimator = null + } + dropTargetAnimator?.start() + } + + private fun <T : Animator> T.addEndAction(runnable: Runnable): T { + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + runnable.run() + } + } + ) + return this + } + + /** Receive updates on location changes */ + interface LocationChangeListener { + /** + * Bubble bar [BubbleBarLocation] has changed as a result of dragging + * + * Triggered when drag gesture passes the middle of the screen and before touch up. Can be + * triggered multiple times per gesture. + * + * @param location new location as a result of the ongoing drag operation + */ + fun onChange(location: BubbleBarLocation) + } + + companion object { + @VisibleForTesting const val DROP_TARGET_ALPHA_IN_DURATION = 150L + @VisibleForTesting const val DROP_TARGET_ALPHA_OUT_DURATION = 100L + } +} |