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