1 /*
2  * Copyright (C) 2023 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.launcher3.responsive
18 
19 import android.content.res.TypedArray
20 import android.util.Log
21 import com.android.launcher3.R
22 import com.android.launcher3.responsive.ResponsiveSpec.Companion.ResponsiveSpecType
23 
24 /**
25  * Interface for responsive grid specs
26  *
27  * @property maxAvailableSize indicates the breakpoint to use this specification.
28  * @property dimensionType indicates whether the paddings and gutters will be applied vertically or
29  *   horizontally.
30  * @property specType a [ResponsiveSpecType] that indicates the type of the spec.
31  */
32 interface IResponsiveSpec {
33     val maxAvailableSize: Int
34     val dimensionType: ResponsiveSpec.DimensionType
35     val specType: ResponsiveSpecType
36 }
37 
38 /**
39  * Class for a responsive specification that is used to calculate the paddings, gutter and cell
40  * size.
41  *
42  * @param maxAvailableSize indicates the breakpoint to use this specification.
43  * @param dimensionType indicates whether the paddings and gutters will be applied vertically or
44  *   horizontally.
45  * @param specType a [ResponsiveSpecType] that indicates the type of the spec.
46  * @param startPadding padding used at the top or left (right in RTL) in the workspace folder.
47  * @param endPadding padding used at the bottom or right (left in RTL) in the workspace folder.
48  * @param gutter the space between the cells vertically or horizontally depending on the
49  *   [dimensionType].
50  * @param cellSize height or width of the cell depending on the [dimensionType].
51  */
52 data class ResponsiveSpec(
53     override val maxAvailableSize: Int,
54     override val dimensionType: DimensionType,
55     override val specType: ResponsiveSpecType,
56     val startPadding: SizeSpec,
57     val endPadding: SizeSpec,
58     val gutter: SizeSpec,
59     val cellSize: SizeSpec,
60 ) : IResponsiveSpec {
61     init {
<lambda>null62         check(isValid()) { "Invalid ResponsiveSpec found. $this" }
63     }
64 
65     constructor(
66         responsiveSpecType: ResponsiveSpecType,
67         attrs: TypedArray,
68         specs: Map<String, SizeSpec>
69     ) : this(
70         maxAvailableSize =
71             attrs.getDimensionPixelSize(R.styleable.ResponsiveSpec_maxAvailableSize, 0),
72         dimensionType =
73             DimensionType.entries[
74                     attrs.getInt(
75                         R.styleable.ResponsiveSpec_dimensionType,
76                         DimensionType.HEIGHT.ordinal
77                     )],
78         specType = responsiveSpecType,
79         startPadding = specs.getOrError(SizeSpec.XmlTags.START_PADDING),
80         endPadding = specs.getOrError(SizeSpec.XmlTags.END_PADDING),
81         gutter = specs.getOrError(SizeSpec.XmlTags.GUTTER),
82         cellSize = specs.getOrError(SizeSpec.XmlTags.CELL_SIZE)
83     )
84 
isValidnull85     fun isValid(): Boolean {
86         if (
87             (specType == ResponsiveSpecType.Workspace) &&
88                 (startPadding.matchWorkspace ||
89                     endPadding.matchWorkspace ||
90                     gutter.matchWorkspace ||
91                     cellSize.matchWorkspace)
92         ) {
93             logError("Workspace spec provided must not have any match workspace value.")
94             return false
95         }
96 
97         if (maxAvailableSize <= 0) {
98             logError("The property maxAvailableSize must be higher than 0.")
99             return false
100         }
101 
102         // All specs need to be individually valid
103         if (!allSpecsAreValid()) {
104             logError("One or more specs are invalid!")
105             return false
106         }
107 
108         if (!isValidRemainderSpace()) {
109             logError("The total Remainder Space used must be equal to 0 or 1.")
110             return false
111         }
112 
113         if (!isValidAvailableSpace()) {
114             logError("The total Available Space used must be lower or equal to 100%.")
115             return false
116         }
117 
118         if (!isValidFixedSize()) {
119             logError("The total Fixed Size used must be lower or equal to $maxAvailableSize.")
120             return false
121         }
122 
123         return true
124     }
125 
allSpecsAreValidnull126     private fun allSpecsAreValid(): Boolean {
127         return startPadding.isValid() &&
128             endPadding.isValid() &&
129             gutter.isValid() &&
130             cellSize.isValid()
131     }
132 
isValidRemainderSpacenull133     private fun isValidRemainderSpace(): Boolean {
134         val remainderSpaceUsed =
135             startPadding.ofRemainderSpace +
136                 endPadding.ofRemainderSpace +
137                 gutter.ofRemainderSpace +
138                 cellSize.ofRemainderSpace
139         return remainderSpaceUsed == 0f || remainderSpaceUsed == 1f
140     }
141 
isValidAvailableSpacenull142     private fun isValidAvailableSpace(): Boolean {
143         return startPadding.ofAvailableSpace +
144             endPadding.ofAvailableSpace +
145             gutter.ofAvailableSpace +
146             cellSize.ofAvailableSpace < 1f
147     }
148 
isValidFixedSizenull149     private fun isValidFixedSize(): Boolean {
150         return startPadding.fixedSize +
151             endPadding.fixedSize +
152             gutter.fixedSize +
153             cellSize.fixedSize <= maxAvailableSize
154     }
155 
logErrornull156     private fun logError(message: String) {
157         Log.e(LOG_TAG, "$LOG_TAG#isValid - $message - $this")
158     }
159 
160     enum class DimensionType {
161         HEIGHT,
162         WIDTH
163     }
164 
165     companion object {
166         private const val LOG_TAG = "ResponsiveSpec"
167 
168         enum class ResponsiveSpecType(val xmlTag: String) {
169             AllApps("allAppsSpec"),
170             Folder("folderSpec"),
171             Workspace("workspaceSpec"),
172             Hotseat("hotseatSpec"),
173             Cell("cellSpec")
174         }
175     }
176 }
177 
178 /**
179  * Calculated responsive specs contains the final paddings, gutter and cell size in pixels after
180  * they are calculated from the available space, cells and workspace specs.
181  */
182 class CalculatedResponsiveSpec {
183     var aspectRatio: Float = Float.NaN
184         private set
185 
186     var availableSpace: Int = 0
187         private set
188 
189     var cells: Int = 0
190         private set
191 
192     var startPaddingPx: Int = 0
193         private set
194 
195     var endPaddingPx: Int = 0
196         private set
197 
198     var gutterPx: Int = 0
199         private set
200 
201     var cellSizePx: Int = 0
202         private set
203 
204     var spec: ResponsiveSpec
205         private set
206 
207     constructor(
208         aspectRatio: Float,
209         availableSpace: Int,
210         cells: Int,
211         spec: ResponsiveSpec,
212         calculatedWorkspaceSpec: CalculatedResponsiveSpec
213     ) {
214         this.aspectRatio = aspectRatio
215         this.availableSpace = availableSpace
216         this.cells = cells
217         this.spec = spec
218 
219         // Map if is fixedSize, ofAvailableSpace or matchWorkspace
220         startPaddingPx =
221             spec.startPadding.getCalculatedValue(
222                 availableSpace,
223                 calculatedWorkspaceSpec.startPaddingPx
224             )
225         endPaddingPx =
226             spec.endPadding.getCalculatedValue(availableSpace, calculatedWorkspaceSpec.endPaddingPx)
227         gutterPx = spec.gutter.getCalculatedValue(availableSpace, calculatedWorkspaceSpec.gutterPx)
228         cellSizePx =
229             spec.cellSize.getCalculatedValue(availableSpace, calculatedWorkspaceSpec.cellSizePx)
230 
231         updateRemainderSpaces(availableSpace, cells, spec)
232     }
233 
234     constructor(aspectRatio: Float, availableSpace: Int, cells: Int, spec: ResponsiveSpec) {
235         this.aspectRatio = aspectRatio
236         this.availableSpace = availableSpace
237         this.cells = cells
238         this.spec = spec
239 
240         // Map if is fixedSize or ofAvailableSpace
241         startPaddingPx = spec.startPadding.getCalculatedValue(availableSpace)
242         endPaddingPx = spec.endPadding.getCalculatedValue(availableSpace)
243         gutterPx = spec.gutter.getCalculatedValue(availableSpace)
244         cellSizePx = spec.cellSize.getCalculatedValue(availableSpace)
245 
246         updateRemainderSpaces(availableSpace, cells, spec)
247     }
248 
isResponsiveSpecTypenull249     fun isResponsiveSpecType(type: ResponsiveSpecType) = spec.specType == type
250 
251     private fun updateRemainderSpaces(availableSpace: Int, cells: Int, spec: ResponsiveSpec) {
252         val gutters = cells - 1
253         val usedSpace = startPaddingPx + endPaddingPx + (gutterPx * gutters) + (cellSizePx * cells)
254         val remainderSpace = availableSpace - usedSpace
255 
256         startPaddingPx = spec.startPadding.getRemainderSpaceValue(remainderSpace, startPaddingPx)
257         endPaddingPx = spec.endPadding.getRemainderSpaceValue(remainderSpace, endPaddingPx)
258         gutterPx = spec.gutter.getRemainderSpaceValue(remainderSpace, gutterPx, gutters)
259         cellSizePx = spec.cellSize.getRemainderSpaceValue(remainderSpace, cellSizePx, cells)
260     }
261 
hashCodenull262     override fun hashCode(): Int {
263         var result = availableSpace.hashCode()
264         result = 31 * result + cells.hashCode()
265         result = 31 * result + startPaddingPx.hashCode()
266         result = 31 * result + endPaddingPx.hashCode()
267         result = 31 * result + gutterPx.hashCode()
268         result = 31 * result + cellSizePx.hashCode()
269         result = 31 * result + spec.hashCode()
270         return result
271     }
272 
equalsnull273     override fun equals(other: Any?): Boolean {
274         return other is CalculatedResponsiveSpec &&
275             availableSpace == other.availableSpace &&
276             cells == other.cells &&
277             startPaddingPx == other.startPaddingPx &&
278             endPaddingPx == other.endPaddingPx &&
279             gutterPx == other.gutterPx &&
280             cellSizePx == other.cellSizePx &&
281             spec == other.spec
282     }
283 
toStringnull284     override fun toString(): String {
285         return "Calculated${spec.specType}Spec(" +
286             "availableSpace=$availableSpace, cells=$cells, startPaddingPx=$startPaddingPx, " +
287             "endPaddingPx=$endPaddingPx, gutterPx=$gutterPx, cellSizePx=$cellSizePx, " +
288             "aspectRatio=${aspectRatio}, " +
289             "${spec.specType}Spec.maxAvailableSize=${spec.maxAvailableSize}" +
290             ")"
291     }
292 }
293