Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 773ed4f8 authored by Ats Jenk's avatar Ats Jenk Committed by Android (Google) Code Review
Browse files

Merge "Restore initial location after drag to dismiss" into main

parents 027a61f3 f1874dff
Loading
Loading
Loading
Loading
+252 −66
Original line number Original line Diff line number Diff line
@@ -26,7 +26,7 @@ import androidx.core.animation.AnimatorTestRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.android.internal.protolog.common.ProtoLog
import com.android.internal.protolog.common.ProtoLog
import com.android.wm.shell.R
import com.android.wm.shell.R
import com.android.wm.shell.bubbles.BubblePositioner
import com.android.wm.shell.bubbles.BubblePositioner
@@ -35,6 +35,8 @@ 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_IN_DURATION
import com.android.wm.shell.common.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_OUT_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.android.wm.shell.common.bubbles.BubbleBarLocation
import com.android.wm.shell.common.bubbles.BubbleBarLocation.LEFT
import com.android.wm.shell.common.bubbles.BubbleBarLocation.RIGHT
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.After
import org.junit.Before
import org.junit.Before
@@ -63,6 +65,9 @@ class BubbleExpandedViewPinControllerTest {
    private lateinit var controller: BubbleExpandedViewPinController
    private lateinit var controller: BubbleExpandedViewPinController
    private lateinit var testListener: TestLocationChangeListener
    private lateinit var testListener: TestLocationChangeListener


    private val dropTargetView: View?
        get() = container.findViewById(R.id.bubble_bar_drop_target)

    private val pointOnLeft = PointF(100f, 100f)
    private val pointOnLeft = PointF(100f, 100f)
    private val pointOnRight = PointF(1900f, 500f)
    private val pointOnRight = PointF(1900f, 500f)


@@ -92,13 +97,14 @@ class BubbleExpandedViewPinControllerTest {


    @After
    @After
    fun tearDown() {
    fun tearDown() {
        runOnMainSync { controller.onDragEnd() }
        getInstrumentation().runOnMainSync { controller.onDragEnd() }
        waitForAnimateOut()
        waitForAnimateOut()
    }
    }


    /** Dragging on same side should not show drop target or trigger location changes */
    @Test
    @Test
    fun drag_stayOnSameSide() {
    fun drag_stayOnRightSide() {
        runOnMainSync {
        getInstrumentation().runOnMainSync {
            controller.onDragStart(initialLocationOnLeft = false)
            controller.onDragStart(initialLocationOnLeft = false)
            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
            controller.onDragEnd()
            controller.onDragEnd()
@@ -106,71 +112,124 @@ class BubbleExpandedViewPinControllerTest {
        waitForAnimateIn()
        waitForAnimateIn()
        assertThat(dropTargetView).isNull()
        assertThat(dropTargetView).isNull()
        assertThat(testListener.locationChanges).isEmpty()
        assertThat(testListener.locationChanges).isEmpty()
        assertThat(testListener.locationReleases).containsExactly(BubbleBarLocation.RIGHT)
        assertThat(testListener.locationReleases).containsExactly(RIGHT)
    }

    /** Dragging on same side should not show drop target or trigger location changes */
    @Test
    fun drag_stayOnLeftSide() {
        getInstrumentation().runOnMainSync {
            controller.onDragStart(initialLocationOnLeft = true)
            controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
            controller.onDragEnd()
        }
        waitForAnimateIn()
        assertThat(dropTargetView).isNull()
        assertThat(testListener.locationChanges).isEmpty()
        assertThat(testListener.locationReleases).containsExactly(LEFT)
    }
    }


    /** Drag crosses to the other side. Show drop target and trigger a location change. */
    @Test
    @Test
    fun drag_toLeft() {
    fun drag_rightToLeft() {
        // Drag to left, but don't finish
        getInstrumentation().runOnMainSync {
        runOnMainSync {
            controller.onDragStart(initialLocationOnLeft = false)
            controller.onDragStart(initialLocationOnLeft = false)
            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
            controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
            controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
        }
        }
        waitForAnimateIn()
        waitForAnimateIn()


        assertThat(dropTargetView).isNotNull()
        assertThat(dropTargetView).isNotNull()
        assertThat(dropTargetView!!.alpha).isEqualTo(1f)
        assertThat(dropTargetView!!.alpha).isEqualTo(1f)
        assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnLeft())
        assertThat(testListener.locationChanges).containsExactly(LEFT)
        assertThat(testListener.locationReleases).isEmpty()
    }


        val expectedDropTargetBounds = getExpectedDropTargetBounds(onLeft = true)
    /** Drag crosses to the other side. Show drop target and trigger a location change. */
        assertThat(dropTargetView!!.layoutParams.width).isEqualTo(expectedDropTargetBounds.width())
    @Test
        assertThat(dropTargetView!!.layoutParams.height)
    fun drag_leftToRight() {
            .isEqualTo(expectedDropTargetBounds.height())
        getInstrumentation().runOnMainSync {
            controller.onDragStart(initialLocationOnLeft = true)
            controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
        }
        waitForAnimateIn()


        assertThat(testListener.locationChanges).containsExactly(BubbleBarLocation.LEFT)
        assertThat(dropTargetView).isNotNull()
        assertThat(dropTargetView!!.alpha).isEqualTo(1f)
        assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnRight())
        assertThat(testListener.locationChanges).containsExactly(RIGHT)
        assertThat(testListener.locationReleases).isEmpty()
        assertThat(testListener.locationReleases).isEmpty()

        // Finish the drag
        runOnMainSync { controller.onDragEnd() }
        assertThat(testListener.locationReleases).containsExactly(BubbleBarLocation.LEFT)
    }
    }


    /**
     * Drop target does not initially show on the side that the drag starts. Check that it shows up
     * after the dragging the view to other side and back to the initial side.
     */
    @Test
    @Test
    fun drag_toLeftAndBackToRight() {
    fun drag_rightToLeftToRight() {
        // Drag to left
        getInstrumentation().runOnMainSync {
        runOnMainSync {
            controller.onDragStart(initialLocationOnLeft = false)
            controller.onDragStart(initialLocationOnLeft = false)
            controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
        }
        }
        waitForAnimateIn()
        waitForAnimateIn()
        assertThat(dropTargetView).isNull()

        getInstrumentation().runOnMainSync { controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) }
        waitForAnimateIn()
        assertThat(dropTargetView).isNotNull()
        assertThat(dropTargetView).isNotNull()


        // Drag to right
        getInstrumentation().runOnMainSync {
        runOnMainSync { controller.onDragUpdate(pointOnRight.x, pointOnRight.y) }
            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
        // We have to wait for existing drop target to animate out and new to animate in
        }
        waitForAnimateOut()
        waitForAnimateOut()
        waitForAnimateIn()
        waitForAnimateIn()

        assertThat(dropTargetView).isNotNull()
        assertThat(dropTargetView).isNotNull()
        assertThat(dropTargetView!!.alpha).isEqualTo(1f)
        assertThat(dropTargetView!!.alpha).isEqualTo(1f)
        assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnRight())
        assertThat(testListener.locationChanges).containsExactly(LEFT, RIGHT).inOrder()
        assertThat(testListener.locationReleases).isEmpty()
    }

    /**
     * Drop target does not initially show on the side that the drag starts. Check that it shows up
     * after the dragging the view to other side and back to the initial side.
     */
    @Test
    fun drag_leftToRightToLeft() {
        getInstrumentation().runOnMainSync {
            controller.onDragStart(initialLocationOnLeft = true)
            controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
        }
        waitForAnimateIn()
        assertThat(dropTargetView).isNull()


        val expectedDropTargetBounds = getExpectedDropTargetBounds(onLeft = false)
        getInstrumentation().runOnMainSync {
        assertThat(dropTargetView!!.layoutParams.width).isEqualTo(expectedDropTargetBounds.width())
            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
        assertThat(dropTargetView!!.layoutParams.height)
        }
            .isEqualTo(expectedDropTargetBounds.height())
        waitForAnimateIn()
        assertThat(dropTargetView).isNotNull()


        assertThat(testListener.locationChanges)
        getInstrumentation().runOnMainSync { controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) }
            .containsExactly(BubbleBarLocation.LEFT, BubbleBarLocation.RIGHT)
        waitForAnimateOut()
        waitForAnimateIn()
        assertThat(dropTargetView).isNotNull()
        assertThat(dropTargetView!!.alpha).isEqualTo(1f)
        assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnLeft())
        assertThat(testListener.locationChanges).containsExactly(RIGHT, LEFT).inOrder()
        assertThat(testListener.locationReleases).isEmpty()
        assertThat(testListener.locationReleases).isEmpty()

        // Release the view
        runOnMainSync { controller.onDragEnd() }
        assertThat(testListener.locationReleases).containsExactly(BubbleBarLocation.RIGHT)
    }
    }


    /**
     * Drag from right to left, but stay in exclusion rect around the dismiss view. Drop target
     * should not show and location change should not trigger.
     */
    @Test
    @Test
    fun drag_toLeftInExclusionRect() {
    fun drag_rightToLeft_inExclusionRect() {
        runOnMainSync {
        getInstrumentation().runOnMainSync {
            controller.onDragStart(initialLocationOnLeft = false)
            controller.onDragStart(initialLocationOnLeft = false)
            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
            // Exclusion rect is around the bottom center area of the screen
            // Exclusion rect is around the bottom center area of the screen
            controller.onDragUpdate(SCREEN_WIDTH / 2f - 50, SCREEN_HEIGHT - 100f)
            controller.onDragUpdate(SCREEN_WIDTH / 2f - 50, SCREEN_HEIGHT - 100f)
        }
        }
@@ -178,85 +237,212 @@ class BubbleExpandedViewPinControllerTest {
        assertThat(dropTargetView).isNull()
        assertThat(dropTargetView).isNull()
        assertThat(testListener.locationChanges).isEmpty()
        assertThat(testListener.locationChanges).isEmpty()
        assertThat(testListener.locationReleases).isEmpty()
        assertThat(testListener.locationReleases).isEmpty()
    }


        runOnMainSync { controller.onDragEnd() }
    /**
        assertThat(testListener.locationReleases).containsExactly(BubbleBarLocation.RIGHT)
     * Drag from left to right, but stay in exclusion rect around the dismiss view. Drop target
     * should not show and location change should not trigger.
     */
    @Test
    fun drag_leftToRight_inExclusionRect() {
        getInstrumentation().runOnMainSync {
            controller.onDragStart(initialLocationOnLeft = true)
            controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
            // 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()
        assertThat(testListener.locationReleases).isEmpty()
    }
    }


    /**
     * Drag to dismiss target and back to the same side should not cause the drop target to show.
     */
    @Test
    @Test
    fun toggleSetDropTargetHidden_dropTargetExists() {
    fun drag_rightToDismissToRight() {
        runOnMainSync {
        getInstrumentation().runOnMainSync {
            controller.onDragStart(initialLocationOnLeft = false)
            controller.onDragStart(initialLocationOnLeft = false)
            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
            controller.onStuckToDismissTarget()
            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
        }
        waitForAnimateIn()
        assertThat(dropTargetView).isNull()
        assertThat(testListener.locationChanges).isEmpty()
        assertThat(testListener.locationReleases).isEmpty()
    }

    /**
     * Drag to dismiss target and back to the same side should not cause the drop target to show.
     */
    @Test
    fun drag_leftToDismissToLeft() {
        getInstrumentation().runOnMainSync {
            controller.onDragStart(initialLocationOnLeft = true)
            controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
            controller.onStuckToDismissTarget()
            controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
            controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
        }
        }
        waitForAnimateIn()
        waitForAnimateIn()
        assertThat(dropTargetView).isNull()
        assertThat(testListener.locationChanges).isEmpty()
        assertThat(testListener.locationReleases).isEmpty()
    }


    /** Drag to dismiss target and other side should show drop target on the other side. */
    @Test
    fun drag_rightToDismissToLeft() {
        getInstrumentation().runOnMainSync {
            controller.onDragStart(initialLocationOnLeft = false)
            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
            controller.onStuckToDismissTarget()
            controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
        }
        waitForAnimateIn()
        assertThat(dropTargetView).isNotNull()
        assertThat(dropTargetView).isNotNull()
        assertThat(dropTargetView!!.alpha).isEqualTo(1f)
        assertThat(dropTargetView!!.alpha).isEqualTo(1f)
        assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnLeft())


        runOnMainSync { controller.setDropTargetHidden(true) }
        assertThat(testListener.locationChanges).containsExactly(LEFT)
        waitForAnimateOut()
        assertThat(testListener.locationReleases).isEmpty()
        assertThat(dropTargetView).isNotNull()
    }
        assertThat(dropTargetView!!.alpha).isEqualTo(0f)


        runOnMainSync { controller.setDropTargetHidden(false) }
    /** Drag to dismiss target and other side should show drop target on the other side. */
    @Test
    fun drag_leftToDismissToRight() {
        getInstrumentation().runOnMainSync {
            controller.onDragStart(initialLocationOnLeft = true)
            controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
            controller.onStuckToDismissTarget()
            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
        }
        waitForAnimateIn()
        waitForAnimateIn()
        assertThat(dropTargetView).isNotNull()
        assertThat(dropTargetView).isNotNull()
        assertThat(dropTargetView!!.alpha).isEqualTo(1f)
        assertThat(dropTargetView!!.alpha).isEqualTo(1f)
        assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnRight())

        assertThat(testListener.locationChanges).containsExactly(RIGHT)
        assertThat(testListener.locationReleases).isEmpty()
    }
    }


    /**
     * Drag to dismiss should trigger a location change to the initial location, if the current
     * location is different. And hide the drop target.
     */
    @Test
    @Test
    fun toggleSetDropTargetHidden_noDropTarget() {
    fun drag_rightToLeftToDismiss() {
        runOnMainSync { controller.setDropTargetHidden(true) }
        getInstrumentation().runOnMainSync {
            controller.onDragStart(initialLocationOnLeft = false)
            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
            controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
        }
        waitForAnimateIn()
        assertThat(dropTargetView).isNotNull()
        assertThat(dropTargetView!!.alpha).isEqualTo(1f)

        getInstrumentation().runOnMainSync { controller.onStuckToDismissTarget() }
        waitForAnimateOut()
        waitForAnimateOut()
        assertThat(dropTargetView).isNull()
        assertThat(dropTargetView!!.alpha).isEqualTo(0f)

        assertThat(testListener.locationChanges).containsExactly(LEFT, RIGHT).inOrder()
        assertThat(testListener.locationReleases).isEmpty()
    }


        runOnMainSync { controller.setDropTargetHidden(false) }
    /**
     * Drag to dismiss should trigger a location change to the initial location, if the current
     * location is different. And hide the drop target.
     */
    @Test
    fun drag_leftToRightToDismiss() {
        getInstrumentation().runOnMainSync {
            controller.onDragStart(initialLocationOnLeft = true)
            controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
        }
        waitForAnimateIn()
        waitForAnimateIn()
        assertThat(dropTargetView).isNull()
        assertThat(dropTargetView).isNotNull()
        assertThat(dropTargetView!!.alpha).isEqualTo(1f)
        getInstrumentation().runOnMainSync { controller.onStuckToDismissTarget() }
        waitForAnimateOut()
        assertThat(dropTargetView!!.alpha).isEqualTo(0f)
        assertThat(testListener.locationChanges).containsExactly(RIGHT, LEFT).inOrder()
        assertThat(testListener.locationReleases).isEmpty()
    }
    }


    /** Finishing drag should remove drop target and send location update. */
    @Test
    @Test
    fun onDragEnd_dropTargetExists() {
    fun drag_rightToLeftRelease() {
        runOnMainSync {
        getInstrumentation().runOnMainSync {
            controller.onDragStart(initialLocationOnLeft = false)
            controller.onDragStart(initialLocationOnLeft = false)
            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
            controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
            controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
        }
        }
        waitForAnimateIn()
        waitForAnimateIn()
        assertThat(dropTargetView).isNotNull()
        assertThat(dropTargetView).isNotNull()


        runOnMainSync { controller.onDragEnd() }
        getInstrumentation().runOnMainSync { controller.onDragEnd() }
        waitForAnimateOut()
        waitForAnimateOut()
        assertThat(dropTargetView).isNull()
        assertThat(dropTargetView).isNull()
        assertThat(testListener.locationChanges).containsExactly(LEFT)
        assertThat(testListener.locationReleases).containsExactly(LEFT)
    }
    }


    /** Finishing drag should remove drop target and send location update. */
    @Test
    @Test
    fun onDragEnd_noDropTarget() {
    fun drag_leftToRightRelease() {
        runOnMainSync { controller.onDragEnd() }
        getInstrumentation().runOnMainSync {
            controller.onDragStart(initialLocationOnLeft = true)
            controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
        }
        waitForAnimateIn()
        assertThat(dropTargetView).isNotNull()

        getInstrumentation().runOnMainSync { controller.onDragEnd() }
        waitForAnimateOut()
        waitForAnimateOut()
        assertThat(dropTargetView).isNull()
        assertThat(dropTargetView).isNull()
        assertThat(testListener.locationChanges).containsExactly(RIGHT)
        assertThat(testListener.locationReleases).containsExactly(RIGHT)
    }
    }


    private val dropTargetView: View?
    private fun getExpectedDropTargetBoundsOnLeft(): Rect =
        get() = container.findViewById(R.id.bubble_bar_drop_target)

    private fun getExpectedDropTargetBounds(onLeft: Boolean): Rect =
        Rect().also {
        Rect().also {
            positioner.getBubbleBarExpandedViewBounds(onLeft, false /* isOveflowExpanded */, it)
            positioner.getBubbleBarExpandedViewBounds(
                true /* onLeft */,
                false /* isOverflowExpanded */,
                it
            )
        }
        }


    private fun runOnMainSync(runnable: Runnable) {
    private fun getExpectedDropTargetBoundsOnRight(): Rect =
        InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable)
        Rect().also {
            positioner.getBubbleBarExpandedViewBounds(
                false /* onLeft */,
                false /* isOverflowExpanded */,
                it
            )
        }
        }


    private fun waitForAnimateIn() {
    private fun waitForAnimateIn() {
        // Advance animator for on-device test
        // Advance animator for on-device test
        runOnMainSync { animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_IN_DURATION) }
        getInstrumentation().runOnMainSync {
            animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_IN_DURATION)
        }
    }
    }


    private fun waitForAnimateOut() {
    private fun waitForAnimateOut() {
        // Advance animator for on-device test
        // Advance animator for on-device test
        runOnMainSync { animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_OUT_DURATION) }
        getInstrumentation().runOnMainSync {
            animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_OUT_DURATION)
        }
    }

    private fun View.bounds(): Rect {
        return Rect(0, 0, layoutParams.width, layoutParams.height).also { rect ->
            rect.offsetTo(x.toInt(), y.toInt())
        }
    }
    }


    internal class TestLocationChangeListener : BaseBubblePinController.LocationChangeListener {
    internal class TestLocationChangeListener : BaseBubblePinController.LocationChangeListener {
+1 −2
Original line number Original line Diff line number Diff line
@@ -150,7 +150,7 @@ class BubbleBarExpandedViewDragController(
            draggedObject: MagnetizedObject<*>
            draggedObject: MagnetizedObject<*>
        ) {
        ) {
            isStuckToDismiss = true
            isStuckToDismiss = true
            pinController.setDropTargetHidden(true)
            pinController.onStuckToDismissTarget()
        }
        }


        override fun onUnstuckFromTarget(
        override fun onUnstuckFromTarget(
@@ -162,7 +162,6 @@ class BubbleBarExpandedViewDragController(
        ) {
        ) {
            isStuckToDismiss = false
            isStuckToDismiss = false
            animationHelper.animateUnstuckFromDismissView(target)
            animationHelper.animateUnstuckFromDismissView(target)
            pinController.setDropTargetHidden(false)
        }
        }


        override fun onReleasedInTarget(
        override fun onReleasedInTarget(
+27 −13
Original line number Original line Diff line number Diff line
@@ -38,8 +38,10 @@ import com.android.wm.shell.common.bubbles.BubbleBarLocation.RIGHT
 */
 */
abstract class BaseBubblePinController(private val screenSizeProvider: () -> Point) {
abstract class BaseBubblePinController(private val screenSizeProvider: () -> Point) {


    private var initialLocationOnLeft = false
    private var onLeft = false
    private var onLeft = false
    private var dismissZone: RectF? = null
    private var dismissZone: RectF? = null
    private var stuckToDismissTarget = false
    private var screenCenterX = 0
    private var screenCenterX = 0
    private var listener: LocationChangeListener? = null
    private var listener: LocationChangeListener? = null
    private var dropTargetAnimator: ObjectAnimator? = null
    private var dropTargetAnimator: ObjectAnimator? = null
@@ -50,6 +52,7 @@ abstract class BaseBubblePinController(private val screenSizeProvider: () -> Poi
     * @param initialLocationOnLeft side of the screen where bubble bar is pinned to
     * @param initialLocationOnLeft side of the screen where bubble bar is pinned to
     */
     */
    fun onDragStart(initialLocationOnLeft: Boolean) {
    fun onDragStart(initialLocationOnLeft: Boolean) {
        this.initialLocationOnLeft = initialLocationOnLeft
        onLeft = initialLocationOnLeft
        onLeft = initialLocationOnLeft
        screenCenterX = screenSizeProvider.invoke().x / 2
        screenCenterX = screenSizeProvider.invoke().x / 2
        dismissZone = getExclusionRect()
        dismissZone = getExclusionRect()
@@ -59,22 +62,33 @@ abstract class BaseBubblePinController(private val screenSizeProvider: () -> Poi
    fun onDragUpdate(x: Float, y: Float) {
    fun onDragUpdate(x: Float, y: Float) {
        if (dismissZone?.contains(x, y) == true) return
        if (dismissZone?.contains(x, y) == true) return


        if (onLeft && x > screenCenterX) {
        val wasOnLeft = onLeft
            onLeft = false
        onLeft = x < screenCenterX
            onLocationChange(RIGHT)
        if (wasOnLeft != onLeft) {
        } else if (!onLeft && x < screenCenterX) {
            onLocationChange(if (onLeft) LEFT else RIGHT)
            onLeft = true
        } else if (stuckToDismissTarget) {
            onLocationChange(LEFT)
            // Moved out of the dismiss view back to initial side, if we have a drop target, show it
            getDropTargetView()?.apply { animateIn() }
        }
        }
        // Make sure this gets cleared
        stuckToDismissTarget = false
    }
    }


    /** Temporarily hide the drop target view */
    /** Signal the controller that view has been dragged to dismiss view. */
    fun setDropTargetHidden(hidden: Boolean) {
    fun onStuckToDismissTarget() {
        val targetView = getDropTargetView() ?: return
        stuckToDismissTarget = true
        if (hidden) {
        // Notify that location may be reset
            targetView.animateOut()
        val shouldResetLocation = onLeft != initialLocationOnLeft
        } else {
        if (shouldResetLocation) {
            targetView.animateIn()
            onLeft = initialLocationOnLeft
            listener?.onChange(if (onLeft) LEFT else RIGHT)
        }
        getDropTargetView()?.apply {
            animateOut {
                if (shouldResetLocation) {
                    updateLocation(if (onLeft) LEFT else RIGHT)
                }
            }
        }
        }
    }
    }