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:
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user