Improving responsive grid xml parser
Refactors AllAppsSpecs, FolderSpecs and WorkspaceSpecs initialization to use the same code to parse the xml with different map function. This CL improves the readability of the code and remove code duplication.
Fix: 286538013
Flag: ENABLE_RESPONSIVE_WORKSPACE
Test: AllAppsSpecsTes
Test: CalculatedAllAppsSpecTest
Test: CalculatedFolderSpecsTest
Test: CalculatedWorkspaceSpecTest
Test: FolderSpecsTest
Test: WorkspaceSpecsTest
Test: DeviceProfileResponsiveDumpTest
Test: DeviceProfileResponsiveAlternativeDisplaysDumpTest
Change-Id: Iec5863619399efd2e80f3db46b75c4d785e1656f
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index f3b5155..88dcfcd 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -56,15 +56,15 @@
import com.android.launcher3.responsive.AllAppsSpecs;
import com.android.launcher3.responsive.CalculatedAllAppsSpec;
import com.android.launcher3.responsive.CalculatedFolderSpec;
+import com.android.launcher3.responsive.CalculatedWorkspaceSpec;
import com.android.launcher3.responsive.FolderSpecs;
+import com.android.launcher3.responsive.WorkspaceSpecs;
import com.android.launcher3.uioverrides.ApiWrapper;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.DisplayController.Info;
import com.android.launcher3.util.IconSizeSteps;
import com.android.launcher3.util.ResourceHelper;
import com.android.launcher3.util.WindowBounds;
-import com.android.launcher3.workspace.CalculatedWorkspaceSpec;
-import com.android.launcher3.workspace.WorkspaceSpecs;
import java.io.PrintWriter;
import java.util.Locale;
@@ -115,13 +115,10 @@
// Responsive grid
private final boolean mIsResponsiveGrid;
- private WorkspaceSpecs mWorkspaceSpecs;
private CalculatedWorkspaceSpec mResponsiveWidthSpec;
private CalculatedWorkspaceSpec mResponsiveHeightSpec;
- private AllAppsSpecs mAllAppsSpecs;
private CalculatedAllAppsSpec mAllAppsResponsiveWidthSpec;
private CalculatedAllAppsSpec mAllAppsResponsiveHeightSpec;
- private FolderSpecs mFolderSpecs;
private CalculatedFolderSpec mResponsiveFolderWidthSpec;
private CalculatedFolderSpec mResponsiveFolderHeightSpec;
@@ -545,29 +542,31 @@
// Needs to be calculated after hotseatBarSizePx is correct,
// for the available height to be correct
if (mIsResponsiveGrid) {
- mWorkspaceSpecs = new WorkspaceSpecs(new ResourceHelper(context, inv.workspaceSpecsId));
+ WorkspaceSpecs workspaceSpecs = WorkspaceSpecs.create(
+ new ResourceHelper(context, inv.workspaceSpecsId));
int availableResponsiveWidth =
availableWidthPx - (isVerticalBarLayout() ? hotseatBarSizePx : 0);
// don't use availableHeightPx because it subtracts bottom padding,
// but the workspace go behind it
int availableResponsiveHeight =
heightPx - mInsets.top - (isVerticalBarLayout() ? 0 : hotseatBarSizePx);
- mResponsiveWidthSpec = mWorkspaceSpecs.getCalculatedWidthSpec(inv.numColumns,
+ mResponsiveWidthSpec = workspaceSpecs.getCalculatedWidthSpec(inv.numColumns,
availableResponsiveWidth);
- mResponsiveHeightSpec = mWorkspaceSpecs.getCalculatedHeightSpec(inv.numRows,
+ mResponsiveHeightSpec = workspaceSpecs.getCalculatedHeightSpec(inv.numRows,
availableResponsiveHeight);
- mAllAppsSpecs = new AllAppsSpecs(new ResourceHelper(context, inv.allAppsSpecsId));
- mAllAppsResponsiveWidthSpec = mAllAppsSpecs.getCalculatedWidthSpec(inv.numColumns,
+ AllAppsSpecs allAppsSpecs = AllAppsSpecs.create(
+ new ResourceHelper(context, inv.allAppsSpecsId));
+ mAllAppsResponsiveWidthSpec = allAppsSpecs.getCalculatedWidthSpec(inv.numColumns,
mResponsiveWidthSpec.getAvailableSpace(), mResponsiveWidthSpec);
- mAllAppsResponsiveHeightSpec = mAllAppsSpecs.getCalculatedHeightSpec(inv.numRows,
- mResponsiveHeightSpec.getAvailableSpace(),
- mResponsiveHeightSpec);
+ mAllAppsResponsiveHeightSpec = allAppsSpecs.getCalculatedHeightSpec(inv.numRows,
+ mResponsiveHeightSpec.getAvailableSpace(), mResponsiveHeightSpec);
- mFolderSpecs = new FolderSpecs(new ResourceHelper(context, inv.folderSpecsId));
- mResponsiveFolderWidthSpec = mFolderSpecs.getWidthSpec(inv.numFolderColumns,
+ FolderSpecs folderSpecs = FolderSpecs.create(
+ new ResourceHelper(context, inv.folderSpecsId));
+ mResponsiveFolderWidthSpec = folderSpecs.getCalculatedWidthSpec(inv.numFolderColumns,
mResponsiveWidthSpec.getAvailableSpace(), mResponsiveWidthSpec);
- mResponsiveFolderHeightSpec = mFolderSpecs.getHeightSpec(inv.numFolderRows,
+ mResponsiveFolderHeightSpec = folderSpecs.getCalculatedHeightSpec(inv.numFolderRows,
mResponsiveHeightSpec.getAvailableSpace(), mResponsiveHeightSpec);
}
diff --git a/src/com/android/launcher3/responsive/AllAppsSpecs.kt b/src/com/android/launcher3/responsive/AllAppsSpecs.kt
index 85e383e..8ed3ffc 100644
--- a/src/com/android/launcher3/responsive/AllAppsSpecs.kt
+++ b/src/com/android/launcher3/responsive/AllAppsSpecs.kt
@@ -16,277 +16,89 @@
package com.android.launcher3.responsive
-import android.content.res.XmlResourceParser
-import android.util.AttributeSet
-import android.util.Log
-import android.util.Xml
+import android.content.res.TypedArray
import com.android.launcher3.R
+import com.android.launcher3.responsive.ResponsiveSpec.SpecType
import com.android.launcher3.util.ResourceHelper
-import com.android.launcher3.workspace.CalculatedWorkspaceSpec
-import java.io.IOException
-import kotlin.math.roundToInt
-import org.xmlpull.v1.XmlPullParser
-import org.xmlpull.v1.XmlPullParserException
-private const val LOG_TAG = "AllAppsSpecs"
+class AllAppsSpecs(widthSpecs: List<AllAppsSpec>, heightSpecs: List<AllAppsSpec>) :
+ ResponsiveSpecs<AllAppsSpec>(widthSpecs, heightSpecs) {
-class AllAppsSpecs(resourceHelper: ResourceHelper) {
- object XmlTags {
- const val ALL_APPS_SPECS = "allAppsSpecs"
-
- const val ALL_APPS_SPEC = "allAppsSpec"
- const val START_PADDING = "startPadding"
- const val END_PADDING = "endPadding"
- const val GUTTER = "gutter"
- const val CELL_SIZE = "cellSize"
- }
-
- val allAppsHeightSpecList = mutableListOf<AllAppsSpec>()
- val allAppsWidthSpecList = mutableListOf<AllAppsSpec>()
-
- // TODO(b/286538013) Remove this init after a more generic or reusable parser is created
- init {
- var parser: XmlResourceParser? = null
- try {
- parser = resourceHelper.getXml()
- val depth = parser.depth
- var type: Int
- while (
- (parser.next().also { type = it } != XmlPullParser.END_TAG ||
- parser.depth > depth) && type != XmlPullParser.END_DOCUMENT
- ) {
- if (type == XmlPullParser.START_TAG && XmlTags.ALL_APPS_SPECS == parser.name) {
- val displayDepth = parser.depth
- while (
- (parser.next().also { type = it } != XmlPullParser.END_TAG ||
- parser.depth > displayDepth) && type != XmlPullParser.END_DOCUMENT
- ) {
- if (
- type == XmlPullParser.START_TAG && XmlTags.ALL_APPS_SPEC == parser.name
- ) {
- val attrs =
- resourceHelper.obtainStyledAttributes(
- Xml.asAttributeSet(parser),
- R.styleable.AllAppsSpec
- )
- val maxAvailableSize =
- attrs.getDimensionPixelSize(
- R.styleable.AllAppsSpec_maxAvailableSize,
- 0
- )
- val specType =
- AllAppsSpec.SpecType.values()[
- attrs.getInt(
- R.styleable.AllAppsSpec_specType,
- AllAppsSpec.SpecType.HEIGHT.ordinal
- )]
- attrs.recycle()
-
- var startPadding: SizeSpec? = null
- var endPadding: SizeSpec? = null
- var gutter: SizeSpec? = null
- var cellSize: SizeSpec? = null
-
- val limitDepth = parser.depth
- while (
- (parser.next().also { type = it } != XmlPullParser.END_TAG ||
- parser.depth > limitDepth) && type != XmlPullParser.END_DOCUMENT
- ) {
- val attr: AttributeSet = Xml.asAttributeSet(parser)
- if (type == XmlPullParser.START_TAG) {
- when (parser.name) {
- XmlTags.START_PADDING -> {
- startPadding = SizeSpec.create(resourceHelper, attr)
- }
- XmlTags.END_PADDING -> {
- endPadding = SizeSpec.create(resourceHelper, attr)
- }
- XmlTags.GUTTER -> {
- gutter = SizeSpec.create(resourceHelper, attr)
- }
- XmlTags.CELL_SIZE -> {
- cellSize = SizeSpec.create(resourceHelper, attr)
- }
- }
- }
- }
-
- if (
- startPadding == null ||
- endPadding == null ||
- gutter == null ||
- cellSize == null
- ) {
- throw IllegalStateException(
- "All attributes in AllAppsSpec must be defined"
- )
- }
-
- val allAppsSpec =
- AllAppsSpec(
- maxAvailableSize,
- specType,
- startPadding,
- endPadding,
- gutter,
- cellSize
- )
- if (allAppsSpec.isValid()) {
- if (allAppsSpec.specType == AllAppsSpec.SpecType.HEIGHT)
- allAppsHeightSpecList.add(allAppsSpec)
- else allAppsWidthSpecList.add(allAppsSpec)
- } else {
- throw IllegalStateException("Invalid AllAppsSpec found.")
- }
- }
- }
-
- if (allAppsWidthSpecList.isEmpty() || allAppsHeightSpecList.isEmpty()) {
- throw IllegalStateException(
- "AllAppsSpecs is incomplete - " +
- "height list size = ${allAppsHeightSpecList.size}; " +
- "width list size = ${allAppsWidthSpecList.size}."
- )
- }
- }
- }
- } catch (e: Exception) {
- when (e) {
- is IOException,
- is XmlPullParserException -> {
- throw RuntimeException("Failure parsing all apps specs file.", e)
- }
- else -> throw e
- }
- } finally {
- parser?.close()
- }
- }
-
- /**
- * Returns the CalculatedAllAppsSpec for width, based on the available width, the AllAppsSpecs
- * and the CalculatedWorkspaceSpec.
- */
fun getCalculatedWidthSpec(
columns: Int,
availableWidth: Int,
calculatedWorkspaceSpec: CalculatedWorkspaceSpec
): CalculatedAllAppsSpec {
- val widthSpec = allAppsWidthSpecList.first { availableWidth <= it.maxAvailableSize }
+ check(calculatedWorkspaceSpec.spec.specType == SpecType.WIDTH) {
+ "Invalid specType for CalculatedWorkspaceSpec. " +
+ "Expected: ${SpecType.WIDTH} - " +
+ "Found: ${calculatedWorkspaceSpec.spec.specType}}"
+ }
- return CalculatedAllAppsSpec(availableWidth, columns, widthSpec, calculatedWorkspaceSpec)
+ val spec = getWidthSpec(availableWidth)
+ return CalculatedAllAppsSpec(availableWidth, columns, spec, calculatedWorkspaceSpec)
}
- /**
- * Returns the CalculatedAllAppsSpec for height, based on the available height, the AllAppsSpecs
- * and the CalculatedWorkspaceSpec.
- */
fun getCalculatedHeightSpec(
rows: Int,
availableHeight: Int,
calculatedWorkspaceSpec: CalculatedWorkspaceSpec
): CalculatedAllAppsSpec {
- val heightSpec = allAppsHeightSpecList.first { availableHeight <= it.maxAvailableSize }
+ check(calculatedWorkspaceSpec.spec.specType == SpecType.HEIGHT) {
+ "Invalid specType for CalculatedWorkspaceSpec. " +
+ "Expected: ${SpecType.HEIGHT} - " +
+ "Found: ${calculatedWorkspaceSpec.spec.specType}}"
+ }
- return CalculatedAllAppsSpec(availableHeight, rows, heightSpec, calculatedWorkspaceSpec)
- }
-}
-
-class CalculatedAllAppsSpec(
- val availableSpace: Int,
- val cells: Int,
- private val allAppsSpec: AllAppsSpec,
- calculatedWorkspaceSpec: CalculatedWorkspaceSpec
-) {
- var startPaddingPx: Int = 0
- private set
- var endPaddingPx: Int = 0
- private set
- var gutterPx: Int = 0
- private set
- var cellSizePx: Int = 0
- private set
- init {
- // Copy values from workspace
- if (allAppsSpec.startPadding.matchWorkspace)
- startPaddingPx = calculatedWorkspaceSpec.startPaddingPx
- if (allAppsSpec.endPadding.matchWorkspace)
- endPaddingPx = calculatedWorkspaceSpec.endPaddingPx
- if (allAppsSpec.gutter.matchWorkspace) gutterPx = calculatedWorkspaceSpec.gutterPx
- if (allAppsSpec.cellSize.matchWorkspace) cellSizePx = calculatedWorkspaceSpec.cellSizePx
-
- // Calculate all fixed size first
- if (allAppsSpec.startPadding.fixedSize > 0)
- startPaddingPx = allAppsSpec.startPadding.fixedSize.roundToInt()
- if (allAppsSpec.endPadding.fixedSize > 0)
- endPaddingPx = allAppsSpec.endPadding.fixedSize.roundToInt()
- if (allAppsSpec.gutter.fixedSize > 0) gutterPx = allAppsSpec.gutter.fixedSize.roundToInt()
- if (allAppsSpec.cellSize.fixedSize > 0)
- cellSizePx = allAppsSpec.cellSize.fixedSize.roundToInt()
-
- // Calculate all available space next
- if (allAppsSpec.startPadding.ofAvailableSpace > 0)
- startPaddingPx =
- (allAppsSpec.startPadding.ofAvailableSpace * availableSpace).roundToInt()
- if (allAppsSpec.endPadding.ofAvailableSpace > 0)
- endPaddingPx = (allAppsSpec.endPadding.ofAvailableSpace * availableSpace).roundToInt()
- if (allAppsSpec.gutter.ofAvailableSpace > 0)
- gutterPx = (allAppsSpec.gutter.ofAvailableSpace * availableSpace).roundToInt()
- if (allAppsSpec.cellSize.ofAvailableSpace > 0)
- cellSizePx = (allAppsSpec.cellSize.ofAvailableSpace * availableSpace).roundToInt()
-
- // Calculate remainder space last
- val gutters = cells - 1
- val usedSpace = startPaddingPx + endPaddingPx + (gutterPx * gutters) + (cellSizePx * cells)
- val remainderSpace = availableSpace - usedSpace
- if (allAppsSpec.startPadding.ofRemainderSpace > 0)
- startPaddingPx =
- (allAppsSpec.startPadding.ofRemainderSpace * remainderSpace).roundToInt()
- if (allAppsSpec.endPadding.ofRemainderSpace > 0)
- endPaddingPx = (allAppsSpec.endPadding.ofRemainderSpace * remainderSpace).roundToInt()
- if (allAppsSpec.gutter.ofRemainderSpace > 0)
- gutterPx = (allAppsSpec.gutter.ofRemainderSpace * remainderSpace).roundToInt()
- if (allAppsSpec.cellSize.ofRemainderSpace > 0)
- cellSizePx = (allAppsSpec.cellSize.ofRemainderSpace * remainderSpace).roundToInt()
+ val spec = getHeightSpec(availableHeight)
+ return CalculatedAllAppsSpec(availableHeight, rows, spec, calculatedWorkspaceSpec)
}
- override fun toString(): String {
- return "CalculatedAllAppsSpec(availableSpace=$availableSpace, " +
- "cells=$cells, startPaddingPx=$startPaddingPx, endPaddingPx=$endPaddingPx, " +
- "gutterPx=$gutterPx, cellSizePx=$cellSizePx, " +
- "AllAppsSpec.maxAvailableSize=${allAppsSpec.maxAvailableSize})"
+ companion object {
+ private const val XML_ALL_APPS_SPEC = "allAppsSpec"
+
+ @JvmStatic
+ fun create(resourceHelper: ResourceHelper): AllAppsSpecs {
+ val parser = ResponsiveSpecsParser(resourceHelper)
+ val specs = parser.parseXML(XML_ALL_APPS_SPEC, ::AllAppsSpec)
+ val (widthSpecs, heightSpecs) = specs.partition { it.specType == SpecType.WIDTH }
+ return AllAppsSpecs(widthSpecs, heightSpecs)
+ }
}
}
data class AllAppsSpec(
- val maxAvailableSize: Int,
- val specType: SpecType,
- val startPadding: SizeSpec,
- val endPadding: SizeSpec,
- val gutter: SizeSpec,
- val cellSize: SizeSpec
-) {
+ override val maxAvailableSize: Int,
+ override val specType: SpecType,
+ override val startPadding: SizeSpec,
+ override val endPadding: SizeSpec,
+ override val gutter: SizeSpec,
+ override val cellSize: SizeSpec
+) : ResponsiveSpec(maxAvailableSize, specType, startPadding, endPadding, gutter, cellSize) {
- enum class SpecType {
- HEIGHT,
- WIDTH
+ init {
+ check(isValid()) { "Invalid AllAppsSpec found." }
}
- fun isValid(): Boolean {
- if (maxAvailableSize <= 0) {
- Log.e(LOG_TAG, "AllAppsSpec#isValid - maxAvailableSize <= 0")
- return false
- }
-
- // All specs need to be individually valid
- if (!allSpecsAreValid()) {
- Log.e(LOG_TAG, "AllAppsSpec#isValid - !allSpecsAreValid()")
- return false
- }
-
- return true
- }
-
- private fun allSpecsAreValid(): Boolean =
- startPadding.isValid() && endPadding.isValid() && gutter.isValid() && cellSize.isValid()
+ constructor(
+ attrs: TypedArray,
+ specs: Map<String, SizeSpec>
+ ) : this(
+ maxAvailableSize =
+ attrs.getDimensionPixelSize(R.styleable.ResponsiveSpec_maxAvailableSize, 0),
+ specType =
+ SpecType.values()[
+ attrs.getInt(R.styleable.ResponsiveSpec_specType, SpecType.HEIGHT.ordinal)],
+ startPadding = specs.getOrError(SizeSpec.XmlTags.START_PADDING),
+ endPadding = specs.getOrError(SizeSpec.XmlTags.END_PADDING),
+ gutter = specs.getOrError(SizeSpec.XmlTags.GUTTER),
+ cellSize = specs.getOrError(SizeSpec.XmlTags.CELL_SIZE)
+ )
}
+
+class CalculatedAllAppsSpec(
+ availableSpace: Int,
+ cells: Int,
+ spec: AllAppsSpec,
+ calculatedWorkspaceSpec: CalculatedWorkspaceSpec
+) : CalculatedResponsiveSpec(availableSpace, cells, spec, calculatedWorkspaceSpec)
diff --git a/src/com/android/launcher3/responsive/FolderSpecs.kt b/src/com/android/launcher3/responsive/FolderSpecs.kt
index f4446bc..bc2db28 100644
--- a/src/com/android/launcher3/responsive/FolderSpecs.kt
+++ b/src/com/android/launcher3/responsive/FolderSpecs.kt
@@ -1,280 +1,105 @@
+/*
+ * 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.XmlResourceParser
-import android.util.AttributeSet
-import android.util.Log
-import android.util.Xml
+import android.content.res.TypedArray
import com.android.launcher3.R
-import com.android.launcher3.responsive.FolderSpec.*
+import com.android.launcher3.responsive.ResponsiveSpec.SpecType
import com.android.launcher3.util.ResourceHelper
-import com.android.launcher3.workspace.CalculatedWorkspaceSpec
-import com.android.launcher3.workspace.WorkspaceSpec
-import java.io.IOException
-import org.xmlpull.v1.XmlPullParser
-import org.xmlpull.v1.XmlPullParserException
-private const val LOG_TAG = "FolderSpecs"
+class FolderSpecs(widthSpecs: List<FolderSpec>, heightSpecs: List<FolderSpec>) :
+ ResponsiveSpecs<FolderSpec>(widthSpecs, heightSpecs) {
-class FolderSpecs(resourceHelper: ResourceHelper) {
-
- object XmlTags {
- const val FOLDER_SPECS = "folderSpecs"
-
- const val FOLDER_SPEC = "folderSpec"
- const val START_PADDING = "startPadding"
- const val END_PADDING = "endPadding"
- const val GUTTER = "gutter"
- const val CELL_SIZE = "cellSize"
- }
-
- private val _heightSpecs = mutableListOf<FolderSpec>()
- val heightSpecs: List<FolderSpec>
- get() = _heightSpecs
-
- private val _widthSpecs = mutableListOf<FolderSpec>()
- val widthSpecs: List<FolderSpec>
- get() = _widthSpecs
-
- // TODO(b/286538013) Remove this init after a more generic or reusable parser is created
- init {
- var parser: XmlResourceParser? = null
- try {
- parser = resourceHelper.getXml()
- val depth = parser.depth
- var type: Int
- while (
- (parser.next().also { type = it } != XmlPullParser.END_TAG ||
- parser.depth > depth) && type != XmlPullParser.END_DOCUMENT
- ) {
- if (type == XmlPullParser.START_TAG && XmlTags.FOLDER_SPECS == parser.name) {
- val displayDepth = parser.depth
- while (
- (parser.next().also { type = it } != XmlPullParser.END_TAG ||
- parser.depth > displayDepth) && type != XmlPullParser.END_DOCUMENT
- ) {
- if (type == XmlPullParser.START_TAG && XmlTags.FOLDER_SPEC == parser.name) {
- val attrs =
- resourceHelper.obtainStyledAttributes(
- Xml.asAttributeSet(parser),
- R.styleable.FolderSpec
- )
- val maxAvailableSize =
- attrs.getDimensionPixelSize(
- R.styleable.FolderSpec_maxAvailableSize,
- 0
- )
- val specType =
- SpecType.values()[
- attrs.getInt(
- R.styleable.FolderSpec_specType,
- SpecType.HEIGHT.ordinal
- )]
- attrs.recycle()
-
- var startPadding: SizeSpec? = null
- var endPadding: SizeSpec? = null
- var gutter: SizeSpec? = null
- var cellSize: SizeSpec? = null
-
- val limitDepth = parser.depth
- while (
- (parser.next().also { type = it } != XmlPullParser.END_TAG ||
- parser.depth > limitDepth) && type != XmlPullParser.END_DOCUMENT
- ) {
- val attr: AttributeSet = Xml.asAttributeSet(parser)
- if (type == XmlPullParser.START_TAG) {
- val sizeSpec = SizeSpec.create(resourceHelper, attr)
- when (parser.name) {
- XmlTags.START_PADDING -> startPadding = sizeSpec
- XmlTags.END_PADDING -> endPadding = sizeSpec
- XmlTags.GUTTER -> gutter = sizeSpec
- XmlTags.CELL_SIZE -> cellSize = sizeSpec
- }
- }
- }
-
- checkNotNull(startPadding) {
- "Attr 'startPadding' in FolderSpec must be defined."
- }
- checkNotNull(endPadding) {
- "Attr 'endPadding' in FolderSpec must be defined."
- }
- checkNotNull(gutter) { "Attr 'gutter' in FolderSpec must be defined." }
- checkNotNull(cellSize) {
- "Attr 'cellSize' in FolderSpec must be defined."
- }
-
- val folderSpec =
- FolderSpec(
- maxAvailableSize,
- specType,
- startPadding,
- endPadding,
- gutter,
- cellSize
- )
-
- check(folderSpec.isValid()) { "Invalid FolderSpec found." }
-
- if (folderSpec.specType == SpecType.HEIGHT) {
- _heightSpecs += folderSpec
- } else {
- _widthSpecs += folderSpec
- }
- }
- }
-
- check(_widthSpecs.isNotEmpty() && _heightSpecs.isNotEmpty()) {
- "FolderSpecs is incomplete - " +
- "height list size = ${_heightSpecs.size}; " +
- "width list size = ${_widthSpecs.size}."
- }
- }
- }
- } catch (e: Exception) {
- when (e) {
- is IOException,
- is XmlPullParserException -> {
- throw RuntimeException("Failure parsing folder specs file.", e)
- }
- else -> throw e
- }
- } finally {
- parser?.close()
- }
- }
-
- /**
- * Returns the [CalculatedFolderSpec] for width, based on the available width, FolderSpecs and
- * WorkspaceSpecs.
- */
- fun getWidthSpec(
+ fun getCalculatedWidthSpec(
columns: Int,
availableWidth: Int,
- workspaceSpec: CalculatedWorkspaceSpec
+ calculatedWorkspaceSpec: CalculatedWorkspaceSpec
): CalculatedFolderSpec {
- check(workspaceSpec.workspaceSpec.specType == WorkspaceSpec.SpecType.WIDTH) {
+ check(calculatedWorkspaceSpec.spec.specType == SpecType.WIDTH) {
"Invalid specType for CalculatedWorkspaceSpec. " +
- "Expected: ${WorkspaceSpec.SpecType.WIDTH} - " +
- "Found: ${workspaceSpec.workspaceSpec.specType}}"
+ "Expected: ${SpecType.WIDTH} - " +
+ "Found: ${calculatedWorkspaceSpec.spec.specType}}"
}
- val widthSpec = _widthSpecs.firstOrNull { availableWidth <= it.maxAvailableSize }
- check(widthSpec != null) { "No FolderSpec for width spec found with $availableWidth." }
-
- return convertToCalculatedFolderSpec(widthSpec, availableWidth, columns, workspaceSpec)
+ val spec = getWidthSpec(availableWidth)
+ return CalculatedFolderSpec(availableWidth, columns, spec, calculatedWorkspaceSpec)
}
- /**
- * Returns the [CalculatedFolderSpec] for height, based on the available height, FolderSpecs and
- * WorkspaceSpecs.
- */
- fun getHeightSpec(
+ fun getCalculatedHeightSpec(
rows: Int,
availableHeight: Int,
- workspaceSpec: CalculatedWorkspaceSpec
+ calculatedWorkspaceSpec: CalculatedWorkspaceSpec
): CalculatedFolderSpec {
- check(workspaceSpec.workspaceSpec.specType == WorkspaceSpec.SpecType.HEIGHT) {
+ check(calculatedWorkspaceSpec.spec.specType == SpecType.HEIGHT) {
"Invalid specType for CalculatedWorkspaceSpec. " +
- "Expected: ${WorkspaceSpec.SpecType.HEIGHT} - " +
- "Found: ${workspaceSpec.workspaceSpec.specType}}"
+ "Expected: ${SpecType.HEIGHT} - " +
+ "Found: ${calculatedWorkspaceSpec.spec.specType}}"
}
- val heightSpec = _heightSpecs.firstOrNull { availableHeight <= it.maxAvailableSize }
- check(heightSpec != null) { "No FolderSpec for height spec found with $availableHeight." }
+ val spec = getHeightSpec(availableHeight)
+ return CalculatedFolderSpec(availableHeight, rows, spec, calculatedWorkspaceSpec)
+ }
- return convertToCalculatedFolderSpec(heightSpec, availableHeight, rows, workspaceSpec)
+ companion object {
+
+ private const val XML_FOLDER_SPEC = "folderSpec"
+
+ @JvmStatic
+ fun create(resourceHelper: ResourceHelper): FolderSpecs {
+ val parser = ResponsiveSpecsParser(resourceHelper)
+ val specs = parser.parseXML(XML_FOLDER_SPEC, ::FolderSpec)
+ val (widthSpecs, heightSpecs) = specs.partition { it.specType == SpecType.WIDTH }
+ return FolderSpecs(widthSpecs, heightSpecs)
+ }
}
}
-data class CalculatedFolderSpec(
- val availableSpace: Int,
- val cells: Int,
- val startPaddingPx: Int,
- val endPaddingPx: Int,
- val gutterPx: Int,
- val cellSizePx: Int
-)
-
-/**
- * Responsive folder specs to be used to calculate the paddings, gutter and cell size for folders in
- * the workspace.
- *
- * @param maxAvailableSize indicates the breakpoint to use this specification.
- * @param specType indicates whether the paddings and gutters will be applied vertically or
- * horizontally.
- * @param startPadding padding used at the top or left (right in RTL) in the workspace folder.
- * @param endPadding padding used at the bottom or right (left in RTL) in the workspace folder.
- * @param gutter the space between the cells vertically or horizontally depending on the [specType].
- * @param cellSize height or width of the cell depending on the [specType].
- */
data class FolderSpec(
- val maxAvailableSize: Int,
- val specType: SpecType,
- val startPadding: SizeSpec,
- val endPadding: SizeSpec,
- val gutter: SizeSpec,
- val cellSize: SizeSpec
-) {
+ override val maxAvailableSize: Int,
+ override val specType: SpecType,
+ override val startPadding: SizeSpec,
+ override val endPadding: SizeSpec,
+ override val gutter: SizeSpec,
+ override val cellSize: SizeSpec
+) : ResponsiveSpec(maxAvailableSize, specType, startPadding, endPadding, gutter, cellSize) {
- enum class SpecType {
- HEIGHT,
- WIDTH
+ init {
+ check(isValid()) { "Invalid FolderSpec found." }
}
- fun isValid(): Boolean {
- if (maxAvailableSize <= 0) {
- Log.e(LOG_TAG, "FolderSpec#isValid - maxAvailableSize <= 0")
- return false
- }
-
- // All specs are valid
- if (
- !(startPadding.isValid() &&
- endPadding.isValid() &&
- gutter.isValid() &&
- cellSize.isValid())
- ) {
- Log.e(LOG_TAG, "FolderSpec#isValid - !allSpecsAreValid()")
- return false
- }
-
- return true
- }
-}
-
-/** Helper function to convert [FolderSpec] to [CalculatedFolderSpec] */
-private fun convertToCalculatedFolderSpec(
- folderSpec: FolderSpec,
- availableSpace: Int,
- cells: Int,
- workspaceSpec: CalculatedWorkspaceSpec
-): CalculatedFolderSpec {
- // Map if is fixedSize, ofAvailableSpace or matchWorkspace
- var startPaddingPx =
- folderSpec.startPadding.getCalculatedValue(availableSpace, workspaceSpec.startPaddingPx)
- var endPaddingPx =
- folderSpec.endPadding.getCalculatedValue(availableSpace, workspaceSpec.endPaddingPx)
- var gutterPx = folderSpec.gutter.getCalculatedValue(availableSpace, workspaceSpec.gutterPx)
- var cellSizePx =
- folderSpec.cellSize.getCalculatedValue(availableSpace, workspaceSpec.cellSizePx)
-
- // Remainder space
- val gutters = cells - 1
- val usedSpace = startPaddingPx + endPaddingPx + (gutterPx * gutters) + (cellSizePx * cells)
- val remainderSpace = availableSpace - usedSpace
-
- startPaddingPx = folderSpec.startPadding.getRemainderSpaceValue(remainderSpace, startPaddingPx)
- endPaddingPx = folderSpec.endPadding.getRemainderSpaceValue(remainderSpace, endPaddingPx)
- gutterPx = folderSpec.gutter.getRemainderSpaceValue(remainderSpace, gutterPx)
- cellSizePx = folderSpec.cellSize.getRemainderSpaceValue(remainderSpace, cellSizePx)
-
- return CalculatedFolderSpec(
- availableSpace = availableSpace,
- cells = cells,
- startPaddingPx = startPaddingPx,
- endPaddingPx = endPaddingPx,
- gutterPx = gutterPx,
- cellSizePx = cellSizePx
+ constructor(
+ attrs: TypedArray,
+ specs: Map<String, SizeSpec>
+ ) : this(
+ maxAvailableSize =
+ attrs.getDimensionPixelSize(R.styleable.ResponsiveSpec_maxAvailableSize, 0),
+ specType =
+ SpecType.values()[
+ attrs.getInt(R.styleable.ResponsiveSpec_specType, SpecType.HEIGHT.ordinal)],
+ startPadding = specs.getOrError(SizeSpec.XmlTags.START_PADDING),
+ endPadding = specs.getOrError(SizeSpec.XmlTags.END_PADDING),
+ gutter = specs.getOrError(SizeSpec.XmlTags.GUTTER),
+ cellSize = specs.getOrError(SizeSpec.XmlTags.CELL_SIZE)
)
}
+
+class CalculatedFolderSpec(
+ availableSpace: Int,
+ cells: Int,
+ spec: FolderSpec,
+ calculatedWorkspaceSpec: CalculatedWorkspaceSpec
+) : CalculatedResponsiveSpec(availableSpace, cells, spec, calculatedWorkspaceSpec)
diff --git a/src/com/android/launcher3/responsive/ResponsiveSpecs.kt b/src/com/android/launcher3/responsive/ResponsiveSpecs.kt
new file mode 100644
index 0000000..72a0ea4
--- /dev/null
+++ b/src/com/android/launcher3/responsive/ResponsiveSpecs.kt
@@ -0,0 +1,222 @@
+/*
+ * 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.util.Log
+
+/**
+ * Base class for responsive specs that holds a list of width and height specs.
+ *
+ * @param widthSpecs List of width responsive specifications
+ * @param heightSpecs List of height responsive specifications
+ */
+abstract class ResponsiveSpecs<T : ResponsiveSpec>(
+ val widthSpecs: List<T>,
+ val heightSpecs: List<T>
+) {
+
+ init {
+ check(widthSpecs.isNotEmpty() && heightSpecs.isNotEmpty()) {
+ "${this::class.simpleName} is incomplete - " +
+ "width list size = ${widthSpecs.size}; " +
+ "height list size = ${heightSpecs.size}."
+ }
+ }
+
+ /**
+ * Get a [ResponsiveSpec] for width within the breakpoint.
+ *
+ * @param availableWidth The width breakpoint for the spec
+ * @return A [ResponsiveSpec] for width.
+ */
+ fun getWidthSpec(availableWidth: Int): T {
+ val spec = widthSpecs.firstOrNull { availableWidth <= it.maxAvailableSize }
+ check(spec != null) { "No available width spec found within $availableWidth." }
+ return spec
+ }
+
+ /**
+ * Get a [ResponsiveSpec] for height within the breakpoint.
+ *
+ * @param availableHeight The height breakpoint for the spec
+ * @return A [ResponsiveSpec] for height.
+ */
+ fun getHeightSpec(availableHeight: Int): T {
+ val spec = heightSpecs.firstOrNull { availableHeight <= it.maxAvailableSize }
+ check(spec != null) { "No available height spec found within $availableHeight." }
+ return spec
+ }
+}
+
+/**
+ * Base class for a responsive specification that is used to calculate the paddings, gutter and cell
+ * size.
+ *
+ * @param maxAvailableSize indicates the breakpoint to use this specification.
+ * @param specType indicates whether the paddings and gutters will be applied vertically or
+ * horizontally.
+ * @param startPadding padding used at the top or left (right in RTL) in the workspace folder.
+ * @param endPadding padding used at the bottom or right (left in RTL) in the workspace folder.
+ * @param gutter the space between the cells vertically or horizontally depending on the [specType].
+ * @param cellSize height or width of the cell depending on the [specType].
+ */
+abstract class ResponsiveSpec(
+ open val maxAvailableSize: Int,
+ open val specType: SpecType,
+ open val startPadding: SizeSpec,
+ open val endPadding: SizeSpec,
+ open val gutter: SizeSpec,
+ open val cellSize: SizeSpec
+) {
+ open fun isValid(): Boolean {
+ if (maxAvailableSize <= 0) {
+ Log.e(LOG_TAG, "${this::class.simpleName}#isValid - maxAvailableSize <= 0")
+ return false
+ }
+
+ // All specs need to be individually valid
+ if (!allSpecsAreValid()) {
+ Log.e(LOG_TAG, "${this::class.simpleName}#isValid - !allSpecsAreValid()")
+ return false
+ }
+
+ return true
+ }
+
+ private fun allSpecsAreValid(): Boolean {
+ return startPadding.isValid() &&
+ endPadding.isValid() &&
+ gutter.isValid() &&
+ cellSize.isValid()
+ }
+
+ enum class SpecType {
+ HEIGHT,
+ WIDTH
+ }
+
+ companion object {
+ private const val LOG_TAG = "ResponsiveSpec"
+ }
+}
+
+/**
+ * Calculated responsive specs contains the final paddings, gutter and cell size in pixels after
+ * they are calculated from the available space, cells and workspace specs.
+ */
+sealed class CalculatedResponsiveSpec {
+ var availableSpace: Int = 0
+ private set
+
+ var cells: Int = 0
+ private set
+
+ var startPaddingPx: Int = 0
+ private set
+
+ var endPaddingPx: Int = 0
+ private set
+
+ var gutterPx: Int = 0
+ private set
+
+ var cellSizePx: Int = 0
+ private set
+
+ var spec: ResponsiveSpec
+ private set
+
+ constructor(
+ availableSpace: Int,
+ cells: Int,
+ spec: ResponsiveSpec,
+ calculatedWorkspaceSpec: CalculatedWorkspaceSpec
+ ) {
+ this.availableSpace = availableSpace
+ this.cells = cells
+ this.spec = spec
+
+ // Map if is fixedSize, ofAvailableSpace or matchWorkspace
+ startPaddingPx =
+ spec.startPadding.getCalculatedValue(
+ availableSpace,
+ calculatedWorkspaceSpec.startPaddingPx
+ )
+ endPaddingPx =
+ spec.endPadding.getCalculatedValue(availableSpace, calculatedWorkspaceSpec.endPaddingPx)
+ gutterPx = spec.gutter.getCalculatedValue(availableSpace, calculatedWorkspaceSpec.gutterPx)
+ cellSizePx =
+ spec.cellSize.getCalculatedValue(availableSpace, calculatedWorkspaceSpec.cellSizePx)
+
+ updateRemainderSpaces(availableSpace, cells, spec)
+ }
+
+ constructor(availableSpace: Int, cells: Int, spec: ResponsiveSpec) {
+ this.availableSpace = availableSpace
+ this.cells = cells
+ this.spec = spec
+
+ // Map if is fixedSize or ofAvailableSpace
+ startPaddingPx = spec.startPadding.getCalculatedValue(availableSpace)
+ endPaddingPx = spec.endPadding.getCalculatedValue(availableSpace)
+ gutterPx = spec.gutter.getCalculatedValue(availableSpace)
+ cellSizePx = spec.cellSize.getCalculatedValue(availableSpace)
+
+ updateRemainderSpaces(availableSpace, cells, spec)
+ }
+
+ private fun updateRemainderSpaces(availableSpace: Int, cells: Int, spec: ResponsiveSpec) {
+ val gutters = cells - 1
+ val usedSpace = startPaddingPx + endPaddingPx + (gutterPx * gutters) + (cellSizePx * cells)
+ val remainderSpace = availableSpace - usedSpace
+
+ startPaddingPx = spec.startPadding.getRemainderSpaceValue(remainderSpace, startPaddingPx)
+ endPaddingPx = spec.endPadding.getRemainderSpaceValue(remainderSpace, endPaddingPx)
+ gutterPx = spec.gutter.getRemainderSpaceValue(remainderSpace, gutterPx)
+ cellSizePx = spec.cellSize.getRemainderSpaceValue(remainderSpace, cellSizePx)
+ }
+
+ override fun hashCode(): Int {
+ var result = availableSpace.hashCode()
+ result = 31 * result + cells.hashCode()
+ result = 31 * result + startPaddingPx.hashCode()
+ result = 31 * result + endPaddingPx.hashCode()
+ result = 31 * result + gutterPx.hashCode()
+ result = 31 * result + cellSizePx.hashCode()
+ result = 31 * result + spec.hashCode()
+ return result
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return other is CalculatedResponsiveSpec &&
+ availableSpace == other.availableSpace &&
+ cells == other.cells &&
+ startPaddingPx == other.startPaddingPx &&
+ endPaddingPx == other.endPaddingPx &&
+ gutterPx == other.gutterPx &&
+ cellSizePx == other.cellSizePx &&
+ spec == other.spec
+ }
+
+ override fun toString(): String {
+ return "${this::class.simpleName}(" +
+ "availableSpace=$availableSpace, cells=$cells, startPaddingPx=$startPaddingPx, " +
+ "endPaddingPx=$endPaddingPx, gutterPx=$gutterPx, cellSizePx=$cellSizePx, " +
+ "${spec::class.simpleName}.maxAvailableSize=${spec.maxAvailableSize}" +
+ ")"
+ }
+}
diff --git a/src/com/android/launcher3/responsive/ResponsiveSpecsParser.kt b/src/com/android/launcher3/responsive/ResponsiveSpecsParser.kt
new file mode 100644
index 0000000..a89b619
--- /dev/null
+++ b/src/com/android/launcher3/responsive/ResponsiveSpecsParser.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.content.res.XmlResourceParser
+import android.util.Xml
+import com.android.launcher3.R
+import com.android.launcher3.util.ResourceHelper
+import java.io.IOException
+import org.xmlpull.v1.XmlPullParser
+import org.xmlpull.v1.XmlPullParserException
+
+class ResponsiveSpecsParser(private val resourceHelper: ResourceHelper) {
+
+ private fun parseSizeSpecs(parser: XmlResourceParser): Map<String, SizeSpec> {
+ val parentName = parser.name
+ parser.next()
+
+ val result = mutableMapOf<String, SizeSpec>()
+ while (parser.eventType != XmlPullParser.END_DOCUMENT && parser.name != parentName) {
+ if (parser.eventType == XmlResourceParser.START_TAG) {
+ result[parser.name] = SizeSpec.create(resourceHelper, Xml.asAttributeSet(parser))
+ }
+ parser.next()
+ }
+
+ return result
+ }
+
+ fun <T> parseXML(
+ tagName: String,
+ map: (attributes: TypedArray, sizeSpecs: Map<String, SizeSpec>) -> T
+ ): List<T> {
+ val parser: XmlResourceParser = resourceHelper.getXml()
+
+ try {
+ val list = mutableListOf<T>()
+
+ var eventType = parser.eventType
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ if (eventType == XmlResourceParser.START_TAG && parser.name == tagName) {
+ val attrs =
+ resourceHelper.obtainStyledAttributes(
+ Xml.asAttributeSet(parser),
+ R.styleable.ResponsiveSpec
+ )
+
+ val sizeSpecs = parseSizeSpecs(parser)
+ list += map(attrs, sizeSpecs)
+ attrs.recycle()
+ }
+
+ eventType = parser.next()
+ }
+
+ parser.close()
+
+ return list
+ } catch (e: Exception) {
+ when (e) {
+ is NoSuchFieldException,
+ is IOException,
+ is XmlPullParserException ->
+ throw RuntimeException("Failure parsing specs file.", e)
+ else -> throw e
+ }
+ } finally {
+ parser.close()
+ }
+ }
+}
+
+fun Map<String, SizeSpec>.getOrError(key: String): SizeSpec {
+ return this.getOrElse(key) { error("Attr '$key' must be defined.") }
+}
diff --git a/src/com/android/launcher3/responsive/SizeSpec.kt b/src/com/android/launcher3/responsive/SizeSpec.kt
index 3d618f9..d3868f0 100644
--- a/src/com/android/launcher3/responsive/SizeSpec.kt
+++ b/src/com/android/launcher3/responsive/SizeSpec.kt
@@ -1,3 +1,19 @@
+/*
+ * 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
@@ -26,7 +42,7 @@
) {
/** Retrieves the correct value for [SizeSpec]. */
- fun getCalculatedValue(availableSpace: Int, workspaceValue: Int): Int {
+ fun getCalculatedValue(availableSpace: Int, workspaceValue: Int = 0): Int {
val calculatedValue =
when {
fixedSize > 0 -> fixedSize.roundToInt()
@@ -91,6 +107,13 @@
return true
}
+ object XmlTags {
+ const val START_PADDING = "startPadding"
+ const val END_PADDING = "endPadding"
+ const val GUTTER = "gutter"
+ const val CELL_SIZE = "cellSize"
+ }
+
companion object {
private const val TAG = "SizeSpec"
diff --git a/src/com/android/launcher3/responsive/WorkspaceSpecs.kt b/src/com/android/launcher3/responsive/WorkspaceSpecs.kt
new file mode 100644
index 0000000..0da7026
--- /dev/null
+++ b/src/com/android/launcher3/responsive/WorkspaceSpecs.kt
@@ -0,0 +1,98 @@
+/*
+ * 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.Log
+import com.android.launcher3.R
+import com.android.launcher3.responsive.ResponsiveSpec.SpecType
+import com.android.launcher3.util.ResourceHelper
+
+private const val TAG = "WorkspaceSpecs"
+
+class WorkspaceSpecs(widthSpecs: List<WorkspaceSpec>, heightSpecs: List<WorkspaceSpec>) :
+ ResponsiveSpecs<WorkspaceSpec>(widthSpecs, heightSpecs) {
+
+ fun getCalculatedWidthSpec(columns: Int, availableWidth: Int): CalculatedWorkspaceSpec {
+ val spec = getWidthSpec(availableWidth)
+ return CalculatedWorkspaceSpec(availableWidth, columns, spec)
+ }
+
+ fun getCalculatedHeightSpec(rows: Int, availableHeight: Int): CalculatedWorkspaceSpec {
+ val spec = getHeightSpec(availableHeight)
+ return CalculatedWorkspaceSpec(availableHeight, rows, spec)
+ }
+
+ companion object {
+ private const val XML_WORKSPACE_SPEC = "workspaceSpec"
+
+ @JvmStatic
+ fun create(resourceHelper: ResourceHelper): WorkspaceSpecs {
+ val parser = ResponsiveSpecsParser(resourceHelper)
+ val specs = parser.parseXML(XML_WORKSPACE_SPEC, ::WorkspaceSpec)
+ val (widthSpecs, heightSpecs) = specs.partition { it.specType == SpecType.WIDTH }
+ return WorkspaceSpecs(widthSpecs, heightSpecs)
+ }
+ }
+}
+
+data class WorkspaceSpec(
+ override val maxAvailableSize: Int,
+ override val specType: SpecType,
+ override val startPadding: SizeSpec,
+ override val endPadding: SizeSpec,
+ override val gutter: SizeSpec,
+ override val cellSize: SizeSpec
+) : ResponsiveSpec(maxAvailableSize, specType, startPadding, endPadding, gutter, cellSize) {
+
+ init {
+ check(isValid()) { "Invalid WorkspaceSpec found." }
+ }
+
+ constructor(
+ attrs: TypedArray,
+ specs: Map<String, SizeSpec>
+ ) : this(
+ maxAvailableSize =
+ attrs.getDimensionPixelSize(R.styleable.ResponsiveSpec_maxAvailableSize, 0),
+ specType =
+ SpecType.values()[
+ attrs.getInt(R.styleable.ResponsiveSpec_specType, SpecType.HEIGHT.ordinal)],
+ startPadding = specs.getOrError(SizeSpec.XmlTags.START_PADDING),
+ endPadding = specs.getOrError(SizeSpec.XmlTags.END_PADDING),
+ gutter = specs.getOrError(SizeSpec.XmlTags.GUTTER),
+ cellSize = specs.getOrError(SizeSpec.XmlTags.CELL_SIZE)
+ )
+
+ override fun isValid(): Boolean {
+ // Workspace spec should not match workspace
+ if (
+ startPadding.matchWorkspace ||
+ endPadding.matchWorkspace ||
+ gutter.matchWorkspace ||
+ cellSize.matchWorkspace
+ ) {
+ Log.e(TAG, "WorkspaceSpec#isValid - workspace shouldn't contain matchWorkspace!")
+ return false
+ }
+
+ return super.isValid()
+ }
+}
+
+class CalculatedWorkspaceSpec(availableSpace: Int, cells: Int, spec: WorkspaceSpec) :
+ CalculatedResponsiveSpec(availableSpace, cells, spec)
diff --git a/src/com/android/launcher3/workspace/WorkspaceSpecs.kt b/src/com/android/launcher3/workspace/WorkspaceSpecs.kt
deleted file mode 100644
index 8cc0c59..0000000
--- a/src/com/android/launcher3/workspace/WorkspaceSpecs.kt
+++ /dev/null
@@ -1,281 +0,0 @@
-/*
- * 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.workspace
-
-import android.content.res.XmlResourceParser
-import android.util.AttributeSet
-import android.util.Log
-import android.util.Xml
-import com.android.launcher3.R
-import com.android.launcher3.responsive.SizeSpec
-import com.android.launcher3.util.ResourceHelper
-import java.io.IOException
-import kotlin.math.roundToInt
-import org.xmlpull.v1.XmlPullParser
-import org.xmlpull.v1.XmlPullParserException
-
-private const val TAG = "WorkspaceSpecs"
-
-class WorkspaceSpecs(resourceHelper: ResourceHelper) {
- object XmlTags {
- const val WORKSPACE_SPECS = "workspaceSpecs"
-
- const val WORKSPACE_SPEC = "workspaceSpec"
- const val START_PADDING = "startPadding"
- const val END_PADDING = "endPadding"
- const val GUTTER = "gutter"
- const val CELL_SIZE = "cellSize"
- }
-
- val workspaceHeightSpecList = mutableListOf<WorkspaceSpec>()
- val workspaceWidthSpecList = mutableListOf<WorkspaceSpec>()
-
- // TODO(b/286538013) Remove this init after a more generic or reusable parser is created
- init {
- try {
- val parser: XmlResourceParser = resourceHelper.getXml()
- val depth = parser.depth
- var type: Int
- while (
- (parser.next().also { type = it } != XmlPullParser.END_TAG ||
- parser.depth > depth) && type != XmlPullParser.END_DOCUMENT
- ) {
- if (type == XmlPullParser.START_TAG && XmlTags.WORKSPACE_SPECS == parser.name) {
- val displayDepth = parser.depth
- while (
- (parser.next().also { type = it } != XmlPullParser.END_TAG ||
- parser.depth > displayDepth) && type != XmlPullParser.END_DOCUMENT
- ) {
- if (
- type == XmlPullParser.START_TAG && XmlTags.WORKSPACE_SPEC == parser.name
- ) {
- val attrs =
- resourceHelper.obtainStyledAttributes(
- Xml.asAttributeSet(parser),
- R.styleable.WorkspaceSpec
- )
- val maxAvailableSize =
- attrs.getDimensionPixelSize(
- R.styleable.WorkspaceSpec_maxAvailableSize,
- 0
- )
- val specType =
- WorkspaceSpec.SpecType.values()[
- attrs.getInt(
- R.styleable.WorkspaceSpec_specType,
- WorkspaceSpec.SpecType.HEIGHT.ordinal
- )]
- attrs.recycle()
-
- var startPadding: SizeSpec? = null
- var endPadding: SizeSpec? = null
- var gutter: SizeSpec? = null
- var cellSize: SizeSpec? = null
-
- val limitDepth = parser.depth
- while (
- (parser.next().also { type = it } != XmlPullParser.END_TAG ||
- parser.depth > limitDepth) && type != XmlPullParser.END_DOCUMENT
- ) {
- val attr: AttributeSet = Xml.asAttributeSet(parser)
- if (type == XmlPullParser.START_TAG) {
- when (parser.name) {
- XmlTags.START_PADDING -> {
- startPadding = SizeSpec.create(resourceHelper, attr)
- }
- XmlTags.END_PADDING -> {
- endPadding = SizeSpec.create(resourceHelper, attr)
- }
- XmlTags.GUTTER -> {
- gutter = SizeSpec.create(resourceHelper, attr)
- }
- XmlTags.CELL_SIZE -> {
- cellSize = SizeSpec.create(resourceHelper, attr)
- }
- }
- }
- }
-
- if (
- startPadding == null ||
- endPadding == null ||
- gutter == null ||
- cellSize == null
- ) {
- throw IllegalStateException(
- "All attributes in workspaceSpec must be defined"
- )
- }
-
- val workspaceSpec =
- WorkspaceSpec(
- maxAvailableSize,
- specType,
- startPadding,
- endPadding,
- gutter,
- cellSize
- )
- if (workspaceSpec.isValid()) {
- if (workspaceSpec.specType == WorkspaceSpec.SpecType.HEIGHT)
- workspaceHeightSpecList.add(workspaceSpec)
- else workspaceWidthSpecList.add(workspaceSpec)
- } else {
- throw IllegalStateException("Invalid workspaceSpec found.")
- }
- }
- }
-
- if (workspaceWidthSpecList.isEmpty() || workspaceHeightSpecList.isEmpty()) {
- throw IllegalStateException(
- "WorkspaceSpecs is incomplete - " +
- "height list size = ${workspaceHeightSpecList.size}; " +
- "width list size = ${workspaceWidthSpecList.size}."
- )
- }
- }
- }
- parser.close()
- } catch (e: Exception) {
- when (e) {
- is IOException,
- is XmlPullParserException -> {
- throw RuntimeException("Failure parsing workspaces specs file.", e)
- }
- else -> throw e
- }
- }
- }
-
- /**
- * Returns the CalculatedWorkspaceSpec for width, based on the available width and the
- * WorkspaceSpecs.
- */
- fun getCalculatedWidthSpec(columns: Int, availableWidth: Int): CalculatedWorkspaceSpec {
- val widthSpec = workspaceWidthSpecList.first { availableWidth <= it.maxAvailableSize }
-
- return CalculatedWorkspaceSpec(availableWidth, columns, widthSpec)
- }
-
- /**
- * Returns the CalculatedWorkspaceSpec for height, based on the available height and the
- * WorkspaceSpecs.
- */
- fun getCalculatedHeightSpec(rows: Int, availableHeight: Int): CalculatedWorkspaceSpec {
- val heightSpec = workspaceHeightSpecList.first { availableHeight <= it.maxAvailableSize }
-
- return CalculatedWorkspaceSpec(availableHeight, rows, heightSpec)
- }
-}
-
-class CalculatedWorkspaceSpec(
- val availableSpace: Int,
- val cells: Int,
- val workspaceSpec: WorkspaceSpec
-) {
- var startPaddingPx: Int = 0
- private set
- var endPaddingPx: Int = 0
- private set
- var gutterPx: Int = 0
- private set
- var cellSizePx: Int = 0
- private set
- init {
- // Calculate all fixed size first
- if (workspaceSpec.startPadding.fixedSize > 0)
- startPaddingPx = workspaceSpec.startPadding.fixedSize.roundToInt()
- if (workspaceSpec.endPadding.fixedSize > 0)
- endPaddingPx = workspaceSpec.endPadding.fixedSize.roundToInt()
- if (workspaceSpec.gutter.fixedSize > 0)
- gutterPx = workspaceSpec.gutter.fixedSize.roundToInt()
- if (workspaceSpec.cellSize.fixedSize > 0)
- cellSizePx = workspaceSpec.cellSize.fixedSize.roundToInt()
-
- // Calculate all available space next
- if (workspaceSpec.startPadding.ofAvailableSpace > 0)
- startPaddingPx =
- (workspaceSpec.startPadding.ofAvailableSpace * availableSpace).roundToInt()
- if (workspaceSpec.endPadding.ofAvailableSpace > 0)
- endPaddingPx = (workspaceSpec.endPadding.ofAvailableSpace * availableSpace).roundToInt()
- if (workspaceSpec.gutter.ofAvailableSpace > 0)
- gutterPx = (workspaceSpec.gutter.ofAvailableSpace * availableSpace).roundToInt()
- if (workspaceSpec.cellSize.ofAvailableSpace > 0)
- cellSizePx = (workspaceSpec.cellSize.ofAvailableSpace * availableSpace).roundToInt()
-
- // Calculate remainder space last
- val gutters = cells - 1
- val usedSpace = startPaddingPx + endPaddingPx + (gutterPx * gutters) + (cellSizePx * cells)
- val remainderSpace = availableSpace - usedSpace
- if (workspaceSpec.startPadding.ofRemainderSpace > 0)
- startPaddingPx =
- (workspaceSpec.startPadding.ofRemainderSpace * remainderSpace).roundToInt()
- if (workspaceSpec.endPadding.ofRemainderSpace > 0)
- endPaddingPx = (workspaceSpec.endPadding.ofRemainderSpace * remainderSpace).roundToInt()
- if (workspaceSpec.gutter.ofRemainderSpace > 0)
- gutterPx = (workspaceSpec.gutter.ofRemainderSpace * remainderSpace).roundToInt()
- if (workspaceSpec.cellSize.ofRemainderSpace > 0)
- cellSizePx = (workspaceSpec.cellSize.ofRemainderSpace * remainderSpace).roundToInt()
- }
-
- override fun toString(): String {
- return "CalculatedWorkspaceSpec(availableSpace=$availableSpace, " +
- "cells=$cells, startPaddingPx=$startPaddingPx, endPaddingPx=$endPaddingPx, " +
- "gutterPx=$gutterPx, cellSizePx=$cellSizePx, " +
- "workspaceSpec.maxAvailableSize=${workspaceSpec.maxAvailableSize})"
- }
-}
-
-data class WorkspaceSpec(
- val maxAvailableSize: Int,
- val specType: SpecType,
- val startPadding: SizeSpec,
- val endPadding: SizeSpec,
- val gutter: SizeSpec,
- val cellSize: SizeSpec
-) {
-
- enum class SpecType {
- HEIGHT,
- WIDTH
- }
-
- fun isValid(): Boolean {
- if (maxAvailableSize <= 0) {
- Log.e(TAG, "WorkspaceSpec#isValid - maxAvailableSize <= 0")
- return false
- }
-
- // All specs need to be individually valid
- if (!allSpecsAreValid()) {
- Log.e(TAG, "WorkspaceSpec#isValid - !allSpecsAreValid()")
- return false
- }
-
- return true
- }
-
- private fun allSpecsAreValid(): Boolean =
- startPadding.isValid() &&
- endPadding.isValid() &&
- gutter.isValid() &&
- cellSize.isValid() &&
- !startPadding.matchWorkspace &&
- !endPadding.matchWorkspace &&
- !gutter.matchWorkspace &&
- !cellSize.matchWorkspace
-}