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
This commit is contained in:
Matthew DeVore
2025-02-11 02:55:23 +00:00
parent da0bd7b412
commit f1fec80d25
3 changed files with 213 additions and 172 deletions

View File

@@ -0,0 +1,85 @@
/*
* 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)
}
}

View File

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

View File

@@ -0,0 +1,128 @@
/*
* 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)
}
}