Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7ae9e3a6 authored by Jordan Silva's avatar Jordan Silva
Browse files

Add FolderSpec for responsive grid support

Adds responsive grid implementation for folders. It follows the same concept as WorkspaceSpecs, so we have a parser similar to WorkspaceSpecs for FolderSpecs.

Bug: 284155638
Flag: ENABLE_RESPONSIVE_WORKSPACE
Test: FolderSpecsTest
Test: CalculatedFolderSpecsTest
Change-Id: Iea6d7d88ef42d1273aed7cf2ed5b397035518a52
parent d22a85cd
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -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" />
+280 −0
Original line number Diff line number Diff line
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
    )
}
+37 −5
Original line number Diff line number Diff line
@@ -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()
+1 −0
Original line number Diff line number Diff line
@@ -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 −0
Original line number Diff line number Diff line
@@ -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>
Loading