/* * Copyright (C) 2023 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.launcher3.responsive import android.content.res.TypedArray import android.util.AttributeSet import android.util.Log import android.util.TypedValue import com.android.launcher3.R import com.android.launcher3.util.ResourceHelper import kotlin.math.roundToInt /** * [SizeSpec] is an attribute used to represent a property in the responsive grid specs. * * @param fixedSize a fixed size in dp to be used * @param ofAvailableSpace a percentage of the available space * @param ofRemainderSpace a percentage of the remaining space (available space minus used space) * @param matchWorkspace indicates whether the workspace value will be used or not. * @param maxSize restricts the maximum value allowed for the [SizeSpec]. */ data class SizeSpec( val fixedSize: Float = 0f, val ofAvailableSpace: Float = 0f, val ofRemainderSpace: Float = 0f, val matchWorkspace: Boolean = false, val maxSize: Int = Int.MAX_VALUE ) { /** Retrieves the correct value for [SizeSpec]. */ fun getCalculatedValue(availableSpace: Int, workspaceValue: Int = 0): Int { val calculatedValue = when { fixedSize > 0 -> fixedSize.roundToInt() matchWorkspace -> workspaceValue ofAvailableSpace > 0 -> (ofAvailableSpace * availableSpace).roundToInt() else -> 0 } return calculatedValue.coerceAtMost(maxSize) } /** * Calculates the [SizeSpec] value when remainder space value is defined. If no remainderSpace * is 0, returns a default value. * * @param remainderSpace The remainder space to be used for the calculation * @param defaultValue The default value to be returned when no ofRemainderSpace is defined * @param factor A number to divide the remainder space. The default value is 1. This property * is used to split equally the remainder space by the number of cells and gutters. */ fun getRemainderSpaceValue(remainderSpace: Int, defaultValue: Int, factor: Int = 1): Int { val remainderSpaceValue = if (ofRemainderSpace > 0) { (ofRemainderSpace * remainderSpace / factor).roundToInt() } else { defaultValue } return remainderSpaceValue.coerceAtMost(maxSize) } fun isValid(): Boolean { // All attributes are empty if (fixedSize < 0f && ofAvailableSpace <= 0f && ofRemainderSpace <= 0f && !matchWorkspace) { Log.e(TAG, "SizeSpec#isValid - all attributes are empty") return false } // More than one attribute is filled val attrCount = (if (fixedSize > 0) 1 else 0) + (if (ofAvailableSpace > 0) 1 else 0) + (if (ofRemainderSpace > 0) 1 else 0) + (if (matchWorkspace) 1 else 0) if (attrCount > 1) { Log.e(TAG, "SizeSpec#isValid - more than one attribute is filled") return false } // Values should be between 0 and 1 if (ofAvailableSpace !in 0f..1f || ofRemainderSpace !in 0f..1f) { Log.e(TAG, "SizeSpec#isValid - values should be between 0 and 1") return false } // Invalid fixed or max size if (fixedSize < 0f || maxSize < 0f) { Log.e(TAG, "SizeSpec#isValid - values should be bigger or equal to zero.") return false } if (fixedSize > 0f && fixedSize > maxSize) { Log.e(TAG, "SizeSpec#isValid - fixed size should be smaller than the max size.") return false } return true } fun onlyFixedSize(): Boolean { if (ofAvailableSpace > 0 || ofRemainderSpace > 0 || matchWorkspace) { Log.e(TAG, "SizeSpec#onlyFixedSize - only fixed size allowed for this tag") return false } return true } object XmlTags { const val START_PADDING = "startPadding" const val END_PADDING = "endPadding" const val GUTTER = "gutter" const val CELL_SIZE = "cellSize" const val HOTSEAT_QSB_SPACE = "hotseatQsbSpace" const val EDGE_PADDING = "edgePadding" const val ICON_SIZE = "iconSize" const val ICON_TEXT_SIZE = "iconTextSize" const val ICON_DRAWABLE_PADDING = "iconDrawablePadding" } companion object { private const val TAG = "SizeSpec" private fun getValue(a: TypedArray, index: Int): Float { return when (a.getType(index)) { TypedValue.TYPE_DIMENSION -> a.getDimensionPixelSize(index, 0).toFloat() TypedValue.TYPE_FLOAT -> a.getFloat(index, 0f) else -> 0f } } fun create(resourceHelper: ResourceHelper, attrs: AttributeSet): SizeSpec { val styledAttrs = resourceHelper.obtainStyledAttributes(attrs, R.styleable.SizeSpec) val fixedSize = getValue(styledAttrs, R.styleable.SizeSpec_fixedSize) val ofAvailableSpace = getValue(styledAttrs, R.styleable.SizeSpec_ofAvailableSpace) val ofRemainderSpace = getValue(styledAttrs, R.styleable.SizeSpec_ofRemainderSpace) val matchWorkspace = styledAttrs.getBoolean(R.styleable.SizeSpec_matchWorkspace, false) val maxSize = styledAttrs.getDimensionPixelSize(R.styleable.SizeSpec_maxSize, Int.MAX_VALUE) styledAttrs.recycle() return SizeSpec(fixedSize, ofAvailableSpace, ofRemainderSpace, matchWorkspace, maxSize) } } }