• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.connecteddevice.display
18 
19 import android.graphics.PointF
20 import android.graphics.RectF
21 
22 import java.util.Locale
23 
24 import kotlin.math.max
25 import kotlin.math.min
26 
27 // These extension methods make calls to min and max chainable.
Floatnull28 fun Float.atMost(n: Number): Float = min(this, n.toFloat())
29 fun Float.atLeast(n: Number): Float = max(this, n.toFloat())
30 
31 /**
32  * Contains the parameters needed for transforming global display coordinates to and from topology
33  * pane coordinates. This is necessary for implementing an interactive display topology pane. The
34  * pane allows dragging and dropping display blocks into place to define the topology. Conversion to
35  * pane coordinates is necessary when rendering the original topology. Conversion in the other
36  * direction, to display coordinates, is necessary for resolve a drag position to display space.
37  *
38  * The topology pane coordinates are physical pixels and represent the relative position from the
39  * upper-left corner of the pane. It uses a scale optimized for showing all displays with minimal
40  * or no scrolling. The display coordinates are floating point and the origin can be in any
41  * position. In practice the origin will be the upper-left coordinate of the primary display.
42  *
43  * @param paneWidth width of the pane in view coordinates
44  * @param minEdgeLength the smallest length permitted of a display block. This should be set based
45  *                      on accessibility requirements, but also accounting for padding that appears
46  *                      around each button.
47  * @param maxEdgeLength the longest width or height permitted of a display block. This will limit
48  *                      the amount of dragging and scrolling the user will need to do to set the
49  *                      arrangement.
50  * @param displaysPos the absolute topology coordinates for each display in the topology.
51  */
52 class TopologyScale(
53         paneWidth: Int, minEdgeLength: Float, maxEdgeLength: Float,
54         displaysPos: Collection<RectF>) {
55     /** Scale of block sizes to real-world display sizes. Should be less than 1. */
56     val blockRatio: Float
57 
58     /** Height of topology pane needed to allow all display blocks to appear with some padding. */
59     val paneHeight: Float
60 
61     /** Pane's X view coordinate that corresponds with topology's X=0 coordinate. */
62     val originPaneX: Float
63 
64     /** Pane's Y view coordinate that corresponds with topology's Y=0 coordinate. */
65     val originPaneY: Float
66 
67     init {
68         val displayBounds = RectF(
69                 Float.MAX_VALUE, Float.MAX_VALUE, Float.MIN_VALUE, Float.MIN_VALUE)
70         var smallestDisplayDim = Float.MAX_VALUE
71         var biggestDisplayDim = Float.MIN_VALUE
72 
73         // displayBounds is the smallest rect encompassing all displays, in display space.
74         // smallestDisplayDim is the size of the smallest display edge, in display space.
75         for (pos in displaysPos) {
76             displayBounds.union(pos)
77             smallestDisplayDim = minOf(smallestDisplayDim, pos.height(), pos.width())
78             biggestDisplayDim = maxOf(biggestDisplayDim, pos.height(), pos.width())
79         }
80 
81         // Initialize blockRatio such that there is 20% padding on left and right sides of the
82         // display bounds.
83         blockRatio = (paneWidth * 0.6 / displayBounds.width()).toFloat()
84                 // If the `ratio` is set too high because one of the displays will have an edge
85                 // greater than maxEdgeLength(px) long, decrease it such that the largest edge is
86                 // that long.
87                 .atMost(maxEdgeLength / biggestDisplayDim)
88                 // Also do the opposite of the above, this latter step taking precedence for a11y
89                 // requirements.
90                 .atLeast(minEdgeLength / smallestDisplayDim)
91 
92         // A tall pane is likely to result in more scrolling. So we
93         // prevent the height from growing too large here, by limiting vertical padding to
94         // 1.5x of the minEdgeLength on each side. This keeps a comfortable amount of
95         // padding without it resulting in too much deadspace.
96         paneHeight = blockRatio * displayBounds.height() + minEdgeLength * 3f
97 
98         // Set originPaneXY (the location of 0,0 in display space in the pane's coordinate system)
99         // such that the display bounds rect is centered in the pane.
100         // It is unlikely that either of these coordinates will be negative since blockRatio has
101         // been chosen to allow 20% padding around each side of the display blocks. However, the
102         // a11y requirement applied above (minEdgeLength / smallestDisplayDim) may cause the blocks
103         // to not fit. This should be rare in practice, and can be worked around by moving the
104         // settings UI to a larger display.
105         val blockMostLeft = (paneWidth - displayBounds.width() * blockRatio) / 2
106         val blockMostTop = (paneHeight - displayBounds.height() * blockRatio) / 2
107 
108         originPaneX = blockMostLeft - displayBounds.left * blockRatio
109         originPaneY = blockMostTop - displayBounds.top * blockRatio
110     }
111 
112     /** Transforms coordinates in view pane space to display space. */
113     fun paneToDisplayCoor(paneX: Float, paneY: Float): PointF {
114         return PointF((paneX - originPaneX) / blockRatio, (paneY - originPaneY) / blockRatio)
115     }
116 
117     /** Transforms coordinates in display space to view pane space. */
118     fun displayToPaneCoor(displayX: Float, displayY: Float): PointF {
119         return PointF(displayX * blockRatio + originPaneX, displayY * blockRatio + originPaneY)
120     }
121 
122     override fun toString() : String {
123         return String.format(
124                 Locale.ROOT,
125                 "{TopologyScale blockRatio=%f originPaneXY=%.1f,%.1f paneHeight=%.1f}",
126                 blockRatio, originPaneX, originPaneY, paneHeight)
127     }
128 }
129