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) } }