Detect when user wanted to drag vs. click

In order to avoid sending a new topology to DisplayManager
unnecessarily, which could cause some disruptive visual effect, don't do
anything if the drag was both (a) brief and (b) did nto deviate from
start position.

Flag: com.android.settings.flags.display_topology_pane_in_display_list
Test: DisplayTopologyPanePreferenceTest
Test: manual - quickly drag as few pixels as possible and verify the block moved back after drag, with no scale change
Bug: b/352648432
Change-Id: I29ffb51c54c9dbac970149cffd86a8027f0a42f5
This commit is contained in:
Matthew DeVore
2025-02-10 17:55:00 +00:00
parent a7caa23deb
commit da0bd7b412
2 changed files with 149 additions and 32 deletions

View File

@@ -224,6 +224,20 @@ class DisplayTopologyPreference(context : Context)
@VisibleForTesting var injector : Injector @VisibleForTesting var injector : Injector
/**
* How many physical pixels to move in pane coordinates (Pythagorean distance) before a drag is
* considered non-trivial and intentional.
*
* This value is computed on-demand so that the injector can be changed at any time.
*/
@VisibleForTesting val accidentalDragDistancePx
get() = DisplayTopology.dpToPx(4f, injector.densityDpi)
/**
* How long before until a tap is considered a drag regardless of distance moved.
*/
@VisibleForTesting val accidentalDragTimeLimitMs = 800L
/** /**
* This is needed to prevent a repopulation of the pane causing another * This is needed to prevent a repopulation of the pane causing another
* relayout and vice-versa ad infinitum. * relayout and vice-versa ad infinitum.
@@ -295,15 +309,19 @@ class DisplayTopologyPreference(context : Context)
open val wallpaper: Bitmap? open val wallpaper: Bitmap?
get() = WallpaperManager.getInstance(context).bitmap get() = WallpaperManager.getInstance(context).bitmap
open val densityDpi: Int /**
get() { * This density is the density of the current display (showing the topology pane). It is
val info = DisplayInfo() * necessary to use this density here because the topology pane coordinates are in physical
return if (context.display.getDisplayInfo(info)) { * pixels, and the display coordinates are in density-independent pixels.
info.logicalDensityDpi */
} else { open val densityDpi: Int by lazy {
DisplayMetrics.DENSITY_DEFAULT val info = DisplayInfo()
} if (context.display.getDisplayInfo(info)) {
info.logicalDensityDpi
} else {
DisplayMetrics.DENSITY_DEFAULT
} }
}
open fun registerTopologyListener(listener: Consumer<DisplayTopology>) { open fun registerTopologyListener(listener: Consumer<DisplayTopology>) {
displayManager.registerTopologyListener(context.mainExecutor, listener) displayManager.registerTopologyListener(context.mainExecutor, listener)
@@ -323,23 +341,29 @@ class DisplayTopologyPreference(context : Context)
val positions: List<Pair<Int, RectF>>) val positions: List<Pair<Int, RectF>>)
/** /**
* Holds information about the current drag operation. * Holds information about the current drag operation. The initial rawX, rawY values of the
* cursor are recorded in order to detect whether the drag was a substantial drag or likely
* accidental.
*
* @param stationaryDisps ID and position of displays that are not moving * @param stationaryDisps ID and position of displays that are not moving
* @param display View that is currently being dragged * @param display View that is currently being dragged
* @param displayId ID of display being dragged * @param displayId ID of display being dragged
* @param displayWidth width of display being dragged in actual (not View) coordinates * @param displayWidth width of display being dragged in actual (not View) coordinates
* @param displayHeight height of display being dragged in actual (not View) coordinates * @param displayHeight height of display being dragged in actual (not View) coordinates
* @param dragOffsetX difference between event rawX coordinate and X of the display in the pane * @param initialBlockX block's X coordinate upon touch down event
* @param dragOffsetY difference between event rawY coordinate and Y of the display in the pane * @param initialBlockY block's Y coordinate upon touch down event
* @param didMove true if we have detected the user intentionally wanted to drag rather than * @param initialTouchX rawX value of the touch down event
* just click * @param initialTouchY rawY value of the touch down event
* @param startTimeMs time when tap down occurred, needed to detect the user intentionally
* wanted to drag rather than just click
*/ */
private data class BlockDrag( private data class BlockDrag(
val stationaryDisps : List<Pair<Int, RectF>>, val stationaryDisps : List<Pair<Int, RectF>>,
val display: DisplayBlock, val displayId: Int, val display: DisplayBlock, val displayId: Int,
val displayWidth: Float, val displayHeight: Float, val displayWidth: Float, val displayHeight: Float,
val dragOffsetX: Float, val dragOffsetY: Float, val initialBlockX: Float, val initialBlockY: Float,
var didMove: Boolean = false) val initialTouchX: Float, val initialTouchY: Float,
val startTimeMs: Long)
private var mTopologyInfo : TopologyInfo? = null private var mTopologyInfo : TopologyInfo? = null
private var mDrag : BlockDrag? = null private var mDrag : BlockDrag? = null
@@ -394,14 +418,10 @@ class DisplayTopologyPreference(context : Context)
recycleableBlocks.add(mPaneContent.getChildAt(i) as DisplayBlock) recycleableBlocks.add(mPaneContent.getChildAt(i) as DisplayBlock)
} }
// This density is the density of the current display (showing the topology pane). It is
// necessary to use this density here because the topology pane coordinates are in physical
// pixels, and the display coordinates are in density-independent pixels.
val dpi = injector.densityDpi
val scaling = TopologyScale( val scaling = TopologyScale(
mPaneContent.width, mPaneContent.width,
minEdgeLength = DisplayTopology.dpToPx(60f, dpi), minEdgeLength = DisplayTopology.dpToPx(60f, injector.densityDpi),
maxEdgeLength = DisplayTopology.dpToPx(256f, dpi), maxEdgeLength = DisplayTopology.dpToPx(256f, injector.densityDpi),
newBounds.map { it.second }.toList()) newBounds.map { it.second }.toList())
mPaneHolder.layoutParams.let { mPaneHolder.layoutParams.let {
val newHeight = scaling.paneHeight.toInt() val newHeight = scaling.paneHeight.toInt()
@@ -431,7 +451,7 @@ class DisplayTopologyPreference(context : Context)
when (ev.actionMasked) { when (ev.actionMasked) {
MotionEvent.ACTION_DOWN -> onBlockTouchDown(id, pos, block, ev) MotionEvent.ACTION_DOWN -> onBlockTouchDown(id, pos, block, ev)
MotionEvent.ACTION_MOVE -> onBlockTouchMove(ev) MotionEvent.ACTION_MOVE -> onBlockTouchMove(ev)
MotionEvent.ACTION_UP -> onBlockTouchUp() MotionEvent.ACTION_UP -> onBlockTouchUp(ev)
else -> false else -> false
} }
} }
@@ -462,7 +482,10 @@ class DisplayTopologyPreference(context : Context)
// moving, and the raw coordinates are relative to the screen. // moving, and the raw coordinates are relative to the screen.
mDrag = BlockDrag( mDrag = BlockDrag(
stationaryDisps.toList(), block, displayId, displayPos.width(), displayPos.height(), stationaryDisps.toList(), block, displayId, displayPos.width(), displayPos.height(),
ev.rawX - block.x, ev.rawY - block.y) initialBlockX = block.x, initialBlockY = block.y,
initialTouchX = ev.rawX, initialTouchY = ev.rawY,
startTimeMs = ev.eventTime,
)
// Prevents a container of this view from intercepting the touch events in the case the // Prevents a container of this view from intercepting the touch events in the case the
// pointer moves outside of the display block or the pane. // pointer moves outside of the display block or the pane.
@@ -474,30 +497,31 @@ class DisplayTopologyPreference(context : Context)
val drag = mDrag ?: return false val drag = mDrag ?: return false
val topology = mTopologyInfo ?: return false val topology = mTopologyInfo ?: return false
val dispDragCoor = topology.scaling.paneToDisplayCoor( val dispDragCoor = topology.scaling.paneToDisplayCoor(
ev.rawX - drag.dragOffsetX, ev.rawY - drag.dragOffsetY) ev.rawX - drag.initialTouchX + drag.initialBlockX,
ev.rawY - drag.initialTouchY + drag.initialBlockY)
val dispDragRect = RectF( val dispDragRect = RectF(
dispDragCoor.x, dispDragCoor.y, dispDragCoor.x, dispDragCoor.y,
dispDragCoor.x + drag.displayWidth, dispDragCoor.y + drag.displayHeight) dispDragCoor.x + drag.displayWidth, dispDragCoor.y + drag.displayHeight)
val snapRect = clampPosition(drag.stationaryDisps.map { it.second }, dispDragRect) val snapRect = clampPosition(drag.stationaryDisps.map { it.second }, dispDragRect)
drag.display.place(topology.scaling.displayToPaneCoor(snapRect.left, snapRect.top)) drag.display.place(topology.scaling.displayToPaneCoor(snapRect.left, snapRect.top))
drag.didMove = true
return true return true
} }
private fun onBlockTouchUp(): Boolean { private fun onBlockTouchUp(ev: MotionEvent): Boolean {
val drag = mDrag ?: return false val drag = mDrag ?: return false
val topology = mTopologyInfo ?: return false val topology = mTopologyInfo ?: return false
mPaneContent.requestDisallowInterceptTouchEvent(false) mPaneContent.requestDisallowInterceptTouchEvent(false)
drag.display.setHighlighted(false) drag.display.setHighlighted(false)
mDrag = null val netPxDragged = Math.hypot(
if (!drag.didMove) { (drag.initialBlockX - drag.display.x).toDouble(),
// If no move event occurred, ignore the drag completely. (drag.initialBlockY - drag.display.y).toDouble())
// TODO(b/352648432): Responding to a single move event no matter how small may be too val timeDownMs = ev.eventTime - drag.startTimeMs
// sensitive. It is easy to slide by a small amount just by force of pressing down the if (netPxDragged < accidentalDragDistancePx && timeDownMs < accidentalDragTimeLimitMs) {
// mouse button. Keep an eye on this. drag.display.x = drag.initialBlockX
drag.display.y = drag.initialBlockY
return true return true
} }

View File

@@ -419,4 +419,97 @@ class DisplayTopologyPreferenceTest {
.build()) .build())
assertThat(leftBlock.background).isEqualTo(leftBlock.mUnselectedImage) assertThat(leftBlock.background).isEqualTo(leftBlock.mUnselectedImage)
} }
fun dragBlockWithOneMoveEvent(
block: DisplayBlock, startTimeMs: Long, endTimeMs: Long, xDiff: Float, yDiff: Float) {
block.dispatchTouchEvent(MotionEventBuilder.newBuilder()
.setAction(MotionEvent.ACTION_DOWN)
.setPointer(0f, 0f)
.setEventTime(startTimeMs)
.build())
block.dispatchTouchEvent(MotionEventBuilder.newBuilder()
.setAction(MotionEvent.ACTION_MOVE)
.setPointer(xDiff, yDiff)
.setEventTime((startTimeMs + endTimeMs) / 2)
.build())
block.dispatchTouchEvent(MotionEventBuilder.newBuilder()
.setAction(MotionEvent.ACTION_UP)
.setPointer(xDiff, yDiff)
.setEventTime(endTimeMs)
.build())
}
@Test
fun accidentalDrag_LittleAndBriefEnoughToBeAccidental() {
val (leftBlock, _) = setupTwoDisplays(POSITION_LEFT, childOffset = 42f)
val startTime = 424242L
val startX = leftBlock.x
val startY = leftBlock.y
preference.mTimesRefreshedBlocks = 0
dragBlockWithOneMoveEvent(
leftBlock, startTime,
endTimeMs = startTime + preference.accidentalDragTimeLimitMs - 10,
xDiff = preference.accidentalDragDistancePx - 1f, yDiff = 0f,
)
assertThat(leftBlock.x).isEqualTo(startX)
assertThat(preference.mTimesRefreshedBlocks).isEqualTo(0)
dragBlockWithOneMoveEvent(
leftBlock, startTime,
endTimeMs = startTime + preference.accidentalDragTimeLimitMs - 10,
xDiff = 0f, yDiff = preference.accidentalDragDistancePx - 1f,
)
assertThat(leftBlock.y).isEqualTo(startY)
assertThat(preference.mTimesRefreshedBlocks).isEqualTo(0)
}
@Test
fun accidentalDrag_TooFarToBeAccidentalXAxis() {
val (topBlock, _) = setupTwoDisplays(POSITION_TOP, childOffset = -42f)
val startTime = 88888L
val startX = topBlock.x
preference.mTimesRefreshedBlocks = 0
dragBlockWithOneMoveEvent(
topBlock, startTime,
endTimeMs = startTime + preference.accidentalDragTimeLimitMs - 10,
xDiff = preference.accidentalDragDistancePx + 1f, yDiff = 0f,
)
assertThat(preference.mTimesRefreshedBlocks).isEqualTo(1)
assertThat(topBlock.x).isNotEqualTo(startX)
}
@Test
fun accidentalDrag_TooFarToBeAccidentalYAxis() {
val (leftBlock, _) = setupTwoDisplays(POSITION_LEFT, childOffset = 42f)
val startTime = 88888L
val startY = leftBlock.y
preference.mTimesRefreshedBlocks = 0
dragBlockWithOneMoveEvent(
leftBlock, startTime,
endTimeMs = startTime + preference.accidentalDragTimeLimitMs - 10,
xDiff = 0f, yDiff = preference.accidentalDragDistancePx + 1f,
)
assertThat(leftBlock.y).isNotEqualTo(startY)
assertThat(preference.mTimesRefreshedBlocks).isEqualTo(1)
}
@Test
fun accidentalDrag_TooSlowToBeAccidental() {
val (topBlock, _) = setupTwoDisplays(POSITION_TOP, childOffset = -42f)
val startTime = 88888L
val startX = topBlock.x
val startY = topBlock.y
preference.mTimesRefreshedBlocks = 0
dragBlockWithOneMoveEvent(
topBlock, startTime,
endTimeMs = startTime + preference.accidentalDragTimeLimitMs + 10,
xDiff = preference.accidentalDragDistancePx - 1f, yDiff = 0f,
)
assertThat(topBlock.x).isNotEqualTo(startX)
assertThat(preference.mTimesRefreshedBlocks).isEqualTo(1)
}
} }