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.util.Log 20 21 /** 22 * Base class for responsive specs that holds a list of width and height specs. 23 * 24 * @param widthSpecs List of width responsive specifications 25 * @param heightSpecs List of height responsive specifications 26 */ 27 abstract class ResponsiveSpecs<T : ResponsiveSpec>( 28 val widthSpecs: List<T>, 29 val heightSpecs: List<T> 30 ) { 31 32 init { <lambda>null33 check(widthSpecs.isNotEmpty() && heightSpecs.isNotEmpty()) { 34 "${this::class.simpleName} is incomplete - " + 35 "width list size = ${widthSpecs.size}; " + 36 "height list size = ${heightSpecs.size}." 37 } 38 } 39 40 /** 41 * Get a [ResponsiveSpec] for width within the breakpoint. 42 * 43 * @param availableWidth The width breakpoint for the spec 44 * @return A [ResponsiveSpec] for width. 45 */ getWidthSpecnull46 fun getWidthSpec(availableWidth: Int): T { 47 val spec = widthSpecs.firstOrNull { availableWidth <= it.maxAvailableSize } 48 check(spec != null) { "No available width spec found within $availableWidth." } 49 return spec 50 } 51 52 /** 53 * Get a [ResponsiveSpec] for height within the breakpoint. 54 * 55 * @param availableHeight The height breakpoint for the spec 56 * @return A [ResponsiveSpec] for height. 57 */ getHeightSpecnull58 fun getHeightSpec(availableHeight: Int): T { 59 val spec = heightSpecs.firstOrNull { availableHeight <= it.maxAvailableSize } 60 check(spec != null) { "No available height spec found within $availableHeight." } 61 return spec 62 } 63 } 64 65 /** 66 * Base class for a responsive specification that is used to calculate the paddings, gutter and cell 67 * size. 68 * 69 * @param maxAvailableSize indicates the breakpoint to use this specification. 70 * @param specType indicates whether the paddings and gutters will be applied vertically or 71 * horizontally. 72 * @param startPadding padding used at the top or left (right in RTL) in the workspace folder. 73 * @param endPadding padding used at the bottom or right (left in RTL) in the workspace folder. 74 * @param gutter the space between the cells vertically or horizontally depending on the [specType]. 75 * @param cellSize height or width of the cell depending on the [specType]. 76 */ 77 abstract class ResponsiveSpec( 78 open val maxAvailableSize: Int, 79 open val specType: SpecType, 80 open val startPadding: SizeSpec, 81 open val endPadding: SizeSpec, 82 open val gutter: SizeSpec, 83 open val cellSize: SizeSpec 84 ) { isValidnull85 open fun isValid(): Boolean { 86 if (maxAvailableSize <= 0) { 87 Log.e(LOG_TAG, "${this::class.simpleName}#isValid - maxAvailableSize <= 0") 88 return false 89 } 90 91 // All specs need to be individually valid 92 if (!allSpecsAreValid()) { 93 Log.e(LOG_TAG, "${this::class.simpleName}#isValid - !allSpecsAreValid()") 94 return false 95 } 96 97 return true 98 } 99 allSpecsAreValidnull100 private fun allSpecsAreValid(): Boolean { 101 return startPadding.isValid() && 102 endPadding.isValid() && 103 gutter.isValid() && 104 cellSize.isValid() 105 } 106 107 enum class SpecType { 108 HEIGHT, 109 WIDTH 110 } 111 112 companion object { 113 private const val LOG_TAG = "ResponsiveSpec" 114 } 115 } 116 117 /** 118 * Calculated responsive specs contains the final paddings, gutter and cell size in pixels after 119 * they are calculated from the available space, cells and workspace specs. 120 */ 121 sealed class CalculatedResponsiveSpec { 122 var availableSpace: Int = 0 123 private set 124 125 var cells: Int = 0 126 private set 127 128 var startPaddingPx: Int = 0 129 private set 130 131 var endPaddingPx: Int = 0 132 private set 133 134 var gutterPx: Int = 0 135 private set 136 137 var cellSizePx: Int = 0 138 private set 139 140 var spec: ResponsiveSpec 141 private set 142 143 constructor( 144 availableSpace: Int, 145 cells: Int, 146 spec: ResponsiveSpec, 147 calculatedWorkspaceSpec: CalculatedWorkspaceSpec 148 ) { 149 this.availableSpace = availableSpace 150 this.cells = cells 151 this.spec = spec 152 153 // Map if is fixedSize, ofAvailableSpace or matchWorkspace 154 startPaddingPx = 155 spec.startPadding.getCalculatedValue( 156 availableSpace, 157 calculatedWorkspaceSpec.startPaddingPx 158 ) 159 endPaddingPx = 160 spec.endPadding.getCalculatedValue(availableSpace, calculatedWorkspaceSpec.endPaddingPx) 161 gutterPx = spec.gutter.getCalculatedValue(availableSpace, calculatedWorkspaceSpec.gutterPx) 162 cellSizePx = 163 spec.cellSize.getCalculatedValue(availableSpace, calculatedWorkspaceSpec.cellSizePx) 164 165 updateRemainderSpaces(availableSpace, cells, spec) 166 } 167 168 constructor(availableSpace: Int, cells: Int, spec: ResponsiveSpec) { 169 this.availableSpace = availableSpace 170 this.cells = cells 171 this.spec = spec 172 173 // Map if is fixedSize or ofAvailableSpace 174 startPaddingPx = spec.startPadding.getCalculatedValue(availableSpace) 175 endPaddingPx = spec.endPadding.getCalculatedValue(availableSpace) 176 gutterPx = spec.gutter.getCalculatedValue(availableSpace) 177 cellSizePx = spec.cellSize.getCalculatedValue(availableSpace) 178 179 updateRemainderSpaces(availableSpace, cells, spec) 180 } 181 updateRemainderSpacesnull182 private fun updateRemainderSpaces(availableSpace: Int, cells: Int, spec: ResponsiveSpec) { 183 val gutters = cells - 1 184 val usedSpace = startPaddingPx + endPaddingPx + (gutterPx * gutters) + (cellSizePx * cells) 185 val remainderSpace = availableSpace - usedSpace 186 187 startPaddingPx = spec.startPadding.getRemainderSpaceValue(remainderSpace, startPaddingPx) 188 endPaddingPx = spec.endPadding.getRemainderSpaceValue(remainderSpace, endPaddingPx) 189 gutterPx = spec.gutter.getRemainderSpaceValue(remainderSpace, gutterPx) 190 cellSizePx = spec.cellSize.getRemainderSpaceValue(remainderSpace, cellSizePx) 191 } 192 hashCodenull193 override fun hashCode(): Int { 194 var result = availableSpace.hashCode() 195 result = 31 * result + cells.hashCode() 196 result = 31 * result + startPaddingPx.hashCode() 197 result = 31 * result + endPaddingPx.hashCode() 198 result = 31 * result + gutterPx.hashCode() 199 result = 31 * result + cellSizePx.hashCode() 200 result = 31 * result + spec.hashCode() 201 return result 202 } 203 equalsnull204 override fun equals(other: Any?): Boolean { 205 return other is CalculatedResponsiveSpec && 206 availableSpace == other.availableSpace && 207 cells == other.cells && 208 startPaddingPx == other.startPaddingPx && 209 endPaddingPx == other.endPaddingPx && 210 gutterPx == other.gutterPx && 211 cellSizePx == other.cellSizePx && 212 spec == other.spec 213 } 214 toStringnull215 override fun toString(): String { 216 return "${this::class.simpleName}(" + 217 "availableSpace=$availableSpace, cells=$cells, startPaddingPx=$startPaddingPx, " + 218 "endPaddingPx=$endPaddingPx, gutterPx=$gutterPx, cellSizePx=$cellSizePx, " + 219 "${spec::class.simpleName}.maxAvailableSize=${spec.maxAvailableSize}" + 220 ")" 221 } 222 } 223