clampPosition function for dragging display blocks
Uses the algorithm in the prototype and the design doc (go/extend-cd-settings). Bug: b/352650922 Test: atest TopologyClampTest.kt Flag: com.android.settings.flags.display_topology_pane_in_display_list Change-Id: I8d8d4427f5d5dc069b1529f8ca6ac2cee259ee8e
This commit is contained in:
@@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
* 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.settings.connecteddevice.display
|
||||||
|
|
||||||
|
import android.graphics.RectF
|
||||||
|
import kotlin.math.hypot
|
||||||
|
|
||||||
|
// Unfortunately, in the world of IEEE 32-bit floats, A + X - X is not always == A
|
||||||
|
// For example: A = 1075.4271f
|
||||||
|
// C = 1249.2203f
|
||||||
|
// For example: - A - 173.79326f = - C
|
||||||
|
// However: - C + A = - 173.79321f
|
||||||
|
// So we need to keep track of how the movingDisplay block is attaching to otherDisplays throughout
|
||||||
|
// the calculations below. We cannot use the rect.left with its width as a proxy for rect.right. We
|
||||||
|
// have to save the "inner" or attached side and use the width or height to calculate the "external"
|
||||||
|
// side.
|
||||||
|
|
||||||
|
/** A potential X position for the display to clamp at. */
|
||||||
|
private class XCoor(
|
||||||
|
val left : Float, val right : Float,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If present, the position of the display being attached to. If absent, indicates the X
|
||||||
|
* position is derived from the exact drag position.
|
||||||
|
*/
|
||||||
|
val attaching : RectF?,
|
||||||
|
)
|
||||||
|
|
||||||
|
/** A potential Y position for the display to clamp at. */
|
||||||
|
private class YCoor(
|
||||||
|
val top : Float, val bottom : Float,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If present, the position of the display being attached to. If absent, indicates the Y
|
||||||
|
* position is derived from the exact drag position.
|
||||||
|
*/
|
||||||
|
val attaching : RectF?,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the optimal clamp position assuming the user has dragged the block to `movingDisplay`.
|
||||||
|
*
|
||||||
|
* @param otherDisplays positions of the stationary displays (every one not being dragged)
|
||||||
|
* @param movingDisplay the position the user is current holding the block during a drag
|
||||||
|
*
|
||||||
|
* @return the clamp position as a RectF, whose dimensions will match that of `movingDisplay`
|
||||||
|
*/
|
||||||
|
fun clampPosition(otherDisplays : Iterable<RectF>, movingDisplay : RectF) : RectF {
|
||||||
|
val xCoors = otherDisplays.flatMap {
|
||||||
|
listOf(
|
||||||
|
// Attaching to left edge of `it`
|
||||||
|
XCoor(it.left - movingDisplay.width(), it.left, it),
|
||||||
|
// Attaching to right edge of `it`
|
||||||
|
XCoor(it.right, it.right + movingDisplay.width(), it),
|
||||||
|
)
|
||||||
|
}.plusElement(XCoor(movingDisplay.left, movingDisplay.right, null))
|
||||||
|
|
||||||
|
val yCoors = otherDisplays.flatMap {
|
||||||
|
listOf(
|
||||||
|
// Attaching to the top edge of `it`
|
||||||
|
YCoor(it.top - movingDisplay.height(), it.top, it),
|
||||||
|
// Attaching to the bottom edge of `it`
|
||||||
|
YCoor(it.bottom, it.bottom + movingDisplay.height(), it),
|
||||||
|
)
|
||||||
|
}.plusElement(YCoor(movingDisplay.top, movingDisplay.bottom, null))
|
||||||
|
|
||||||
|
class Cand(val x : XCoor, val y : YCoor)
|
||||||
|
|
||||||
|
val candidateGrid = xCoors.flatMap { x -> yCoors.map { y -> Cand(x, y) }}
|
||||||
|
val hasAttachInRange = candidateGrid.filter {
|
||||||
|
if (it.x.attaching != null) {
|
||||||
|
// Attaching to a vertical (left or right) edge. The y range of dragging and
|
||||||
|
// stationary blocks must overlap.
|
||||||
|
it.y.top <= it.x.attaching.bottom && it.y.bottom >= it.x.attaching.top
|
||||||
|
} else if (it.y.attaching != null) {
|
||||||
|
// Attaching to a horizontal (top or bottom) edge. The x range of dragging and
|
||||||
|
// stationary blocks must overlap.
|
||||||
|
it.x.left <= it.y.attaching.right && it.x.right >= it.y.attaching.left
|
||||||
|
} else {
|
||||||
|
// Not attaching to another display's edge at all, so not a valid clamp position.
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Clamp positions closest to the user's drag position are best. Sort by increasing distance
|
||||||
|
// from it, so the best will be first.
|
||||||
|
val prioritized = hasAttachInRange.sortedBy {
|
||||||
|
hypot(it.x.left - movingDisplay.left, it.y.top - movingDisplay.top)
|
||||||
|
}
|
||||||
|
val notIntersectingAny = prioritized.asSequence()
|
||||||
|
.map { RectF(it.x.left, it.y.top, it.x.right, it.y.bottom) }
|
||||||
|
.filter { p -> otherDisplays.all { !RectF.intersects(p, it) } }
|
||||||
|
|
||||||
|
// Note we return a copy of `movingDisplay` if there is no valid clamp position, which will only
|
||||||
|
// happen if `otherDisplays` is empty or has no valid rectangles. It may not be wise to rely on
|
||||||
|
// this behavior.
|
||||||
|
return notIntersectingAny.firstOrNull() ?: RectF(movingDisplay)
|
||||||
|
}
|
@@ -0,0 +1,144 @@
|
|||||||
|
/*
|
||||||
|
* 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.settings.connecteddevice.display
|
||||||
|
|
||||||
|
import android.graphics.PointF
|
||||||
|
import android.graphics.RectF
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
class TopologyClampTest {
|
||||||
|
@Test
|
||||||
|
fun clampToSides() {
|
||||||
|
val start = RectF(6f, 0f, 16f, 10f)
|
||||||
|
val clamp1 = clampPosition(listOf(RectF(0f, 0f, 10f, 10f)), start)
|
||||||
|
assertEquals(RectF(10f, 0f, 20f, 10f), clamp1)
|
||||||
|
|
||||||
|
val clamp2 = clampPosition(listOf(RectF(18f, 0f, 28f, 10f)), start)
|
||||||
|
assertEquals(RectF(8f, 0f, 18f, 10f), clamp2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun clampToTopOrBottom() {
|
||||||
|
val start = RectF(0f, 6f, 10f, 16f)
|
||||||
|
val clamp1 = clampPosition(listOf(RectF(0f, 0f, 10f, 10f)), start)
|
||||||
|
assertEquals(RectF(0f, 10f, 10f, 20f), clamp1)
|
||||||
|
|
||||||
|
val clamp2 = clampPosition(listOf(RectF(0f, 18f, 10f, 28f)), start)
|
||||||
|
assertEquals(RectF(0f, 8f, 10f, 18f), clamp2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun clampToCloserSide() {
|
||||||
|
// Shift one pixel right.
|
||||||
|
val start = RectF(9f, 8f, 19f, 18f)
|
||||||
|
val clamp1 = clampPosition(listOf(RectF(0f, 0f, 10f, 10f)), start)
|
||||||
|
assertEquals(RectF(10f, 8f, 20f, 18f), clamp1)
|
||||||
|
|
||||||
|
// Shift two pixels down.
|
||||||
|
start.set(7f, 8f, 17f, 18f)
|
||||||
|
val clamp2 = clampPosition(listOf(RectF(0f, 0f, 10f, 10f)), start)
|
||||||
|
assertEquals(RectF(7f, 10f, 17f, 20f), clamp2)
|
||||||
|
|
||||||
|
// Shift three pixels left.
|
||||||
|
start.set(-7f, -6f, 3f, 4f);
|
||||||
|
val s3 = clampPosition(listOf(RectF(0f, 0f, 10f, 10f)), start)
|
||||||
|
assertEquals(RectF(-10f, -6f, 0f, 4f), s3)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun clampToCloserDisplayInCorner() {
|
||||||
|
val start = RectF(9f, 6f, 19f, 16f)
|
||||||
|
val clamp1 = clampPosition(listOf(RectF(0f, 0f, 8f, 8f), RectF(8f, 0f, 16f, 4f)), start)
|
||||||
|
assertEquals(RectF(8f, 6f, 18f, 16f), clamp1)
|
||||||
|
|
||||||
|
start.set(10f, 5f, 20f, 15f)
|
||||||
|
val clamp2 = clampPosition(listOf(RectF(0f, 0f, 8f, 8f), RectF(8f, 0f, 16f, 4f)), start)
|
||||||
|
assertEquals(RectF(10f, 4f, 20f, 14f), clamp2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun clampToSecondDisplayToAvoidOverlap() {
|
||||||
|
val start = RectF(8f, 3f, 18f, 13f)
|
||||||
|
val clamp = clampPosition(listOf(RectF(0f, 0f, 8f, 8f), RectF(8f, 0f, 16f, 4f)), start)
|
||||||
|
assertEquals(RectF(8f, 4f, 18f, 14f), clamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun clampToInnerCorner() {
|
||||||
|
val start = RectF(4f, 4f, 14f, 14f)
|
||||||
|
val clamp = clampPosition(listOf(RectF(5f, 0f, 10f, 5f), RectF(0f, 5f, 5f, 10f)), start)
|
||||||
|
assertEquals(RectF(5f, 5f, 15f, 15f), clamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun mustBeAdjacent() {
|
||||||
|
val start = RectF(9f, 10f, 14f, 15f)
|
||||||
|
|
||||||
|
// Have candidate X, Y pair that is not adjacent to any display.
|
||||||
|
val clamp = clampPosition(listOf(RectF(5f, 0f, 10f, 5f), RectF(0f, 5f, 5f, 10f)), start)
|
||||||
|
assertEquals(RectF(5f, 10f, 10f, 15f), clamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun mustNotIntersect() {
|
||||||
|
// 1 and 2 are attached with 1/3 of their respective sides. Attempt to drag the other
|
||||||
|
// display to 1's lower-right corner. It should be forced to the right side of 2.
|
||||||
|
//111
|
||||||
|
//111
|
||||||
|
//111
|
||||||
|
// 222
|
||||||
|
// 222
|
||||||
|
// 222
|
||||||
|
|
||||||
|
val start = RectF(30f, 30f, 60f, 60f)
|
||||||
|
val clamp = clampPosition(listOf(RectF(0f, 0f, 30f, 30f), RectF(20f, 30f, 50f, 60f)), start)
|
||||||
|
assertEquals(RectF(50f, 30f, 80f, 60f), clamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun attachingToTwoRectsAtOnce() {
|
||||||
|
// 2 is being dragged and starts out overlapping 0 and 1, then it is
|
||||||
|
// clamped to the right side of 0 and the bottom of 1 at the same time.
|
||||||
|
//
|
||||||
|
//00
|
||||||
|
//002
|
||||||
|
// 2
|
||||||
|
// 11
|
||||||
|
// 11
|
||||||
|
|
||||||
|
val clamp = clampPosition(
|
||||||
|
listOf(RectF(0f, 0f, 20f, 20f), RectF(10f, 30f, 30f, 50f)),
|
||||||
|
RectF(10f, 11f, 20f, 31f))
|
||||||
|
|
||||||
|
assertEquals(RectF(20f, 10f, 30f, 30f), clamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun attachingToTwoRectsAtOnceAxisSwapped() {
|
||||||
|
// Same as previous but with x and y swapped.
|
||||||
|
|
||||||
|
val clamp = clampPosition(
|
||||||
|
listOf(RectF(0f, 0f, 20f, 20f), RectF(30f, 10f, 50f, 30f)),
|
||||||
|
RectF(11f, 10f, 31f, 20f))
|
||||||
|
|
||||||
|
assertEquals(RectF(10f, 20f, 30f, 30f), clamp)
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user