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

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

Split up DisplayTopology.kt into 1-per-class files

This avoids a file naming conflict with DisplayTopology.java in
DisplayManager, and splits a large file. It was trivial to do since the
file had 3 top-level classes.

Test: local build and SQ
Test: atest DisplayTopologyPreferenceTest.kt
Flag: com.android.settings.flags.display_topology_pane_in_display_list
Bug: b/352648432
Change-Id: I4adc8167ab01b39a6da49f95f0cd072acec67ad4
parent da0bd7b4
Loading
Loading
Loading
Loading
+85 −0
Original line number Diff line number Diff line
/*
 * 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 com.android.settings.R

import android.content.Context
import android.graphics.Bitmap
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 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)

    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
        y = topLeft.y
    }

    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)
            }
        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).toInt()
        layout.height = (bottomRight.y - topLeft.y).toInt()
        layoutParams = layout
        place(topLeft)
    }
}
+0 −172
Original line number Diff line number Diff line
@@ -21,27 +21,14 @@ 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
import android.hardware.display.DisplayTopology.TreeNode.POSITION_LEFT
import android.hardware.display.DisplayTopology.TreeNode.POSITION_RIGHT
import android.hardware.display.DisplayTopology.TreeNode.POSITION_TOP
import android.util.DisplayMetrics
import android.util.Log
import android.view.DisplayInfo
import android.view.MotionEvent
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.Button
import android.widget.FrameLayout
import android.widget.TextView

@@ -49,168 +36,9 @@ import androidx.annotation.VisibleForTesting
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder

import java.util.Locale
import java.util.function.Consumer

import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min

// These extension methods make calls to min and max chainable.
fun Float.atMost(n: Number): Float = min(this, n.toFloat())
fun Float.atLeast(n: Number): Float = max(this, n.toFloat())

/**
 * Contains the parameters needed for transforming global display coordinates to and from topology
 * pane coordinates. This is necessary for implementing an interactive display topology pane. The
 * pane allows dragging and dropping display blocks into place to define the topology. Conversion to
 * pane coordinates is necessary when rendering the original topology. Conversion in the other
 * direction, to display coordinates, is necessary for resolve a drag position to display space.
 *
 * The topology pane coordinates are physical pixels and represent the relative position from the
 * upper-left corner of the pane. It uses a scale optimized for showing all displays with minimal
 * or no scrolling. The display coordinates are floating point and the origin can be in any
 * position. In practice the origin will be the upper-left coordinate of the primary display.
 *
 * @param paneWidth width of the pane in view coordinates
 * @param minEdgeLength the smallest length permitted of a display block. This should be set based
 *                      on accessibility requirements, but also accounting for padding that appears
 *                      around each button.
 * @param maxEdgeLength the longest width or height permitted of a display block. This will limit
 *                      the amount of dragging and scrolling the user will need to do to set the
 *                      arrangement.
 * @param displaysPos the absolute topology coordinates for each display in the topology.
 */
class TopologyScale(
        paneWidth: Int, minEdgeLength: Float, maxEdgeLength: Float,
        displaysPos: Collection<RectF>) {
    /** Scale of block sizes to real-world display sizes. Should be less than 1. */
    val blockRatio: Float

    /** Height of topology pane needed to allow all display blocks to appear with some padding. */
    val paneHeight: Float

    /** Pane's X view coordinate that corresponds with topology's X=0 coordinate. */
    val originPaneX: Float

    /** Pane's Y view coordinate that corresponds with topology's Y=0 coordinate. */
    val originPaneY: Float

    init {
        val displayBounds = RectF(
                Float.MAX_VALUE, Float.MAX_VALUE, Float.MIN_VALUE, Float.MIN_VALUE)
        var smallestDisplayDim = Float.MAX_VALUE
        var biggestDisplayDim = Float.MIN_VALUE

        // displayBounds is the smallest rect encompassing all displays, in display space.
        // smallestDisplayDim is the size of the smallest display edge, in display space.
        for (pos in displaysPos) {
            displayBounds.union(pos)
            smallestDisplayDim = minOf(smallestDisplayDim, pos.height(), pos.width())
            biggestDisplayDim = maxOf(biggestDisplayDim, pos.height(), pos.width())
        }

        // Initialize blockRatio such that there is 20% padding on left and right sides of the
        // display bounds.
        blockRatio = (paneWidth * 0.6 / displayBounds.width()).toFloat()
                // If the `ratio` is set too high because one of the displays will have an edge
                // greater than maxEdgeLength(px) long, decrease it such that the largest edge is
                // that long.
                .atMost(maxEdgeLength / biggestDisplayDim)
                // Also do the opposite of the above, this latter step taking precedence for a11y
                // requirements.
                .atLeast(minEdgeLength / smallestDisplayDim)

        // A tall pane is likely to result in more scrolling. So we
        // prevent the height from growing too large here, by limiting vertical padding to
        // 1.5x of the minEdgeLength on each side. This keeps a comfortable amount of
        // padding without it resulting in too much deadspace.
        paneHeight = blockRatio * displayBounds.height() + minEdgeLength * 3f

        // Set originPaneXY (the location of 0,0 in display space in the pane's coordinate system)
        // such that the display bounds rect is centered in the pane.
        // It is unlikely that either of these coordinates will be negative since blockRatio has
        // been chosen to allow 20% padding around each side of the display blocks. However, the
        // a11y requirement applied above (minEdgeLength / smallestDisplayDim) may cause the blocks
        // to not fit. This should be rare in practice, and can be worked around by moving the
        // settings UI to a larger display.
        val blockMostLeft = (paneWidth - displayBounds.width() * blockRatio) / 2
        val blockMostTop = (paneHeight - displayBounds.height() * blockRatio) / 2

        originPaneX = blockMostLeft - displayBounds.left * blockRatio
        originPaneY = blockMostTop - displayBounds.top * blockRatio
    }

    /** Transforms coordinates in view pane space to display space. */
    fun paneToDisplayCoor(paneX: Float, paneY: Float): PointF {
        return PointF((paneX - originPaneX) / blockRatio, (paneY - originPaneY) / blockRatio)
    }

    /** Transforms coordinates in display space to view pane space. */
    fun displayToPaneCoor(displayX: Float, displayY: Float): PointF {
        return PointF(displayX * blockRatio + originPaneX, displayY * blockRatio + originPaneY)
    }

    override fun toString() : String {
        return String.format(
                Locale.ROOT,
                "{TopologyScale blockRatio=%f originPaneXY=%.1f,%.1f paneHeight=%.1f}",
                blockRatio, originPaneX, originPaneY, paneHeight)
    }
}

/** 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
        y = topLeft.y
    }

    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)
            }
        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).toInt()
        layout.height = (bottomRight.y - topLeft.y).toInt()
        layoutParams = layout
        place(topLeft)
    }
}

/**
 * DisplayTopologyPreference allows the user to change the display topology
+128 −0
Original line number Diff line number Diff line
/*
 * 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 java.util.Locale

import kotlin.math.max
import kotlin.math.min

// These extension methods make calls to min and max chainable.
fun Float.atMost(n: Number): Float = min(this, n.toFloat())
fun Float.atLeast(n: Number): Float = max(this, n.toFloat())

/**
 * Contains the parameters needed for transforming global display coordinates to and from topology
 * pane coordinates. This is necessary for implementing an interactive display topology pane. The
 * pane allows dragging and dropping display blocks into place to define the topology. Conversion to
 * pane coordinates is necessary when rendering the original topology. Conversion in the other
 * direction, to display coordinates, is necessary for resolve a drag position to display space.
 *
 * The topology pane coordinates are physical pixels and represent the relative position from the
 * upper-left corner of the pane. It uses a scale optimized for showing all displays with minimal
 * or no scrolling. The display coordinates are floating point and the origin can be in any
 * position. In practice the origin will be the upper-left coordinate of the primary display.
 *
 * @param paneWidth width of the pane in view coordinates
 * @param minEdgeLength the smallest length permitted of a display block. This should be set based
 *                      on accessibility requirements, but also accounting for padding that appears
 *                      around each button.
 * @param maxEdgeLength the longest width or height permitted of a display block. This will limit
 *                      the amount of dragging and scrolling the user will need to do to set the
 *                      arrangement.
 * @param displaysPos the absolute topology coordinates for each display in the topology.
 */
class TopologyScale(
        paneWidth: Int, minEdgeLength: Float, maxEdgeLength: Float,
        displaysPos: Collection<RectF>) {
    /** Scale of block sizes to real-world display sizes. Should be less than 1. */
    val blockRatio: Float

    /** Height of topology pane needed to allow all display blocks to appear with some padding. */
    val paneHeight: Float

    /** Pane's X view coordinate that corresponds with topology's X=0 coordinate. */
    val originPaneX: Float

    /** Pane's Y view coordinate that corresponds with topology's Y=0 coordinate. */
    val originPaneY: Float

    init {
        val displayBounds = RectF(
                Float.MAX_VALUE, Float.MAX_VALUE, Float.MIN_VALUE, Float.MIN_VALUE)
        var smallestDisplayDim = Float.MAX_VALUE
        var biggestDisplayDim = Float.MIN_VALUE

        // displayBounds is the smallest rect encompassing all displays, in display space.
        // smallestDisplayDim is the size of the smallest display edge, in display space.
        for (pos in displaysPos) {
            displayBounds.union(pos)
            smallestDisplayDim = minOf(smallestDisplayDim, pos.height(), pos.width())
            biggestDisplayDim = maxOf(biggestDisplayDim, pos.height(), pos.width())
        }

        // Initialize blockRatio such that there is 20% padding on left and right sides of the
        // display bounds.
        blockRatio = (paneWidth * 0.6 / displayBounds.width()).toFloat()
                // If the `ratio` is set too high because one of the displays will have an edge
                // greater than maxEdgeLength(px) long, decrease it such that the largest edge is
                // that long.
                .atMost(maxEdgeLength / biggestDisplayDim)
                // Also do the opposite of the above, this latter step taking precedence for a11y
                // requirements.
                .atLeast(minEdgeLength / smallestDisplayDim)

        // A tall pane is likely to result in more scrolling. So we
        // prevent the height from growing too large here, by limiting vertical padding to
        // 1.5x of the minEdgeLength on each side. This keeps a comfortable amount of
        // padding without it resulting in too much deadspace.
        paneHeight = blockRatio * displayBounds.height() + minEdgeLength * 3f

        // Set originPaneXY (the location of 0,0 in display space in the pane's coordinate system)
        // such that the display bounds rect is centered in the pane.
        // It is unlikely that either of these coordinates will be negative since blockRatio has
        // been chosen to allow 20% padding around each side of the display blocks. However, the
        // a11y requirement applied above (minEdgeLength / smallestDisplayDim) may cause the blocks
        // to not fit. This should be rare in practice, and can be worked around by moving the
        // settings UI to a larger display.
        val blockMostLeft = (paneWidth - displayBounds.width() * blockRatio) / 2
        val blockMostTop = (paneHeight - displayBounds.height() * blockRatio) / 2

        originPaneX = blockMostLeft - displayBounds.left * blockRatio
        originPaneY = blockMostTop - displayBounds.top * blockRatio
    }

    /** Transforms coordinates in view pane space to display space. */
    fun paneToDisplayCoor(paneX: Float, paneY: Float): PointF {
        return PointF((paneX - originPaneX) / blockRatio, (paneY - originPaneY) / blockRatio)
    }

    /** Transforms coordinates in display space to view pane space. */
    fun displayToPaneCoor(displayX: Float, displayY: Float): PointF {
        return PointF(displayX * blockRatio + originPaneX, displayY * blockRatio + originPaneY)
    }

    override fun toString() : String {
        return String.format(
                Locale.ROOT,
                "{TopologyScale blockRatio=%f originPaneXY=%.1f,%.1f paneHeight=%.1f}",
                blockRatio, originPaneX, originPaneY, paneHeight)
    }
}