Show border highlight when dragging display

In the process of adding highlight we use an extra feature of
LayerDrawable to add insets so that we can add padding without changing
the actual dimensions or position of the DisplayBlock Views.

To make it easier to keep the values consistent and to aid in conversion
between px and dp, use dimen values to store padding and highlight
metrics.

Bug: b/352650922
Flag: com.android.settings.flags.display_topology_pane_in_display_list
Test: atest DisplayTopologyPreferenceTest.kt
Change-Id: I51ff2ce4a086e84a0c529346f8ede90430090b11
This commit is contained in:
Matthew DeVore
2025-01-21 18:36:24 +00:00
parent dac774edab
commit 1fb8fb0b4f
7 changed files with 155 additions and 40 deletions

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 2025 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.
~
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Inner border -->
<item>
<shape android:shape="rectangle">
<stroke
android:color="@color/display_topology_background_color"
android:width="@dimen/display_block_padding" />
<corners android:radius="@dimen/display_block_corner_radius" />
</shape>
</item>
<!-- Outer border -->
<item>
<shape android:shape="rectangle">
<stroke
android:color="@color/system_secondary"
android:width="@dimen/display_block_highlight_width" />
<corners android:radius="@dimen/display_block_corner_radius" />
</shape>
</item>
</layer-list>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 2025 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.
~
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<stroke
android:color="@color/display_topology_background_color"
android:width="@dimen/display_block_padding" />
<corners android:radius="@dimen/display_block_corner_radius" />
</shape>

View File

@@ -82,5 +82,6 @@
<!-- Connected displays -->
<color name="display_topology_background_color">@color/settingslib_color_charcoal</color>
<color name="system_secondary">@android:color/system_secondary_dark</color>
</resources>

View File

@@ -224,4 +224,5 @@
<!-- Connected displays -->
<color name="display_topology_background_color">@color/settingslib_color_grey100</color>
<color name="system_secondary">@android:color/system_secondary_light</color>
</resources>

View File

@@ -550,4 +550,7 @@
<!-- Connected displays -->
<dimen name="display_topology_pane_margin">24dp</dimen>
<dimen name="display_block_padding">5dp</dimen>
<dimen name="display_block_highlight_width">2dp</dimen>
<dimen name="display_block_corner_radius">10dp</dimen>
</resources>

View File

@@ -20,12 +20,15 @@ import android.app.WallpaperManager
import com.android.settings.R
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.Point
import android.graphics.PointF
import android.graphics.RectF
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.LayerDrawable
import android.hardware.display.DisplayManager
import android.hardware.display.DisplayTopology
import android.hardware.display.DisplayTopology.TreeNode.POSITION_BOTTOM
@@ -168,36 +171,54 @@ class TopologyScale(
const val TOPOLOGY_PREFERENCE_KEY = "display_topology_preference"
/** Padding in pane coordinate pixels on each side of a display block. */
const val BLOCK_PADDING = 2f
/** Represents a draggable block in the topology pane. */
class DisplayBlock(context : Context) : Button(context) {
@VisibleForTesting var mSelectedImage: Drawable = ColorDrawable(Color.BLACK)
@VisibleForTesting var mUnselectedImage: Drawable = ColorDrawable(Color.BLACK)
private val mSelectedBg = context.getDrawable(
R.drawable.display_block_selection_marker_background)!!
private val mUnselectedBg = context.getDrawable(
R.drawable.display_block_unselected_background)!!
private val mInsetPx = context.resources.getDimensionPixelSize(R.dimen.display_block_padding)
init {
isScrollContainer = false
isVerticalScrollBarEnabled = false
isHorizontalScrollBarEnabled = false
// Prevents shadow from appearing around edge of button.
stateListAnimator = null
}
/** Sets position of the block given unpadded coordinates. */
fun place(topLeft: PointF) {
x = topLeft.x + BLOCK_PADDING
y = topLeft.y + BLOCK_PADDING
x = topLeft.x
y = topLeft.y
}
val unpaddedX: Float
get() = x - BLOCK_PADDING
fun setWallpaper(wallpaper: Bitmap?) {
val wallpaperDrawable = BitmapDrawable(context.resources, wallpaper ?: return)
val unpaddedY: Float
get() = y - BLOCK_PADDING
fun framedBy(bg: Drawable): Drawable =
LayerDrawable(arrayOf(wallpaperDrawable, bg)).apply {
setLayerInsetRelative(0, mInsetPx, mInsetPx, mInsetPx, mInsetPx)
}
mSelectedImage = framedBy(mSelectedBg)
mUnselectedImage = framedBy(mUnselectedBg)
}
fun setHighlighted(value: Boolean) {
background = if (value) mSelectedImage else mUnselectedImage
}
/** Sets position and size of the block given unpadded bounds. */
fun placeAndSize(bounds : RectF, scale : TopologyScale) {
val topLeft = scale.displayToPaneCoor(bounds.left, bounds.top)
val bottomRight = scale.displayToPaneCoor(bounds.right, bounds.bottom)
val layout = layoutParams
layout.width = (bottomRight.x - topLeft.x - BLOCK_PADDING * 2f).toInt()
layout.height = (bottomRight.y - topLeft.y - BLOCK_PADDING * 2f).toInt()
layout.width = (bottomRight.x - topLeft.x).toInt()
layout.height = (bottomRight.y - topLeft.y).toInt()
layoutParams = layout
place(topLeft)
}
@@ -284,8 +305,8 @@ class DisplayTopologyPreference(context : Context)
get() = displayManager.displayTopology
set(value) { displayManager.displayTopology = value }
open val wallpaper : Drawable
get() = WallpaperManager.getInstance(context).drawable ?: ColorDrawable(Color.BLACK)
open val wallpaper: Bitmap?
get() = WallpaperManager.getInstance(context).bitmap
open fun registerTopologyListener(listener: Consumer<DisplayTopology>) {
displayManager.registerTopologyListener(context.mainExecutor, listener)
@@ -386,14 +407,20 @@ class DisplayTopologyPreference(context : Context)
}
}
var wallpaperBitmap : Bitmap? = null
newBounds.forEach { (id, pos) ->
val block = recycleableBlocks.removeFirstOrNull() ?: DisplayBlock(context).apply {
if (wallpaperBitmap == null) {
wallpaperBitmap = injector.wallpaper
}
// We need a separate wallpaper Drawable for each display block, since each needs to
// be drawn at a separate size.
background = injector.wallpaper
setWallpaper(wallpaperBitmap)
mPaneContent.addView(this)
}
block.setHighlighted(false)
block.placeAndSize(pos, scaling)
block.setOnTouchListener { view, ev ->
@@ -422,12 +449,15 @@ class DisplayTopologyPreference(context : Context)
val stationaryDisps = positions.filter { it.first != displayId }
mDrag?.display?.setHighlighted(false)
block.setHighlighted(true)
// We have to use rawX and rawY for the coordinates since the view receiving the event is
// also the view that is moving. We need coordinates relative to something that isn't
// moving, and the raw coordinates are relative to the screen.
mDrag = BlockDrag(
stationaryDisps.toList(), block, displayId, displayPos.width(), displayPos.height(),
ev.rawX - block.unpaddedX, ev.rawY - block.unpaddedY)
ev.rawX - block.x, ev.rawY - block.y)
// 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.
@@ -454,9 +484,10 @@ class DisplayTopologyPreference(context : Context)
val drag = mDrag ?: return false
val topology = mTopologyInfo ?: return false
mPaneContent.requestDisallowInterceptTouchEvent(false)
drag.display.setHighlighted(false)
val newCoor = topology.scaling.paneToDisplayCoor(
drag.display.unpaddedX, drag.display.unpaddedY)
drag.display.x, drag.display.y)
val newTopology = topology.topology.copy()
val newPositions = drag.stationaryDisps.map { (id, pos) -> id to PointF(pos.left, pos.top) }
.plus(drag.displayId to newCoor)

View File

@@ -21,9 +21,9 @@ import android.hardware.display.DisplayTopology.TreeNode.POSITION_LEFT
import android.hardware.display.DisplayTopology.TreeNode.POSITION_TOP
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.graphics.drawable.ColorDrawable
import android.hardware.display.DisplayTopology
import android.view.MotionEvent
import android.view.View
@@ -50,7 +50,8 @@ class DisplayTopologyPreferenceTest {
val injector = TestInjector(context)
val rootView = View.inflate(context, preference.layoutResource, /*parent=*/ null)
val holder = PreferenceViewHolder.createInstanceForTests(rootView)
val wallpaper = ColorDrawable(Color.MAGENTA)
val wallpaper = Bitmap.createBitmap(
intArrayOf(Color.MAGENTA), /*width=*/ 1, /*height=*/ 1, Bitmap.Config.RGB_565)
init {
preference.injector = injector
@@ -60,14 +61,14 @@ class DisplayTopologyPreferenceTest {
class TestInjector(context : Context) : DisplayTopologyPreference.Injector(context) {
var topology: DisplayTopology? = null
var systemWallpaper: Drawable? = null
var systemWallpaper: Bitmap? = null
var topologyListener: Consumer<DisplayTopology>? = null
override var displayTopology : DisplayTopology?
get() = topology
set(value) { topology = value }
override val wallpaper : Drawable
override val wallpaper: Bitmap?
get() = systemWallpaper!!
override fun registerTopologyListener(listener: Consumer<DisplayTopology>) {
@@ -164,14 +165,14 @@ class DisplayTopologyPreferenceTest {
val (childBlock, rootBlock) = setupTwoDisplays()
// After accounting for padding, child should be half the length of root in each dimension.
assertThat(childBlock.layoutParams.width + BLOCK_PADDING)
assertThat(childBlock.layoutParams.width)
.isEqualTo(rootBlock.layoutParams.width / 2)
assertThat(childBlock.layoutParams.height + BLOCK_PADDING)
assertThat(childBlock.layoutParams.height)
.isEqualTo(rootBlock.layoutParams.height / 2)
assertThat(childBlock.y).isGreaterThan(rootBlock.y)
assertThat(childBlock.background).isEqualTo(wallpaper)
assertThat(rootBlock.background).isEqualTo(wallpaper)
assertThat(rootBlock.x - BLOCK_PADDING * 2)
assertThat(childBlock.background).isEqualTo(childBlock.mUnselectedImage)
assertThat(rootBlock.background).isEqualTo(rootBlock.mUnselectedImage)
assertThat(rootBlock.x)
.isEqualTo(childBlock.x + childBlock.layoutParams.width)
assertThat(preference.mTopologyHint.text)
@@ -180,7 +181,7 @@ class DisplayTopologyPreferenceTest {
@Test
fun dragDisplayDownward() {
val (leftBlock, rightBlock) = setupTwoDisplays()
val (leftBlock, _) = setupTwoDisplays()
val downEvent = MotionEventBuilder.newBuilder()
.setPointer(0f, 0f)
@@ -191,7 +192,7 @@ class DisplayTopologyPreferenceTest {
// coordinates. The original offset is 42, so the new offset will be 42 + 40.
val moveEvent = MotionEventBuilder.newBuilder()
.setAction(MotionEvent.ACTION_MOVE)
.setPointer(0f, leftBlock.layoutParams.height / 2f + BLOCK_PADDING)
.setPointer(0f, leftBlock.layoutParams.height / 2f)
.build()
val upEvent = MotionEventBuilder.newBuilder().setAction(MotionEvent.ACTION_UP).build()
@@ -220,7 +221,7 @@ class DisplayTopologyPreferenceTest {
val moveEvent = MotionEventBuilder.newBuilder()
.setAction(MotionEvent.ACTION_MOVE)
.setPointer(
-leftBlock.layoutParams.width - 2f * BLOCK_PADDING,
-leftBlock.layoutParams.width.toFloat(),
-leftBlock.layoutParams.height / 2f)
.build()
@@ -278,8 +279,8 @@ class DisplayTopologyPreferenceTest {
// Look for a display with the same unusual aspect ratio as the one we've added.
val expectedAspectRatio = 300f/320f
assertThat(paneChildren
.map { (it.layoutParams.width.toFloat() + BLOCK_PADDING*2) /
(it.layoutParams.height.toFloat() + BLOCK_PADDING*2) }
.map { it.layoutParams.width.toFloat() /
it.layoutParams.height.toFloat() }
.filter { abs(it - expectedAspectRatio) < 0.001f }
).hasSize(1)
}
@@ -305,7 +306,7 @@ class DisplayTopologyPreferenceTest {
assertThat(paneChildren).hasSize(1)
val block = paneChildren[0]
val origY = block.unpaddedY
val origY = block.y
block.dispatchTouchEvent(MotionEventBuilder.newBuilder()
.setAction(MotionEvent.ACTION_DOWN)
@@ -316,21 +317,21 @@ class DisplayTopologyPreferenceTest {
.setPointer(0f, 30f)
.build())
assertThat(block.unpaddedY).isWithin(0.01f).of(origY)
assertThat(block.y).isWithin(0.01f).of(origY)
block.dispatchTouchEvent(MotionEventBuilder.newBuilder()
.setAction(MotionEvent.ACTION_UP)
.build())
// Block should be back to original position.
assertThat(block.unpaddedY).isWithin(0.01f).of(origY)
assertThat(block.y).isWithin(0.01f).of(origY)
}
@Test
fun updatedTopologyCancelsDragIfNonTrivialChange() {
val (leftBlock, rightBlock) = setupTwoDisplays(POSITION_LEFT, /* childOffset= */ 42f)
val (leftBlock, _) = setupTwoDisplays(POSITION_LEFT, /* childOffset= */ 42f)
assertThat(leftBlock.unpaddedY).isWithin(0.01f).of(142.17f)
assertThat(leftBlock.y).isWithin(0.01f).of(142.17f)
leftBlock.dispatchTouchEvent(MotionEventBuilder.newBuilder()
.setAction(MotionEvent.ACTION_DOWN)
@@ -340,29 +341,45 @@ class DisplayTopologyPreferenceTest {
.setAction(MotionEvent.ACTION_MOVE)
.setPointer(0f, 30f)
.build())
assertThat(leftBlock.unpaddedY).isWithin(0.01f).of(172.17f)
assertThat(leftBlock.y).isWithin(0.01f).of(172.17f)
// Offset is only different by 0.5 dp, so the drag will not cancel.
injector.topology = twoDisplayTopology(POSITION_LEFT, /* childOffset= */ 41.5f)
injector.topologyListener!!.accept(injector.topology!!)
assertThat(leftBlock.unpaddedY).isWithin(0.01f).of(172.17f)
assertThat(leftBlock.y).isWithin(0.01f).of(172.17f)
// Move block farther downward.
leftBlock.dispatchTouchEvent(MotionEventBuilder.newBuilder()
.setAction(MotionEvent.ACTION_MOVE)
.setPointer(0f, 50f)
.build())
assertThat(leftBlock.unpaddedY).isWithin(0.01f).of(192.17f)
assertThat(leftBlock.y).isWithin(0.01f).of(192.17f)
injector.topology = twoDisplayTopology(POSITION_LEFT, /* childOffset= */ 20f)
injector.topologyListener!!.accept(injector.topology!!)
assertThat(leftBlock.unpaddedY).isWithin(0.01f).of(125.67f)
assertThat(leftBlock.y).isWithin(0.01f).of(125.67f)
// Another move in the opposite direction should not move the left block.
leftBlock.dispatchTouchEvent(MotionEventBuilder.newBuilder()
.setAction(MotionEvent.ACTION_MOVE)
.setPointer(0f, -20f)
.build())
assertThat(leftBlock.unpaddedY).isWithin(0.01f).of(125.67f)
assertThat(leftBlock.y).isWithin(0.01f).of(125.67f)
}
@Test
fun highlightDuringDrag() {
val (leftBlock, _) = setupTwoDisplays(POSITION_LEFT, /* childOffset= */ 42f)
assertThat(leftBlock.background).isEqualTo(leftBlock.mUnselectedImage)
leftBlock.dispatchTouchEvent(MotionEventBuilder.newBuilder()
.setAction(MotionEvent.ACTION_DOWN)
.setPointer(0f, 0f)
.build())
assertThat(leftBlock.background).isEqualTo(leftBlock.mSelectedImage)
leftBlock.dispatchTouchEvent(MotionEventBuilder.newBuilder()
.setAction(MotionEvent.ACTION_UP)
.build())
assertThat(leftBlock.background).isEqualTo(leftBlock.mUnselectedImage)
}
}