Loading src/com/android/settings/connecteddevice/display/DisplayBlock.kt 0 → 100644 +85 −0 Original line number Original line 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) } } src/com/android/settings/connecteddevice/display/DisplayTopology.kt→src/com/android/settings/connecteddevice/display/DisplayTopologyPreference.kt +0 −172 Original line number Original line Diff line number Diff line Loading @@ -21,27 +21,14 @@ import com.android.settings.R import android.content.Context import android.content.Context import android.graphics.Bitmap import android.graphics.Bitmap import android.graphics.Color import android.graphics.Point import android.graphics.PointF import android.graphics.PointF import android.graphics.RectF 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.DisplayManager import android.hardware.display.DisplayTopology 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.DisplayMetrics import android.util.Log import android.view.DisplayInfo import android.view.DisplayInfo import android.view.MotionEvent import android.view.MotionEvent import android.view.ViewGroup import android.view.ViewTreeObserver import android.view.ViewTreeObserver import android.widget.Button import android.widget.FrameLayout import android.widget.FrameLayout import android.widget.TextView import android.widget.TextView Loading @@ -49,168 +36,9 @@ import androidx.annotation.VisibleForTesting import androidx.preference.Preference import androidx.preference.Preference import androidx.preference.PreferenceViewHolder import androidx.preference.PreferenceViewHolder import java.util.Locale import java.util.function.Consumer import java.util.function.Consumer import kotlin.math.abs 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 * DisplayTopologyPreference allows the user to change the display topology Loading src/com/android/settings/connecteddevice/display/TopologyScale.kt 0 → 100644 +128 −0 Original line number Original line 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) } } Loading
src/com/android/settings/connecteddevice/display/DisplayBlock.kt 0 → 100644 +85 −0 Original line number Original line 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) } }
src/com/android/settings/connecteddevice/display/DisplayTopology.kt→src/com/android/settings/connecteddevice/display/DisplayTopologyPreference.kt +0 −172 Original line number Original line Diff line number Diff line Loading @@ -21,27 +21,14 @@ import com.android.settings.R import android.content.Context import android.content.Context import android.graphics.Bitmap import android.graphics.Bitmap import android.graphics.Color import android.graphics.Point import android.graphics.PointF import android.graphics.PointF import android.graphics.RectF 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.DisplayManager import android.hardware.display.DisplayTopology 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.DisplayMetrics import android.util.Log import android.view.DisplayInfo import android.view.DisplayInfo import android.view.MotionEvent import android.view.MotionEvent import android.view.ViewGroup import android.view.ViewTreeObserver import android.view.ViewTreeObserver import android.widget.Button import android.widget.FrameLayout import android.widget.FrameLayout import android.widget.TextView import android.widget.TextView Loading @@ -49,168 +36,9 @@ import androidx.annotation.VisibleForTesting import androidx.preference.Preference import androidx.preference.Preference import androidx.preference.PreferenceViewHolder import androidx.preference.PreferenceViewHolder import java.util.Locale import java.util.function.Consumer import java.util.function.Consumer import kotlin.math.abs 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 * DisplayTopologyPreference allows the user to change the display topology Loading
src/com/android/settings/connecteddevice/display/TopologyScale.kt 0 → 100644 +128 −0 Original line number Original line 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) } }