Update dismiss view location in one-handed mode
Updates dismiss view location in one-handed mode using margin instead of
padding. Dismiss view has internal padding that ensures it is not
against the bottom edge of the display. We were previously using padding
to update the dismiss view location in one-handed mode to ensure dismiss
view is visible. But when exiting one-handed mode, we cleared the
padding and dismiss view remained touching the bottom area of the display.
Switching to margin for repositioning dismiss view, when one-handed mode
is enabled. This ensures padding is not altered.
Update MagnetizedObject to use the same coordinate systems when
checking if a MagnetizedObject is in the bounds of a MagneticTarget.
MagnetizedObject relied on View#getLocationOnScreen() to determine the
location of itself and the location of MagneticTarget.
For dragging and determining if pointer location is in the bounds of a
MagneticTarget, it used MotionEvent#getRawX/Y().
These two work in different coordinate systems.
View#getLocationOnScreen() returns the location in screen coordinates.
MotionEvent works in display coordinates. Usually these match. But not
in the case of one-handed mode. When one-handed mode is enabled, we
translate the screen surface, to move it closer to the bottom edge.
View location in terms of screen coordinates does not change. But
MotionEvent is still working in display coordinates.
This led to an issue where based on screen coordinates a MagneticTarget
was in one location and when using those values to compare to
MotionEvent, they did not match.
Introduced a screenVerticalOffset option to MagneticTarget which allows
clients to define how much the screen is offset. That value is used
during checks if a MotionEvent pointer location falls into the
MagneticTarget bounds or not.
Bug: 311251698
Test: atest MagnetizedObjectTest
Test: manual, have a bubble and enter one-handed mode, drag the bubble
and observer the dismiss view shows up at the same location on the
display, drag bubble around the screen, observe bubble is stuck to
dismiss view when hovering around it
Change-Id: Ie556d880ce9cc0e9bc8f3683e1cd6010142b9de4
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index 9facef3..a8d3104 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -3545,7 +3545,14 @@
*/
void onVerticalOffsetChanged(int offset) {
// adjust dismiss view vertical position, so that it is still visible to the user
- mDismissView.setPadding(/* left = */ 0, /* top = */ 0, /* right = */ 0, offset);
+ ViewGroup.LayoutParams lp = mDismissView.getLayoutParams();
+ if (lp instanceof FrameLayout.LayoutParams) {
+ FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) lp;
+ layoutParams.bottomMargin = offset;
+ mDismissView.setLayoutParams(layoutParams);
+ }
+ mMagneticTarget.setScreenVerticalOffset(offset);
+ mMagneticTarget.updateLocationOnScreen();
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt
index aac1d062..7c931df 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt
@@ -352,8 +352,8 @@
val targetObjectIsInMagneticFieldOf = associatedTargets.firstOrNull { target ->
val distanceFromTargetCenter = hypot(
- ev.rawX - target.centerOnScreen.x,
- ev.rawY - target.centerOnScreen.y)
+ ev.rawX - target.centerOnDisplayX(),
+ ev.rawY - target.centerOnDisplayY())
distanceFromTargetCenter < target.magneticFieldRadiusPx
}
@@ -406,7 +406,6 @@
// First, check for relevant gestures concluding with an ACTION_UP.
if (ev.action == MotionEvent.ACTION_UP) {
-
velocityTracker.computeCurrentVelocity(1000 /* units */)
val velX = velocityTracker.xVelocity
val velY = velocityTracker.yVelocity
@@ -542,7 +541,7 @@
// Whether velocity is sufficient, depending on whether we're flinging into a target at the
// top or the bottom of the screen.
val velocitySufficient =
- if (rawY < target.centerOnScreen.y) velY > flingToTargetMinVelocity
+ if (rawY < target.centerOnDisplayY()) velY > flingToTargetMinVelocity
else velY < flingToTargetMinVelocity
if (!velocitySufficient) {
@@ -560,15 +559,15 @@
val yIntercept = rawY - slope * rawX
// ...calculate the x value when y = the target's y-coordinate.
- targetCenterXIntercept = (target.centerOnScreen.y - yIntercept) / slope
+ targetCenterXIntercept = (target.centerOnDisplayY() - yIntercept) / slope
}
// The width of the area we're looking for a fling towards.
val targetAreaWidth = target.targetView.width * flingToTargetWidthPercent
// Velocity was sufficient, so return true if the intercept is within the target area.
- return targetCenterXIntercept > target.centerOnScreen.x - targetAreaWidth / 2 &&
- targetCenterXIntercept < target.centerOnScreen.x + targetAreaWidth / 2
+ return targetCenterXIntercept > target.centerOnDisplayX() - targetAreaWidth / 2 &&
+ targetCenterXIntercept < target.centerOnDisplayX() + targetAreaWidth / 2
}
/** Cancel animations on this object's x/y properties. */
@@ -601,6 +600,22 @@
) {
val centerOnScreen = PointF()
+ /**
+ * Set screen vertical offset amount.
+ *
+ * Screen surface may be vertically shifted in some cases, for example when one-handed mode
+ * is enabled. [MagneticTarget] and [MagnetizedObject] set their location in screen
+ * coordinates (see [MagneticTarget.centerOnScreen] and
+ * [MagnetizedObject.getLocationOnScreen] respectively).
+ *
+ * When a [MagnetizedObject] is dragged, the touch location is determined by
+ * [MotionEvent.getRawX] and [MotionEvent.getRawY]. These work in display coordinates. When
+ * screen is shifted due to one-handed mode, display coordinates and screen coordinates do
+ * not match. To determine if a [MagnetizedObject] is dragged into a [MagneticTarget], view
+ * location on screen is translated to display coordinates using this offset value.
+ */
+ var screenVerticalOffset: Int = 0
+
private val tempLoc = IntArray(2)
fun updateLocationOnScreen() {
@@ -614,6 +629,23 @@
tempLoc[1] + targetView.height / 2f - targetView.translationY)
}
}
+
+ /**
+ * Get target center coordinate on x-axis on display. [centerOnScreen] has to be up to date
+ * by calling [updateLocationOnScreen] first.
+ */
+ fun centerOnDisplayX(): Float {
+ return centerOnScreen.x
+ }
+
+ /**
+ * Get target center coordinate on y-axis on display. [centerOnScreen] has to be up to date
+ * by calling [updateLocationOnScreen] first. Use [screenVerticalOffset] to update the
+ * screen offset compared to the display.
+ */
+ fun centerOnDisplayY(): Float {
+ return centerOnScreen.y + screenVerticalOffset
+ }
}
companion object {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt
index 9f1ee6c..a9f054e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt
@@ -30,6 +30,7 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
+import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyFloat
import org.mockito.Mockito
import org.mockito.Mockito.`when`
@@ -97,7 +98,7 @@
// The mock target view will pretend that it's 200x200, and at (400, 800). This means it's
// occupying the bounds (400, 800, 600, 1000) and it has a center of (500, 900).
- `when`(targetView.width).thenReturn(targetSize) // width = 200
+ `when`(targetView.width).thenReturn(targetSize) // width = 200
`when`(targetView.height).thenReturn(targetSize) // height = 200
doAnswer { invocation ->
(invocation.arguments[0] as IntArray).also { location ->
@@ -275,11 +276,11 @@
// Forcefully fling the object towards the target (but never touch the magnetic field).
dispatchMotionEvents(
getMotionEvent(
- x = targetCenterX,
+ x = 0,
y = 0,
action = MotionEvent.ACTION_DOWN),
getMotionEvent(
- x = targetCenterX,
+ x = targetCenterX / 2,
y = targetCenterY / 2),
getMotionEvent(
x = targetCenterX,
@@ -405,15 +406,78 @@
verify(magnetListener).onStuckToTarget(magneticTarget)
}
+ @Test
+ fun testMagneticTargetHasScreenOffset_moveIntoAndReleaseInTarget() {
+ magneticTarget.screenVerticalOffset = 500
+
+ dispatchMotionEvents(getMotionEvent(x = targetCenterX, y = targetCenterY))
+ // Moved into the target location, but it should be shifted due to screen offset.
+ // Should not get stuck.
+ verify(magnetListener, never()).onStuckToTarget(magneticTarget)
+
+ dispatchMotionEvents(getMotionEvent(x = targetCenterX, y = targetCenterY + 500))
+ verify(magnetListener).onStuckToTarget(magneticTarget)
+
+ dispatchMotionEvents(
+ getMotionEvent(
+ x = targetCenterX,
+ y = targetCenterY + 500,
+ action = MotionEvent.ACTION_UP
+ )
+ )
+
+ verify(magnetListener).onReleasedInTarget(magneticTarget)
+ verifyNoMoreInteractions(magnetListener)
+ }
+
+ @Test
+ fun testMagneticTargetHasScreenOffset_screenOffsetUpdates() {
+ magneticTarget.screenVerticalOffset = 500
+ val adjustedTargetCenter = targetCenterY + 500
+
+ dispatchMotionEvents(getMotionEvent(x = targetCenterX, y = adjustedTargetCenter))
+ dispatchMotionEvents(getMotionEvent(x = 0, y = 0))
+ verify(magnetListener).onStuckToTarget(magneticTarget)
+ verify(magnetListener)
+ .onUnstuckFromTarget(eq(magneticTarget), anyFloat(), anyFloat(), anyBoolean())
+
+ // Offset if removed, we should now get stuck at the target location
+ magneticTarget.screenVerticalOffset = 0
+ dispatchMotionEvents(getMotionEvent(x = targetCenterX, y = targetCenterY))
+ verify(magnetListener, times(2)).onStuckToTarget(magneticTarget)
+ }
+
+ @Test
+ fun testMagneticTargetHasScreenOffset_flingTowardsTarget() {
+ timeStep = 10
+
+ magneticTarget.screenVerticalOffset = 500
+ val adjustedTargetCenter = targetCenterY + 500
+
+ // Forcefully fling the object towards the target (but never touch the magnetic field).
+ dispatchMotionEvents(
+ getMotionEvent(x = 0, y = 0, action = MotionEvent.ACTION_DOWN),
+ getMotionEvent(x = targetCenterX / 2, y = adjustedTargetCenter / 2),
+ getMotionEvent(
+ x = targetCenterX,
+ y = adjustedTargetCenter - magneticFieldRadius * 2,
+ action = MotionEvent.ACTION_UP
+ )
+ )
+
+ // Nevertheless it should have ended up stuck to the target.
+ verify(magnetListener, times(1)).onStuckToTarget(magneticTarget)
+ }
+
private fun getSecondMagneticTarget(): MagnetizedObject.MagneticTarget {
// The first target view is at bounds (400, 800, 600, 1000) and it has a center of
// (500, 900). We'll add a second one at bounds (0, 800, 200, 1000) with center (100, 900).
val secondTargetView = mock(View::class.java)
- var secondTargetCenterX = 100
- var secondTargetCenterY = 900
+ val secondTargetCenterX = 100
+ val secondTargetCenterY = 900
`when`(secondTargetView.context).thenReturn(context)
- `when`(secondTargetView.width).thenReturn(targetSize) // width = 200
+ `when`(secondTargetView.width).thenReturn(targetSize) // width = 200
`when`(secondTargetView.height).thenReturn(targetSize) // height = 200
doAnswer { invocation ->
(invocation.arguments[0] as Runnable).run()