Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit cd218c49 authored by Matthew DeVore's avatar Matthew DeVore
Browse files

Topology Pane: narrow padding between blocks

Before this patch, the padding between display blocks was so wide that
the potential highlight border of all displays would not overlap with
other potential border positions.

This patch makes the padding between blocks narrower so that the
highlight border consumes a bigger proportion of the padding space.

When mirroring a potentially-non-Bitmap wallpaper (see b/397231553),
we need to use a SurfaceView, and cannot use View.background to show
the wallpaper. This CL prepares to use a SurfaceView by making the
wallpaper image its own separate View (mWallpaperView) rather than a
single layer in a LayerDrawable. The rest of the layers are separate
Views too.

Test: manual check with 3 display blocks in the pane
Test: DisplayTopologyPreferenceTest
Bug: b/397231553
Flag: com.android.settings.flags.display_topology_pane_in_display_list
Change-Id: Ib57bca55fb56448aab3fe1eb022ae5518c3163c8
parent 3796c202
Loading
Loading
Loading
Loading
+39 −0
Original line number Diff line number Diff line
@@ -15,9 +15,25 @@
  ~
  -->

<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- A non-rounded rectangle is needed to prevent the enclosed image from
         leaking out behind the rounded corners. -->
    <item>
        <shape android:shape="rectangle">
            <stroke
                android:color="@color/display_topology_background_color"
                android:width="@dimen/display_block_padding" />
        </shape>
    </item>

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

</layer-list>
+6 −22
Original line number Diff line number Diff line
@@ -15,25 +15,9 @@
  ~
  -->

<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">
<shape xmlns:android="http://schemas.android.com/apk/res/android" 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>
+56 −29
Original line number Diff line number Diff line
@@ -24,23 +24,25 @@ import android.graphics.Color
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.widget.Button
import android.view.View
import android.widget.FrameLayout

import androidx.annotation.VisibleForTesting

/** 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)
class DisplayBlock(context : Context) : FrameLayout(context) {
    @VisibleForTesting
    val mHighlightPx = context.resources.getDimensionPixelSize(
            R.dimen.display_block_highlight_width)

    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)
    val mWallpaperView = View(context)
    private val mBackgroundView = View(context).apply {
        background = context.getDrawable(R.drawable.display_block_background)
    }
    @VisibleForTesting
    val mSelectionMarkerView = View(context).apply {
        background = context.getDrawable(R.drawable.display_block_selection_marker_background)
    }

    init {
        isScrollContainer = false
@@ -49,27 +51,33 @@ class DisplayBlock(context : Context) : Button(context) {

        // 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
        y = topLeft.y
        addView(mWallpaperView)
        addView(mBackgroundView)
        addView(mSelectionMarkerView)
    }

    fun setWallpaper(wallpaper: Bitmap?) {
        val wallpaperDrawable = BitmapDrawable(context.resources, wallpaper ?: return)

        fun framedBy(bg: Drawable): Drawable =
            LayerDrawable(arrayOf(wallpaperDrawable, bg)).apply {
                setLayerInsetRelative(0, mInsetPx, mInsetPx, mInsetPx, mInsetPx)
        mWallpaperView.background = BitmapDrawable(context.resources, wallpaper ?: return)
    }
        mSelectedImage = framedBy(mSelectedBg)
        mUnselectedImage = framedBy(mUnselectedBg)

    /**
     * The coordinates of the upper-left corner of the block in pane coordinates, not including the
     * highlight border.
     */
    var positionInPane: PointF
        get() = PointF(x + mHighlightPx, y + mHighlightPx)
        set(value: PointF) {
            x = value.x - mHighlightPx
            y = value.y - mHighlightPx
        }

    fun setHighlighted(value: Boolean) {
        background = if (value) mSelectedImage else mUnselectedImage
        mSelectionMarkerView.visibility = if (value) View.VISIBLE else View.INVISIBLE

        // The highlighted block must be draw last so that its highlight shows over the borders of
        // other displays.
        z = if (value) 2f else 1f
    }

    /** Sets position and size of the block given unpadded bounds. */
@@ -77,9 +85,28 @@ class DisplayBlock(context : Context) : Button(context) {
        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).toInt()
        layout.height = (bottomRight.y - topLeft.y).toInt()
        val newWidth = (bottomRight.x - topLeft.x).toInt()
        val newHeight = (bottomRight.y - topLeft.y).toInt()
        layout.width = newWidth + 2*mHighlightPx
        layout.height = newHeight + 2*mHighlightPx
        layoutParams = layout
        place(topLeft)
        positionInPane = topLeft

        // The highlight is the outermost border. The highlight is shown outside of the parent
        // FrameLayout so that it consumes the padding between the blocks.
        mWallpaperView.layoutParams.let {
            it.width = newWidth
            it.height = newHeight
            if (it is MarginLayoutParams) {
                it.leftMargin = mHighlightPx
                it.topMargin = mHighlightPx
                it.bottomMargin = mHighlightPx
                it.topMargin = mHighlightPx
            }
            mWallpaperView.layoutParams = it
        }

        // The other two child views are MATCH_PARENT by default so will resize to fill up the
        // FrameLayout.
    }
}
+8 −8
Original line number Diff line number Diff line
@@ -268,9 +268,10 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector)
        // 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.
        val initialTopLeft = block.positionInPane
        mDrag = BlockDrag(
                stationaryDisps.toList(), block, displayId, displayPos.width(), displayPos.height(),
                initialBlockX = block.x, initialBlockY = block.y,
                initialBlockX = initialTopLeft.x, initialBlockY = initialTopLeft.y,
                initialTouchX = ev.rawX, initialTouchY = ev.rawY,
                startTimeMs = ev.eventTime,
        )
@@ -292,7 +293,7 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector)
                dispDragCoor.x + drag.displayWidth, dispDragCoor.y + drag.displayHeight)
        val snapRect = clampPosition(drag.stationaryDisps.map { it.second }, dispDragRect)

        drag.display.place(topology.scaling.displayToPaneCoor(snapRect.left, snapRect.top))
        drag.display.positionInPane = topology.scaling.displayToPaneCoor(snapRect.left, snapRect.top)

        return true
    }
@@ -303,18 +304,17 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector)
        mPaneContent.requestDisallowInterceptTouchEvent(false)
        drag.display.setHighlighted(false)

        val dropTopLeft = drag.display.positionInPane
        val netPxDragged = Math.hypot(
                (drag.initialBlockX - drag.display.x).toDouble(),
                (drag.initialBlockY - drag.display.y).toDouble())
                (drag.initialBlockX - dropTopLeft.x).toDouble(),
                (drag.initialBlockY - dropTopLeft.y).toDouble())
        val timeDownMs = ev.eventTime - drag.startTimeMs
        if (netPxDragged < accidentalDragDistancePx && timeDownMs < accidentalDragTimeLimitMs) {
            drag.display.x = drag.initialBlockX
            drag.display.y = drag.initialBlockY
            drag.display.positionInPane = PointF(drag.initialBlockX, drag.initialBlockY)
            return true
        }

        val newCoor = topology.scaling.paneToDisplayCoor(
                drag.display.x, drag.display.y)
        val newCoor = topology.scaling.paneToDisplayCoor(dropTopLeft.x, dropTopLeft.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)
+45 −26
Original line number Diff line number Diff line
@@ -23,7 +23,7 @@ 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.RectF
import android.hardware.display.DisplayTopology
import android.util.DisplayMetrics
import android.view.MotionEvent
@@ -100,6 +100,20 @@ class DisplayTopologyPreferenceTest {
        assertThat(preference.mTopologyHint.text).isEqualTo("")
    }

    /**
     * Returns the bounds of the non-highlighting part of the block relative to the parent.
     */
    private fun virtualBounds(block: DisplayBlock): RectF {
        val d = block.mHighlightPx.toFloat()
        val x = block.x + d
        val y = block.y + d
        // Using layoutParams as a proxy for the actual width and height appears to be standard
        // practice in Robolectric tests, as they do not actually process layout requests.
        val w = block.layoutParams.width - 2*d
        val h = block.layoutParams.height - 2*d
        return RectF(x, y, x + w, y + h)
    }

    private fun getPaneChildren(): List<DisplayBlock> =
        (0..preference.mPaneContent.childCount-1)
                .map { preference.mPaneContent.getChildAt(it) as DisplayBlock }
@@ -164,20 +178,26 @@ class DisplayTopologyPreferenceTest {
                paneChildren.reversed()
    }

    fun assertSelected(block: DisplayBlock, expected: Boolean) {
        val vis = if (expected) View.VISIBLE else View.INVISIBLE
        assertThat(block.mSelectionMarkerView.visibility).isEqualTo(vis)
    }

    @Test
    fun twoDisplaysGenerateBlocks() {
        val (childBlock, rootBlock) = setupTwoDisplays()
        val childBounds = virtualBounds(childBlock)
        val rootBounds = virtualBounds(rootBlock)

        // After accounting for padding, child should be half the length of root in each dimension.
        assertThat(childBlock.layoutParams.width)
                .isEqualTo(rootBlock.layoutParams.width / 2)
        assertThat(childBlock.layoutParams.height)
                .isEqualTo(rootBlock.layoutParams.height / 2)
        assertThat(childBlock.y).isGreaterThan(rootBlock.y)
        assertThat(childBlock.background).isEqualTo(childBlock.mUnselectedImage)
        assertThat(rootBlock.background).isEqualTo(rootBlock.mUnselectedImage)
        assertThat(rootBlock.x)
                .isEqualTo(childBlock.x + childBlock.layoutParams.width)
        assertThat(childBounds.width())
                .isEqualTo(rootBounds.width() / 2)
        assertThat(childBounds.height())
                .isEqualTo(rootBounds.height() / 2)
        assertThat(childBounds.top).isGreaterThan(rootBounds.top)
        assertSelected(childBlock, false)
        assertSelected(rootBlock, false)
        assertThat(rootBounds.left).isEqualTo(childBounds.right)

        assertThat(preference.mTopologyHint.text)
                .isEqualTo(context.getString(R.string.external_display_topology_hint))
@@ -196,9 +216,10 @@ class DisplayTopologyPreferenceTest {

        // Move the left block half of its height downward. This is 40 pixels in display
        // coordinates. The original offset is 42, so the new offset will be 42 + 40.
        val leftBounds = virtualBounds(leftBlock)
        val moveEvent = MotionEventBuilder.newBuilder()
                .setAction(MotionEvent.ACTION_MOVE)
                .setPointer(0f, leftBlock.layoutParams.height / 2f)
                .setPointer(0f, leftBounds.height() / 2f)
                .build()
        val upEvent = MotionEventBuilder.newBuilder().setAction(MotionEvent.ACTION_UP).build()

@@ -218,6 +239,7 @@ class DisplayTopologyPreferenceTest {
    @Test
    fun dragRootDisplayToNewSide() {
        val (leftBlock, rightBlock) = setupTwoDisplays()
        val leftBounds = virtualBounds(leftBlock)

        preference.mTimesRefreshedBlocks = 0

@@ -230,9 +252,7 @@ class DisplayTopologyPreferenceTest {
        // relying on the clamp algorithm to choose the correct side and offset.
        val moveEvent = MotionEventBuilder.newBuilder()
                .setAction(MotionEvent.ACTION_MOVE)
                .setPointer(
                        -leftBlock.layoutParams.width.toFloat(),
                        -leftBlock.layoutParams.height / 2f)
                .setPointer(-leftBounds.width(), -leftBounds.height() / 2f)
                .build()

        val upEvent = MotionEventBuilder.newBuilder().setAction(MotionEvent.ACTION_UP).build()
@@ -318,8 +338,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() /
                        it.layoutParams.height.toFloat() }
                .map { virtualBounds(it) }
                .map { it.width() / it.height() }
                .filter { abs(it - expectedAspectRatio) < 0.001f }
        ).hasSize(1)
    }
@@ -370,7 +390,7 @@ class DisplayTopologyPreferenceTest {
    fun updatedTopologyCancelsDragIfNonTrivialChange() {
        val (leftBlock, _) = setupTwoDisplays(POSITION_LEFT, /* childOffset= */ 42f)

        assertThat(leftBlock.y).isWithin(0.05f).of(143.76f)
        assertThat(leftBlock.positionInPane.y).isWithin(0.05f).of(143.76f)

        leftBlock.dispatchTouchEvent(MotionEventBuilder.newBuilder()
                .setAction(MotionEvent.ACTION_DOWN)
@@ -380,46 +400,46 @@ class DisplayTopologyPreferenceTest {
                .setAction(MotionEvent.ACTION_MOVE)
                .setPointer(0f, 30f)
                .build())
        assertThat(leftBlock.y).isWithin(0.05f).of(173.76f)
        assertThat(leftBlock.positionInPane.y).isWithin(0.05f).of(173.76f)

        // 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.y).isWithin(0.05f).of(173.76f)
        assertThat(leftBlock.positionInPane.y).isWithin(0.05f).of(173.76f)
        // Move block farther downward.
        leftBlock.dispatchTouchEvent(MotionEventBuilder.newBuilder()
                .setAction(MotionEvent.ACTION_MOVE)
                .setPointer(0f, 50f)
                .build())
        assertThat(leftBlock.y).isWithin(0.05f).of(193.76f)
        assertThat(leftBlock.positionInPane.y).isWithin(0.05f).of(193.76f)

        injector.topology = twoDisplayTopology(POSITION_LEFT, /* childOffset= */ 20f)
        injector.topologyListener!!.accept(injector.topology!!)

        assertThat(leftBlock.y).isWithin(0.05f).of(115.60f)
        assertThat(leftBlock.positionInPane.y).isWithin(0.05f).of(115.60f)
        // 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.y).isWithin(0.05f).of(115.60f)
        assertThat(leftBlock.positionInPane.y).isWithin(0.05f).of(115.60f)
    }

    @Test
    fun highlightDuringDrag() {
        val (leftBlock, _) = setupTwoDisplays(POSITION_LEFT, /* childOffset= */ 42f)

        assertThat(leftBlock.background).isEqualTo(leftBlock.mUnselectedImage)
        assertSelected(leftBlock, false)
        leftBlock.dispatchTouchEvent(MotionEventBuilder.newBuilder()
                .setAction(MotionEvent.ACTION_DOWN)
                .setPointer(0f, 0f)
                .build())
        assertThat(leftBlock.background).isEqualTo(leftBlock.mSelectedImage)
        assertSelected(leftBlock, true)
        leftBlock.dispatchTouchEvent(MotionEventBuilder.newBuilder()
                .setAction(MotionEvent.ACTION_UP)
                .build())
        assertThat(leftBlock.background).isEqualTo(leftBlock.mUnselectedImage)
        assertSelected(leftBlock, false)
    }

    fun dragBlockWithOneMoveEvent(
@@ -503,7 +523,6 @@ class DisplayTopologyPreferenceTest {
        val (topBlock, _) = setupTwoDisplays(POSITION_TOP, childOffset = -42f)
        val startTime = 88888L
        val startX = topBlock.x
        val startY = topBlock.y

        preference.mTimesRefreshedBlocks = 0
        dragBlockWithOneMoveEvent(
Loading