Merge "Add FolderSpec for responsive grid support" into udc-qpr-dev

This commit is contained in:
Jordan Silva
2023-06-13 15:41:49 +00:00
committed by Android (Google) Code Review
15 changed files with 989 additions and 9 deletions
+5
View File
@@ -259,6 +259,11 @@
<attr name="matchWorkspace" format="boolean" />
</declare-styleable>
<declare-styleable name="FolderSpec">
<attr name="specType" />
<attr name="maxAvailableSize" />
</declare-styleable>
<declare-styleable name="ProfileDisplayOption">
<attr name="name" />
<attr name="minWidthDps" format="float" />
@@ -0,0 +1,280 @@
package com.android.launcher3.responsive
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.FolderSpec.*
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(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(
columns: Int,
availableWidth: Int,
workspaceSpec: CalculatedWorkspaceSpec
): CalculatedFolderSpec {
check(workspaceSpec.workspaceSpec.specType == WorkspaceSpec.SpecType.WIDTH) {
"Invalid specType for CalculatedWorkspaceSpec. " +
"Expected: ${WorkspaceSpec.SpecType.WIDTH} - " +
"Found: ${workspaceSpec.workspaceSpec.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)
}
/**
* Returns the [CalculatedFolderSpec] for height, based on the available height, FolderSpecs and
* WorkspaceSpecs.
*/
fun getHeightSpec(
rows: Int,
availableHeight: Int,
workspaceSpec: CalculatedWorkspaceSpec
): CalculatedFolderSpec {
check(workspaceSpec.workspaceSpec.specType == WorkspaceSpec.SpecType.HEIGHT) {
"Invalid specType for CalculatedWorkspaceSpec. " +
"Expected: ${WorkspaceSpec.SpecType.HEIGHT} - " +
"Found: ${workspaceSpec.workspaceSpec.specType}}"
}
val heightSpec = _heightSpecs.firstOrNull { availableHeight <= it.maxAvailableSize }
check(heightSpec != null) { "No FolderSpec for height spec found with $availableHeight." }
return convertToCalculatedFolderSpec(heightSpec, availableHeight, rows, workspaceSpec)
}
}
data class CalculatedFolderSpec(
val startPaddingPx: Int,
val endPaddingPx: Int,
val gutterPx: Int,
val cellSizePx: Int,
val availableSpace: Int,
val cells: 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
) {
enum class SpecType {
HEIGHT,
WIDTH
}
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(
startPaddingPx = startPaddingPx,
endPaddingPx = endPaddingPx,
gutterPx = gutterPx,
cellSizePx = cellSizePx,
availableSpace = availableSpace,
cells = cells
)
}
@@ -6,14 +6,45 @@ 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.
*/
data class SizeSpec(
val fixedSize: Float,
val ofAvailableSpace: Float,
val ofRemainderSpace: Float,
val matchWorkspace: Boolean
val fixedSize: Float = 0f,
val ofAvailableSpace: Float = 0f,
val ofRemainderSpace: Float = 0f,
val matchWorkspace: Boolean = false
) {
/** Retrieves the correct value for [SizeSpec]. */
fun getCalculatedValue(availableSpace: Int, workspaceValue: Int): Int {
return when {
fixedSize > 0 -> fixedSize.roundToInt()
ofAvailableSpace > 0 -> (ofAvailableSpace * availableSpace).roundToInt()
matchWorkspace -> workspaceValue
else -> 0
}
}
/**
* Calculates the [SizeSpec] value when remainder space value is defined. If no remainderSpace
* is 0, returns a default value.
*/
fun getRemainderSpaceValue(remainderSpace: Int, defaultValue: Int): Int {
return if (ofRemainderSpace > 0) {
(ofRemainderSpace * remainderSpace).roundToInt()
} else {
defaultValue
}
}
fun isValid(): Boolean {
// All attributes are empty
if (fixedSize < 0f && ofAvailableSpace <= 0f && ofRemainderSpace <= 0f && !matchWorkspace) {
@@ -48,7 +79,8 @@ data class SizeSpec(
}
companion object {
private const val TAG = "WorkspaceSpecs::SizeSpec"
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()
@@ -44,6 +44,7 @@ class WorkspaceSpecs(resourceHelper: ResourceHelper) {
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()
+6
View File
@@ -32,4 +32,10 @@
<attr name="ofRemainderSpace" format="float" />
<attr name="matchWorkspace" format="boolean" />
</declare-styleable>
<declare-styleable name="FolderSpec">
<attr name="specType" />
<attr name="maxAvailableSize" />
</declare-styleable>
</resources>
+40
View File
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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.
-->
<!-- Tablet - 6x5 portrait -->
<folderSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
<folderSpec launcher:specType="width" launcher:maxAvailableSize="800dp">
<!-- missing startPadding -->
<endPadding launcher:fixedSize="16dp" />
<gutter launcher:fixedSize="16dp" />
<cellSize launcher:matchWorkspace="true" />
</folderSpec>
<folderSpec launcher:specType="width" launcher:maxAvailableSize="9999dp">
<startPadding launcher:fixedSize="16dp" />
<endPadding launcher:fixedSize="16dp" />
<gutter launcher:fixedSize="16dp" />
<cellSize launcher:fixedSize="102dp" />
</folderSpec>
<!-- Height spec is fixed -->
<folderSpec launcher:specType="height" launcher:maxAvailableSize="9999dp">
<startPadding launcher:fixedSize="24dp" />
<!-- mapped to footer height size -->
<endPadding launcher:fixedSize="64dp" />
<gutter launcher:fixedSize="16dp" />
<cellSize launcher:fixedSize="104dp" />
</folderSpec>
</folderSpecs>
+43
View File
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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.
-->
<!-- Tablet - 6x5 portrait -->
<folderSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
<folderSpec launcher:specType="width" launcher:maxAvailableSize="800dp">
<startPadding launcher:fixedSize="16dp" />
<endPadding launcher:fixedSize="16dp" />
<!-- more than 1 value in one tag -->
<gutter
launcher:ofAvailableSpace="0.0125"
launcher:fixedSize="16dp" />
<cellSize launcher:matchWorkspace="true" />
</folderSpec>
<folderSpec launcher:specType="width" launcher:maxAvailableSize="9999dp">
<startPadding launcher:fixedSize="16dp" />
<endPadding launcher:fixedSize="16dp" />
<gutter launcher:fixedSize="16dp" />
<cellSize launcher:fixedSize="102dp" />
</folderSpec>
<!-- Height spec is fixed -->
<folderSpec launcher:specType="height" launcher:maxAvailableSize="9999dp">
<startPadding launcher:fixedSize="24dp" />
<!-- mapped to footer height size -->
<endPadding launcher:fixedSize="64dp" />
<gutter launcher:fixedSize="16dp" />
<cellSize launcher:fixedSize="104dp" />
</folderSpec>
</folderSpecs>
+41
View File
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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.
-->
<!-- Tablet - 6x5 portrait - More the one value first gutter -->
<folderSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
<folderSpec launcher:specType="width" launcher:maxAvailableSize="800dp">
<startPadding launcher:fixedSize="16dp" />
<endPadding launcher:fixedSize="16dp" />
<gutter launcher:fixedSize="16dp" />
<!-- value bigger than 1 -->
<cellSize launcher:ofRemainderSpace="1.001" />
</folderSpec>
<folderSpec launcher:specType="width" launcher:maxAvailableSize="9999dp">
<startPadding launcher:fixedSize="16dp" />
<endPadding launcher:fixedSize="16dp" />
<gutter launcher:fixedSize="16dp" />
<cellSize launcher:fixedSize="102dp" />
</folderSpec>
<!-- Height spec is fixed -->
<folderSpec launcher:specType="height" launcher:maxAvailableSize="9999dp">
<startPadding launcher:fixedSize="24dp" />
<!-- mapped to footer height size -->
<endPadding launcher:fixedSize="64dp" />
<gutter launcher:fixedSize="16dp" />
<cellSize launcher:fixedSize="104dp" />
</folderSpec>
</folderSpecs>
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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.
-->
<!-- missing height spec -->
<folderSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
<folderSpec launcher:specType="width" launcher:maxAvailableSize="800dp">
<startPadding launcher:fixedSize="16dp" />
<endPadding launcher:fixedSize="16dp" />
<gutter launcher:fixedSize="16dp" />
<cellSize launcher:matchWorkspace="true" />
</folderSpec>
</folderSpecs>
+33
View File
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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.
-->
<!-- missing breakpoints > 800dp -->
<folderSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
<folderSpec launcher:specType="width" launcher:maxAvailableSize="800dp">
<startPadding launcher:fixedSize="16dp" />
<endPadding launcher:fixedSize="16dp" />
<gutter launcher:fixedSize="16dp" />
<cellSize launcher:matchWorkspace="true" />
</folderSpec>
<!-- Height spec is fixed -->
<folderSpec launcher:specType="height" launcher:maxAvailableSize="800dp">
<startPadding launcher:fixedSize="24dp" />
<!-- mapped to footer height size -->
<endPadding launcher:fixedSize="64dp" />
<gutter launcher:fixedSize="16dp" />
<cellSize launcher:fixedSize="104dp" />
</folderSpec>
</folderSpecs>
+38
View File
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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.
-->
<folderSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
<folderSpec launcher:specType="width" launcher:maxAvailableSize="800dp">
<startPadding launcher:fixedSize="16dp" />
<endPadding launcher:fixedSize="16dp" />
<gutter launcher:fixedSize="16dp" />
<cellSize launcher:matchWorkspace="true" />
</folderSpec>
<folderSpec launcher:specType="width" launcher:maxAvailableSize="9999dp">
<startPadding launcher:fixedSize="16dp" />
<endPadding launcher:fixedSize="16dp" />
<gutter launcher:fixedSize="16dp" />
<cellSize launcher:fixedSize="102dp" />
</folderSpec>
<!-- Height spec is fixed -->
<folderSpec launcher:specType="height" launcher:maxAvailableSize="9999dp">
<startPadding launcher:fixedSize="24dp" />
<!-- mapped to footer height size -->
<endPadding launcher:fixedSize="64dp" />
<gutter launcher:fixedSize="16dp" />
<cellSize launcher:matchWorkspace="true" />
</folderSpec>
</folderSpecs>
@@ -0,0 +1,126 @@
/*
* 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.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.AbstractDeviceProfileTest
import com.android.launcher3.testing.shared.ResourceUtils
import com.android.launcher3.tests.R
import com.android.launcher3.util.TestResourceHelper
import com.android.launcher3.workspace.WorkspaceSpecs
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidJUnit4::class)
class CalculatedFolderSpecsTest : AbstractDeviceProfileTest() {
override val runningContext: Context = InstrumentationRegistry.getInstrumentation().context
private val deviceSpec = deviceSpecs["phone"]!!
@Before
fun setup() {
initializeVarsForPhone(deviceSpec)
}
@Test
fun validate_matchWidthWorkspace() {
val columns = 6
// Loading workspace specs
val resourceHelperWorkspace = TestResourceHelper(context!!, R.xml.valid_workspace_file)
val workspaceSpecs = WorkspaceSpecs(resourceHelperWorkspace)
// Loading folders specs
val resourceHelperFolder = TestResourceHelper(context!!, R.xml.valid_folders_specs)
val folderSpecs = FolderSpecs(resourceHelperFolder)
assertThat(folderSpecs.widthSpecs.size).isEqualTo(2)
assertThat(folderSpecs.widthSpecs[0].cellSize.matchWorkspace).isEqualTo(true)
assertThat(folderSpecs.widthSpecs[1].cellSize.matchWorkspace).isEqualTo(false)
// Validate width spec <= 800
var availableWidth = deviceSpec.naturalSize.first
var calculatedWorkspace = workspaceSpecs.getCalculatedWidthSpec(columns, availableWidth)
var calculatedWidthFolderSpec =
folderSpecs.getWidthSpec(columns, availableWidth, calculatedWorkspace)
with(calculatedWidthFolderSpec) {
assertThat(availableSpace).isEqualTo(availableWidth)
assertThat(cells).isEqualTo(columns)
assertThat(startPaddingPx).isEqualTo(16.dpToPx())
assertThat(endPaddingPx).isEqualTo(16.dpToPx())
assertThat(gutterPx).isEqualTo(16.dpToPx())
assertThat(cellSizePx).isEqualTo(calculatedWorkspace.cellSizePx)
}
// Validate width spec > 800
availableWidth = 2000.dpToPx()
calculatedWorkspace = workspaceSpecs.getCalculatedWidthSpec(columns, availableWidth)
calculatedWidthFolderSpec =
folderSpecs.getWidthSpec(columns, availableWidth, calculatedWorkspace)
with(calculatedWidthFolderSpec) {
assertThat(availableSpace).isEqualTo(availableWidth)
assertThat(cells).isEqualTo(columns)
assertThat(startPaddingPx).isEqualTo(16.dpToPx())
assertThat(endPaddingPx).isEqualTo(16.dpToPx())
assertThat(gutterPx).isEqualTo(16.dpToPx())
assertThat(cellSizePx).isEqualTo(102.dpToPx())
}
}
@Test
fun validate_matchHeightWorkspace() {
// Hotseat is roughly 495px on a real device, it doesn't need to be precise on unit tests
val hotseatSize = 495
val statusBarHeight = deviceSpec.statusBarNaturalPx
val availableHeight = deviceSpec.naturalSize.second - statusBarHeight - hotseatSize
val rows = 5
// Loading workspace specs
val resourceHelperWorkspace = TestResourceHelper(context!!, R.xml.valid_workspace_file)
val workspaceSpecs = WorkspaceSpecs(resourceHelperWorkspace)
// Loading folders specs
val resourceHelperFolder = TestResourceHelper(context!!, R.xml.valid_folders_specs)
val folderSpecs = FolderSpecs(resourceHelperFolder)
assertThat(folderSpecs.heightSpecs.size).isEqualTo(1)
assertThat(folderSpecs.heightSpecs[0].cellSize.matchWorkspace).isEqualTo(true)
// Validate height spec
val calculatedWorkspace = workspaceSpecs.getCalculatedHeightSpec(rows, availableHeight)
val calculatedFolderSpec =
folderSpecs.getHeightSpec(rows, availableHeight, calculatedWorkspace)
with(calculatedFolderSpec) {
assertThat(availableSpace).isEqualTo(availableHeight)
assertThat(cells).isEqualTo(rows)
assertThat(startPaddingPx).isEqualTo(24.dpToPx())
assertThat(endPaddingPx).isEqualTo(64.dpToPx())
assertThat(gutterPx).isEqualTo(16.dpToPx())
assertThat(cellSizePx).isEqualTo(calculatedWorkspace.cellSizePx)
}
}
private fun Int.dpToPx(): Int {
return ResourceUtils.pxFromDp(this.toFloat(), context!!.resources.displayMetrics)
}
}
@@ -0,0 +1,269 @@
/*
* 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.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.AbstractDeviceProfileTest
import com.android.launcher3.testing.shared.ResourceUtils
import com.android.launcher3.tests.R
import com.android.launcher3.util.TestResourceHelper
import com.android.launcher3.workspace.CalculatedWorkspaceSpec
import com.android.launcher3.workspace.WorkspaceSpec
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidJUnit4::class)
class FolderSpecsTest : AbstractDeviceProfileTest() {
override val runningContext: Context = InstrumentationRegistry.getInstrumentation().context
@Before
fun setup() {
initializeVarsForPhone(deviceSpecs["tablet"]!!)
}
@Test
fun parseValidFile() {
val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs)
val folderSpecs = FolderSpecs(resourceHelper)
val sizeSpec16 = SizeSpec(16f.dpToPx())
val widthSpecsExpected =
listOf(
FolderSpec(
maxAvailableSize = 800.dpToPx(),
specType = FolderSpec.SpecType.WIDTH,
startPadding = sizeSpec16,
endPadding = sizeSpec16,
gutter = sizeSpec16,
cellSize = SizeSpec(matchWorkspace = true)
),
FolderSpec(
maxAvailableSize = 9999.dpToPx(),
specType = FolderSpec.SpecType.WIDTH,
startPadding = sizeSpec16,
endPadding = sizeSpec16,
gutter = sizeSpec16,
cellSize = SizeSpec(102f.dpToPx())
)
)
val heightSpecsExpected =
FolderSpec(
maxAvailableSize = 9999.dpToPx(),
specType = FolderSpec.SpecType.HEIGHT,
startPadding = SizeSpec(24f.dpToPx()),
endPadding = SizeSpec(64f.dpToPx()),
gutter = sizeSpec16,
cellSize = SizeSpec(matchWorkspace = true)
)
assertThat(folderSpecs.widthSpecs.size).isEqualTo(widthSpecsExpected.size)
assertThat(folderSpecs.widthSpecs[0]).isEqualTo(widthSpecsExpected[0])
assertThat(folderSpecs.widthSpecs[1]).isEqualTo(widthSpecsExpected[1])
assertThat(folderSpecs.heightSpecs.size).isEqualTo(1)
assertThat(folderSpecs.heightSpecs[0]).isEqualTo(heightSpecsExpected)
}
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_missingTag_throwsError() {
val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_1)
FolderSpecs(resourceHelper)
}
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_moreThanOneValuePerTag_throwsError() {
val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_2)
FolderSpecs(resourceHelper)
}
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_valueBiggerThan1_throwsError() {
val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_3)
FolderSpecs(resourceHelper)
}
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_missingSpecs_throwsError() {
val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_4)
FolderSpecs(resourceHelper)
}
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_missingWidthBreakpoint_throwsError() {
val availableSpace = 900.dpToPx()
val cells = 3
val workspaceSpec =
WorkspaceSpec(
maxAvailableSize = availableSpace,
specType = WorkspaceSpec.SpecType.WIDTH,
startPadding = SizeSpec(fixedSize = 10f),
endPadding = SizeSpec(fixedSize = 10f),
gutter = SizeSpec(fixedSize = 10f),
cellSize = SizeSpec(fixedSize = 10f)
)
val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_5)
val folderSpecs = FolderSpecs(resourceHelper)
folderSpecs.getWidthSpec(cells, availableSpace, calculatedWorkspaceSpec)
}
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_missingHeightBreakpoint_throwsError() {
val availableSpace = 900.dpToPx()
val cells = 3
val workspaceSpec =
WorkspaceSpec(
maxAvailableSize = availableSpace,
specType = WorkspaceSpec.SpecType.HEIGHT,
startPadding = SizeSpec(fixedSize = 10f),
endPadding = SizeSpec(fixedSize = 10f),
gutter = SizeSpec(fixedSize = 10f),
cellSize = SizeSpec(fixedSize = 10f)
)
val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_5)
val folderSpecs = FolderSpecs(resourceHelper)
folderSpecs.getHeightSpec(cells, availableSpace, calculatedWorkspaceSpec)
}
@Test
fun retrievesCalculatedWidthSpec() {
val availableSpace = 800.dpToPx()
val cells = 3
val workspaceSpec =
WorkspaceSpec(
maxAvailableSize = availableSpace,
specType = WorkspaceSpec.SpecType.WIDTH,
startPadding = SizeSpec(fixedSize = 10f),
endPadding = SizeSpec(fixedSize = 10f),
gutter = SizeSpec(fixedSize = 10f),
cellSize = SizeSpec(fixedSize = 10f)
)
val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
val expectedResult =
CalculatedFolderSpec(
startPaddingPx = 16.dpToPx(),
endPaddingPx = 16.dpToPx(),
gutterPx = 16.dpToPx(),
cellSizePx = calculatedWorkspaceSpec.cellSizePx,
availableSpace = availableSpace,
cells = cells
)
val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs)
val folderSpecs = FolderSpecs(resourceHelper)
val calculatedWidthSpec =
folderSpecs.getWidthSpec(cells, availableSpace, calculatedWorkspaceSpec)
assertThat(calculatedWidthSpec).isEqualTo(expectedResult)
}
@Test(expected = IllegalStateException::class)
fun retrievesCalculatedWidthSpec_invalidCalculatedWorkspaceSpecType_throwsError() {
val availableSpace = 10.dpToPx()
val cells = 3
val workspaceSpec =
WorkspaceSpec(
maxAvailableSize = availableSpace,
specType = WorkspaceSpec.SpecType.HEIGHT,
startPadding = SizeSpec(fixedSize = 10f),
endPadding = SizeSpec(fixedSize = 10f),
gutter = SizeSpec(fixedSize = 10f),
cellSize = SizeSpec(fixedSize = 10f)
)
val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs)
val folderSpecs = FolderSpecs(resourceHelper)
folderSpecs.getWidthSpec(cells, availableSpace, calculatedWorkspaceSpec)
}
@Test
fun retrievesCalculatedHeightSpec() {
val availableSpace = 700.dpToPx()
val cells = 3
val workspaceSpec =
WorkspaceSpec(
maxAvailableSize = availableSpace,
specType = WorkspaceSpec.SpecType.HEIGHT,
startPadding = SizeSpec(fixedSize = 10f),
endPadding = SizeSpec(fixedSize = 10f),
gutter = SizeSpec(fixedSize = 10f),
cellSize = SizeSpec(fixedSize = 10f)
)
val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
val expectedResult =
CalculatedFolderSpec(
startPaddingPx = 24.dpToPx(),
endPaddingPx = 64.dpToPx(),
gutterPx = 16.dpToPx(),
cellSizePx = calculatedWorkspaceSpec.cellSizePx,
availableSpace = availableSpace,
cells = cells
)
val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs)
val folderSpecs = FolderSpecs(resourceHelper)
val calculatedHeightSpec =
folderSpecs.getHeightSpec(cells, availableSpace, calculatedWorkspaceSpec)
assertThat(calculatedHeightSpec).isEqualTo(expectedResult)
}
@Test(expected = IllegalStateException::class)
fun retrievesCalculatedHeightSpec_invalidCalculatedWorkspaceSpecType_throwsError() {
val availableSpace = 10.dpToPx()
val cells = 3
val workspaceSpec =
WorkspaceSpec(
maxAvailableSize = availableSpace,
specType = WorkspaceSpec.SpecType.WIDTH,
startPadding = SizeSpec(fixedSize = 10f),
endPadding = SizeSpec(fixedSize = 10f),
gutter = SizeSpec(fixedSize = 10f),
cellSize = SizeSpec(fixedSize = 10f)
)
val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs)
val folderSpecs = FolderSpecs(resourceHelper)
folderSpecs.getHeightSpec(cells, availableSpace, calculatedWorkspaceSpec)
}
private fun Float.dpToPx(): Float {
return ResourceUtils.pxFromDp(this, context!!.resources.displayMetrics).toFloat()
}
private fun Int.dpToPx(): Int {
return ResourceUtils.pxFromDp(this.toFloat(), context!!.resources.displayMetrics)
}
}
@@ -22,6 +22,7 @@ import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.AbstractDeviceProfileTest
import com.google.common.truth.Truth.assertThat
import kotlin.math.roundToInt
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -52,6 +53,42 @@ class SizeSpecTest : AbstractDeviceProfileTest() {
}
}
@Test
fun validate_getCalculatedValue() {
val availableSpace = 100
val matchWorkspaceValue = 101
val combinations =
listOf(
SizeSpec(100f) to 100,
SizeSpec(ofAvailableSpace = .5f) to (availableSpace * .5f).roundToInt(),
SizeSpec(ofRemainderSpace = .5f) to 0,
SizeSpec(matchWorkspace = true) to matchWorkspaceValue
)
for ((sizeSpec, expectedValue) in combinations) {
val value = sizeSpec.getCalculatedValue(availableSpace, matchWorkspaceValue)
assertThat(value).isEqualTo(expectedValue)
}
}
@Test
fun validate_getRemainderSpaceValue() {
val remainderSpace = 100
val defaultValue = 10
val combinations =
listOf(
SizeSpec(100f) to defaultValue,
SizeSpec(ofAvailableSpace = .5f) to defaultValue,
SizeSpec(ofRemainderSpace = .5f) to (remainderSpace * .5f).roundToInt(),
SizeSpec(matchWorkspace = true) to defaultValue
)
for ((sizeSpec, expectedValue) in combinations) {
val value = sizeSpec.getRemainderSpaceValue(remainderSpace, defaultValue)
assertThat(value).isEqualTo(expectedValue)
}
}
@Test
fun multiple_values_assigned() {
val combinations =
@@ -23,12 +23,17 @@ import com.android.launcher3.R
import com.android.launcher3.tests.R as TestR
import kotlin.IntArray
class TestResourceHelper(private val context: Context, private val specsFileId: Int) :
class TestResourceHelper(private val context: Context, specsFileId: Int) :
ResourceHelper(context, specsFileId) {
override fun obtainStyledAttributes(attrs: AttributeSet, styleId: IntArray): TypedArray {
var clone = styleId.clone()
if (styleId == R.styleable.SizeSpec) clone = TestR.styleable.SizeSpec
else if (styleId == R.styleable.WorkspaceSpec) clone = TestR.styleable.WorkspaceSpec
val clone =
when {
styleId.contentEquals(R.styleable.SizeSpec) -> TestR.styleable.SizeSpec
styleId.contentEquals(R.styleable.WorkspaceSpec) -> TestR.styleable.WorkspaceSpec
styleId.contentEquals(R.styleable.FolderSpec) -> TestR.styleable.FolderSpec
else -> styleId.clone()
}
return context.obtainStyledAttributes(attrs, clone)
}
}