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

Commit e1141275 authored by Chaohui Wang's avatar Chaohui Wang Committed by Android (Google) Code Review
Browse files

Merge "[Spa] Refactor LazyCategory" into main

parents 983bc5f9 7028f285
Loading
Loading
Loading
Loading
+5 −7
Original line number Diff line number Diff line
@@ -17,7 +17,7 @@
package com.android.settingslib.spa.gallery.ui

import android.os.Bundle
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -93,12 +93,10 @@ object CategoryPageProvider : SettingsPageProvider {
                entries[2].UiLayout()
                entries[3].UiLayout()
            }
            Column(Modifier.height(200.dp)) {
                LazyCategory(
                    list = entries,
                    entry = { index: Int -> @Composable { entries[index].UiLayout() } },
                    title = { index: Int -> if (index == 0 || index == 2) "LazyCategory" else null },
                ) {}
            Box(Modifier.height(400.dp)) {
                LazyCategory(count = entries.size, key = { index -> index }) { index ->
                    entries[index].UiLayout()
                }
            }
        }
    }
+29 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.settingslib.spa.framework.theme

import androidx.compose.ui.unit.dp

object SettingsRadius {
    val none = 0.dp
    val extraSmall2 = 4.dp
    val medium = 12.dp
    val large1 = 16.dp
    val large2 = 20.dp
    val large3 = 24.dp
    val extraLarge1 = 28.dp
}
+6 −15
Original line number Diff line number Diff line
@@ -17,23 +17,14 @@
package com.android.settingslib.spa.framework.theme

import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.unit.dp

object SettingsShape {
    val CornerFull = CircleShape
    val CornerExtraSmall2 = RoundedCornerShape(4.dp)
    val CornerMedium = RoundedCornerShape(12.dp)
    val CornerLarge1 = RoundedCornerShape(16.dp)
    val CornerLarge2 = RoundedCornerShape(20.dp)
    val CornerExtraLarge1 = RoundedCornerShape(28.dp)

    // Legacy tokens below

    val TopCornerMedium2 =
        RoundedCornerShape(CornerSize(20.dp), CornerSize(20.dp), CornerSize(0), CornerSize(0))

    val BottomCornerMedium2 =
        RoundedCornerShape(CornerSize(0), CornerSize(0), CornerSize(20.dp), CornerSize(20.dp))
    val CornerExtraSmall2 = RoundedCornerShape(SettingsRadius.extraSmall2)
    val CornerMedium = RoundedCornerShape(SettingsRadius.medium)
    val CornerLarge1 = RoundedCornerShape(SettingsRadius.large1)
    val CornerLarge2 = RoundedCornerShape(SettingsRadius.large2)
    val CornerLarge3 = RoundedCornerShape(SettingsRadius.large3)
    val CornerExtraLarge1 = RoundedCornerShape(SettingsRadius.extraLarge1)
}
+2 −109
Original line number Diff line number Diff line
@@ -19,13 +19,8 @@ package com.android.settingslib.spa.widget.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.TouchApp
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
@@ -33,16 +28,15 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import com.android.settingslib.spa.framework.compose.thenIf
import com.android.settingslib.spa.framework.theme.SettingsDimension
import com.android.settingslib.spa.framework.theme.SettingsShape
@@ -116,88 +110,8 @@ fun Category(
    }
}

/**
 * A container that is used to group items with lazy loading.
 *
 * @param list The list of items to display.
 * @param entry The entry for each list item according to its index in list.
 * @param key Optional. The key for each item in list to provide unique item identifiers, making the
 *   list more efficient.
 * @param title Optional. Category title for each item or each group of items in the list. It should
 *   be decided by the index.
 * @param bottomPadding Optional. Bottom outside padding of the category.
 * @param state Optional. State of LazyList.
 * @param footer Optional. Content to be shown at the bottom of the category.
 * @param header Optional. Content to be shown at the top of the category.
 */
@Composable
fun LazyCategory(
    list: List<Any>,
    entry: (Int) -> @Composable () -> Unit,
    key: ((Int) -> Any)? = null,
    title: ((Int) -> String?)? = null,
    bottomPadding: Dp = SettingsDimension.paddingSmall,
    state: LazyListState = rememberLazyListState(),
    footer: @Composable () -> Unit = {},
    header: @Composable () -> Unit,
) {
    Column(
        Modifier.padding(
            PaddingValues(
                start = SettingsDimension.paddingLarge,
                end = SettingsDimension.paddingLarge,
                top = SettingsDimension.paddingSmall,
                bottom = bottomPadding,
            )
        )
    ) {
        LazyColumn(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.spacedBy(SettingsDimension.paddingTiny),
            state = state,
        ) {
            item { CompositionLocalProvider(LocalIsInCategory provides false) { header() } }

            items(count = list.size, key = key) {
                title?.invoke(it)?.let { title -> CategoryTitle(title) }
                when (it) {
                    0 -> {
                        if (list.size == 1) {
                            Column(modifier = Modifier.clip(SettingsShape.CornerLarge2)) {
                                CompositionLocalProvider(LocalIsInCategory provides true) {
                                    entry(it)()
                                }
                            }
                        } else {
                            Column(modifier = Modifier.clip(SettingsShape.TopCornerMedium2)) {
                                CompositionLocalProvider(LocalIsInCategory provides true) {
                                    entry(it)()
                                }
                            }
                        }
                    }

                    list.size - 1 -> {
                        Column(modifier = Modifier.clip(SettingsShape.BottomCornerMedium2)) {
                            CompositionLocalProvider(LocalIsInCategory provides true) {
                                entry(it)()
                            }
                        }
                    }

                    else -> {
                        CompositionLocalProvider(LocalIsInCategory provides true) { entry(it)() }
                    }
                }
            }

            item { CompositionLocalProvider(LocalIsInCategory provides true) { footer() } }
        }
    }
}

/** LocalIsInCategory containing the if the current composable is in a category. */
internal val LocalIsInCategory = compositionLocalOf { false }
internal val LocalIsInCategory = staticCompositionLocalOf { false }

@Preview
@Composable
@@ -221,24 +135,3 @@ private fun CategoryPreview() {
        }
    }
}

@Preview
@Composable
private fun LazyCategoryPreview() {
    SettingsTheme {
        LazyCategory(
            list = listOf(1, 2, 3),
            entry = { key ->
                @Composable {
                    Preference(
                        object : PreferenceModel {
                            override val title = key.toString()
                        }
                    )
                }
            },
            footer = @Composable { Footer("Footer") },
            header = @Composable { Text("Header") },
        )
    }
}
+169 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.settingslib.spa.widget.ui

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import com.android.settingslib.spa.framework.theme.SettingsRadius
import com.android.settingslib.spa.framework.theme.SettingsSpace
import com.android.settingslib.spa.framework.theme.SettingsTheme
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel

/**
 * A container that is used to group items with lazy loading.
 *
 * @param count the items count
 * @param key Optional. The key for each item in list to provide unique item identifiers, making the
 *   list more efficient.
 * @param bottomPadding Optional. Bottom outside padding of the category.
 * @param state Optional. State of LazyList.
 * @param header Optional. Content to be shown at the top of the category.
 * @param footer Optional. Content to be shown at the bottom of the category.
 * @param groupTitle Optional. A function to get the title for a group. Items with the same non-null
 *   title are considered part of the same group. A title is displayed before the first item of each
 *   group.
 * @param content - the content displayed by a single item
 */
@Composable
fun LazyCategory(
    count: Int,
    key: (Int) -> Any,
    bottomPadding: Dp = SettingsSpace.extraSmall4,
    state: LazyListState = rememberLazyListState(),
    header: @Composable () -> Unit = {},
    footer: @Composable () -> Unit = {},
    groupTitle: ((index: Int) -> String?)? = null,
    content: @Composable (index: Int) -> Unit,
) {
    LazyColumn(
        modifier = Modifier.fillMaxSize(),
        contentPadding =
            PaddingValues(
                start = SettingsSpace.small1,
                top = SettingsSpace.extraSmall4,
                end = SettingsSpace.small1,
                bottom = bottomPadding,
            ),
        verticalArrangement = Arrangement.spacedBy(SettingsSpace.extraSmall1),
        state = state,
    ) {
        item(contentType = "header") { header() }

        data class GroupTitles(val current: String?, val previous: String?, val next: String?)

        items(count = count, key = key) { index ->
            val groupTitles =
                remember(groupTitle, index) {
                    GroupTitles(
                        current = groupTitle?.invoke(index),
                        previous = if (index > 0) groupTitle?.invoke(index - 1) else null,
                        next = if (index + 1 < count) groupTitle?.invoke(index + 1) else null,
                    )
                }
            val isFirstInGroup = index == 0 || groupTitles.current != groupTitles.previous
            val isLastInGroup = index == count - 1 || groupTitles.current != groupTitles.next

            // Display group title if:
            // 1. The groupTitle feature is enabled (groupTitle parameter is not null).
            // 2. The current item has a non-null title.
            // 3. The current item's title is different from the last displayed title.
            if (isFirstInGroup) {
                groupTitles.current?.let { CategoryTitle(it) }
            }

            val topRadius = if (isFirstInGroup) SettingsRadius.large2 else SettingsRadius.none
            val bottomRadius = if (isLastInGroup) SettingsRadius.large2 else SettingsRadius.none
            val shape = RoundedCornerShape(topRadius, topRadius, bottomRadius, bottomRadius)
            Box(modifier = Modifier.clip(shape)) {
                CompositionLocalProvider(LocalIsInCategory provides true) { content(index) }
            }
        }

        item(contentType = "footer") { footer() }
    }
}

@Preview
@Composable
private fun LazyCategoryPreview() {
    fun groupTitle(index: Int): String? =
        when (index) {
            0,
            1 -> "General"

            2,
            3 -> "Display"

            4 -> "Sound" // Single item group

            // Item 5 will have null title, thus no explicit group title before it.
            else -> null
        }

    SettingsTheme {
        LazyCategory(
            count = 6,
            key = { index -> index },
            header = { SettingsIntro("All My Settings") },
            footer = { Footer("End of settings list") },
            groupTitle = ::groupTitle,
        ) { index ->
            Preference(
                object : PreferenceModel {
                    override val title = "Preference Item $index"
                    override val summary = {
                        "This is item number $index in group '${groupTitle(index)}'."
                    }
                }
            )
        }
    }
}

@Preview
@Composable
private fun LazyCategoryNoGroupTitlePreview() {
    SettingsTheme {
        LazyCategory(
            count = 3,
            key = { index -> index },
            header = { SettingsIntro("Simple List") },
            footer = { Footer("Simple Footer") },
            groupTitle = null,
        ) { index ->
            Preference(
                object : PreferenceModel {
                    override val title = "Item $index (No Groups)"
                }
            )
        }
    }
}
Loading