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:
@@ -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)
|
||||
}
|
||||
}
|
@@ -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
|
@@ -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)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user