From 1fb8fb0b4fccbae03772e31581f22aed3cc81ad7 Mon Sep 17 00:00:00 2001 From: Matthew DeVore Date: Tue, 21 Jan 2025 18:36:24 +0000 Subject: [PATCH] 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 --- ...play_block_selection_marker_background.xml | 39 +++++++++++ .../display_block_unselected_background.xml | 23 +++++++ res/values-night/colors.xml | 1 + res/values/colors.xml | 1 + res/values/dimens.xml | 3 + .../display/DisplayTopology.kt | 63 +++++++++++++----- .../display/DisplayTopologyPreferenceTest.kt | 65 ++++++++++++------- 7 files changed, 155 insertions(+), 40 deletions(-) create mode 100644 res/drawable/display_block_selection_marker_background.xml create mode 100644 res/drawable/display_block_unselected_background.xml diff --git a/res/drawable/display_block_selection_marker_background.xml b/res/drawable/display_block_selection_marker_background.xml new file mode 100644 index 00000000000..54f2bd2e2a0 --- /dev/null +++ b/res/drawable/display_block_selection_marker_background.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/res/drawable/display_block_unselected_background.xml b/res/drawable/display_block_unselected_background.xml new file mode 100644 index 00000000000..7c964717e2a --- /dev/null +++ b/res/drawable/display_block_unselected_background.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/res/values-night/colors.xml b/res/values-night/colors.xml index 3892138bc25..df061b74193 100644 --- a/res/values-night/colors.xml +++ b/res/values-night/colors.xml @@ -82,5 +82,6 @@ @color/settingslib_color_charcoal + @android:color/system_secondary_dark diff --git a/res/values/colors.xml b/res/values/colors.xml index 6e3f96fc32c..5c516d59024 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -224,4 +224,5 @@ @color/settingslib_color_grey100 + @android:color/system_secondary_light diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 3359758d8bc..1f4a555b0eb 100755 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -550,4 +550,7 @@ 24dp + 5dp + 2dp + 10dp diff --git a/src/com/android/settings/connecteddevice/display/DisplayTopology.kt b/src/com/android/settings/connecteddevice/display/DisplayTopology.kt index cc075ebe76d..78f29e4cbc0 100644 --- a/src/com/android/settings/connecteddevice/display/DisplayTopology.kt +++ b/src/com/android/settings/connecteddevice/display/DisplayTopology.kt @@ -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) { 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) diff --git a/tests/robotests/src/com/android/settings/connecteddevice/display/DisplayTopologyPreferenceTest.kt b/tests/robotests/src/com/android/settings/connecteddevice/display/DisplayTopologyPreferenceTest.kt index d149605a748..08abfe2dc5e 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/display/DisplayTopologyPreferenceTest.kt +++ b/tests/robotests/src/com/android/settings/connecteddevice/display/DisplayTopologyPreferenceTest.kt @@ -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? = 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) { @@ -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) } }