From f1fec80d2583c953367576f95bd0f88d68657388 Mon Sep 17 00:00:00 2001 From: Matthew DeVore Date: Tue, 11 Feb 2025 02:55:23 +0000 Subject: [PATCH] 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 --- .../connecteddevice/display/DisplayBlock.kt | 85 +++++++++ ...pology.kt => DisplayTopologyPreference.kt} | 172 ------------------ .../connecteddevice/display/TopologyScale.kt | 128 +++++++++++++ 3 files changed, 213 insertions(+), 172 deletions(-) create mode 100644 src/com/android/settings/connecteddevice/display/DisplayBlock.kt rename src/com/android/settings/connecteddevice/display/{DisplayTopology.kt => DisplayTopologyPreference.kt} (63%) create mode 100644 src/com/android/settings/connecteddevice/display/TopologyScale.kt diff --git a/src/com/android/settings/connecteddevice/display/DisplayBlock.kt b/src/com/android/settings/connecteddevice/display/DisplayBlock.kt new file mode 100644 index 00000000000..1226839317a --- /dev/null +++ b/src/com/android/settings/connecteddevice/display/DisplayBlock.kt @@ -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) + } +} diff --git a/src/com/android/settings/connecteddevice/display/DisplayTopology.kt b/src/com/android/settings/connecteddevice/display/DisplayTopologyPreference.kt similarity index 63% rename from src/com/android/settings/connecteddevice/display/DisplayTopology.kt rename to src/com/android/settings/connecteddevice/display/DisplayTopologyPreference.kt index 9cb79eed2ef..42e633f62e7 100644 --- a/src/com/android/settings/connecteddevice/display/DisplayTopology.kt +++ b/src/com/android/settings/connecteddevice/display/DisplayTopologyPreference.kt @@ -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) { - /** 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 diff --git a/src/com/android/settings/connecteddevice/display/TopologyScale.kt b/src/com/android/settings/connecteddevice/display/TopologyScale.kt new file mode 100644 index 00000000000..090646d8eda --- /dev/null +++ b/src/com/android/settings/connecteddevice/display/TopologyScale.kt @@ -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) { + /** 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) + } +}