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

Commit a59b570c authored by Yi-an Chen's avatar Yi-an Chen Committed by Android (Google) Code Review
Browse files

Merge "[XPD] Support DrawableStateLayout in SettingsPreferenceGroupAdapter" into main

parents 93fd1c63 94c6cf3c
Loading
Loading
Loading
Loading
+21 −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.widget

interface DrawableStateLayout {
    var extraDrawableState: IntArray?
}
 No newline at end of file
+60 −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.widget

import android.content.Context
import android.util.AttributeSet
import android.widget.LinearLayout
import androidx.annotation.AttrRes
import androidx.annotation.StyleRes

/** This is a simple wrapper for [LinearLayout] that allows setting an extra drawable state. */
class DrawableStateLinearLayout : LinearLayout, DrawableStateLayout {
    override var extraDrawableState: IntArray? = null
        set(value) {
            if (field != value) {
                field = value
                refreshDrawableState()
            }
        }

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    constructor(
        context: Context,
        attrs: AttributeSet?,
        @AttrRes defStyleAttr: Int
    ) : super(context, attrs, defStyleAttr)

    constructor(
        context: Context,
        attrs: AttributeSet?,
        @AttrRes defStyleAttr: Int,
        @StyleRes defStyleRes: Int
    ) : super(context, attrs, defStyleAttr, defStyleRes)

    override fun onCreateDrawableState(extraSpace: Int): IntArray {
        val extraDrawableState =
            extraDrawableState ?: return super.onCreateDrawableState(extraSpace)
        return mergeDrawableStates(
            super.onCreateDrawableState(extraSpace + extraDrawableState.size),
            extraDrawableState
        )
    }
}
+119 −138
Original line number Diff line number Diff line
@@ -29,15 +29,23 @@ import androidx.preference.PreferenceViewHolder
import com.android.settingslib.widget.theme.R

/**
 * A custom adapter for displaying settings preferences in a list, handling rounded corners for
 * preference items within a group.
 * This is an extension over [PreferenceGroupAdapter] that allows creating visual sections for
 * preferences. It sets the following drawable states on item views when they are a
 * [DrawableStateLayout]:
 * - [android.R.attr.state_single] if the item is the only one in a section
 * - [android.R.attr.state_first] if the item is the first one in a section
 * - [android.R.attr.state_middle] if the item is neither the first one or the last one in a section
 * - [android.R.attr.state_last] if the item is the last one in a section
 *
 * Note that [androidx.preference.PreferenceManager.PreferenceComparisonCallback] isn't supported
 * (yet).
 */
@SuppressLint("RestrictedApi")
open class SettingsPreferenceGroupAdapter(preferenceGroup: PreferenceGroup) :
    PreferenceGroupAdapter(preferenceGroup) {

    private val mPreferenceGroup = preferenceGroup
    private var mRoundCornerMappingList: ArrayList<Int> = ArrayList()
    private var mItemPositionStates = intArrayOf()

    private var mNormalPaddingStart = 0
    private var mGroupPaddingStart = 0
@@ -67,7 +75,6 @@ open class SettingsPreferenceGroupAdapter(preferenceGroup: PreferenceGroup) :
        updatePreferencesList()
    }

    @SuppressLint("RestrictedApi")
    override fun onPreferenceHierarchyChange(preference: Preference) {
        super.onPreferenceHierarchyChange(preference)

@@ -92,17 +99,15 @@ open class SettingsPreferenceGroupAdapter(preferenceGroup: PreferenceGroup) :
            return
        }

        val oldList = ArrayList(mRoundCornerMappingList)
        mRoundCornerMappingList = ArrayList()
        mappingPreferenceGroup(mRoundCornerMappingList, mPreferenceGroup)

        if (mRoundCornerMappingList != oldList) {
            notifyOnlyChangedItems(oldList, mRoundCornerMappingList)
        val oldItemPositionStates = mItemPositionStates
        mItemPositionStates = buildItemPositionStates()
        if (!(mItemPositionStates contentEquals oldItemPositionStates)) {
            notifyOnlyChangedItems(oldItemPositionStates, mItemPositionStates)
        }
    }

    /** Notify any registered observers if the new list's items changed. */
    private fun notifyOnlyChangedItems(oldList: ArrayList<Int>, newList: ArrayList<Int>) {
    private fun notifyOnlyChangedItems(oldList: IntArray, newList: IntArray) {
        val minLength = minOf(oldList.size, newList.size)

        for (position in 0 until minLength) {
@@ -119,81 +124,53 @@ open class SettingsPreferenceGroupAdapter(preferenceGroup: PreferenceGroup) :
        }
    }

    @SuppressLint("RestrictedApi")
    private fun mappingPreferenceGroup(cornerStyles: MutableList<Int>, group: PreferenceGroup) {
        cornerStyles.clear()
        cornerStyles.addAll(MutableList(itemCount) { 0 })

        // the first item in to group
        var startIndex = -1
        // the last item in the group
        var endIndex = -1
        var currentParent: PreferenceGroup? = group
        for (i in 0 until itemCount) {
            when (val pref = getItem(i)) {
                // the preference has round corner background, so we don't need to handle it.
                is GroupSectionDividerMixin -> {
                    cornerStyles[i] = 0
                    startIndex = -1
                    endIndex = -1
                }

                // PreferenceCategory should not have round corner background.
                is PreferenceCategory -> {
                    cornerStyles[i] = 0
                    startIndex = -1
                    endIndex = -1
                    currentParent = pref
                }

                // SpacePreference should not have round corner background.
                is SpacePreference -> {
                    cornerStyles[i] = 0
                    endIndex = endIndex - 1
                }

                else -> {
                    // When ExpandablePreference is expanded, we treat is as the first item.
                    if (pref is Expandable && pref.isExpanded()) {
                        currentParent = pref as? PreferenceGroup
                        startIndex = i
                        cornerStyles[i] = cornerStyles[i] or ROUND_CORNER_TOP or ROUND_CORNER_CENTER
                        endIndex = -1
    private fun buildItemPositionStates(): IntArray {
        val itemCount = itemCount
        val itemPositionStates = IntArray(itemCount)

        var prevItemIndex = -2
        var previousParent: Preference? = null
        var currentParent: Preference? = null
        for (i in 0..<itemCount) {
            val preference = getItem(i)!!
            // If the preference is a group divider, skip this index (resulting in new group)
            if (isGroupDivider(preference)) {
                itemPositionStates[i] = 0
                continue
            }

            // Start a new group if any of the following are true:
            //     - We're at the first index
            //     - We've skipped an index
            //     - We've hit an expanded Expandable parent
            //     - We've changed parent (except: if parent is null, or we hit an Expandable child)
            previousParent = currentParent
            currentParent = preference.parent
            val isExpandedParent = preference is Expandable && preference.isExpanded()
            val isExpandedChild = currentParent is Expandable && currentParent.isExpanded()
            val changedParent = previousParent != currentParent && currentParent != null
            if (prevItemIndex != i - 1 || isExpandedParent || (changedParent && !isExpandedChild)) {
                closeGroup(itemPositionStates, prevItemIndex)
                itemPositionStates[i] = android.R.attr.state_first
                prevItemIndex = i
            } else {

                        val parent = pref?.parent

                        // item in the group should have round corner background.
                        cornerStyles[i] = cornerStyles[i] or ROUND_CORNER_CENTER
                        // We should treat the ExpandButton as a part of the previous group
                        // despite that it doesn't have a parent.
                        if (parent === currentParent || parent == null) {
                            // find the first item in the group
                            if (startIndex == -1) {
                                startIndex = i
                                cornerStyles[i] = cornerStyles[i] or ROUND_CORNER_TOP
                            }

                            // find the last item in the group, if we find the new last item, we should
                            // remove the old last item round corner.
                            if (endIndex == -1 || endIndex < i) {
                                if (endIndex != -1) {
                                    cornerStyles[endIndex] =
                                        cornerStyles[endIndex] and ROUND_CORNER_BOTTOM.inv()
                                }
                                endIndex = i
                                cornerStyles[i] = cornerStyles[i] or ROUND_CORNER_BOTTOM
                            }
                        } else {
                            // this item is new group, we should reset the index.
                            currentParent = parent
                            startIndex = i
                            cornerStyles[i] = cornerStyles[i] or ROUND_CORNER_TOP
                            endIndex = i
                            cornerStyles[i] = cornerStyles[i] or ROUND_CORNER_BOTTOM
                // Otherwise, continue current group
                itemPositionStates[i] = android.R.attr.state_middle
                prevItemIndex = i
            }
        }
        // Close current group
        closeGroup(itemPositionStates, prevItemIndex)
        return itemPositionStates
    }

    private fun closeGroup(itemPositionStates: IntArray, i: Int) {
        if (i >= 0) {
            itemPositionStates[i] =
                when (itemPositionStates[i]) {
                    0 -> 0
                    android.R.attr.state_first -> android.R.attr.state_single
                    else -> android.R.attr.state_last
                }
        }
    }
@@ -201,26 +178,26 @@ open class SettingsPreferenceGroupAdapter(preferenceGroup: PreferenceGroup) :
    /** handle roundCorner background */
    private fun updateBackground(holder: PreferenceViewHolder, position: Int) {
        val context = holder.itemView.context
        @DrawableRes
        val backgroundRes =
            when (SettingsThemeHelper.isExpressiveTheme(context)) {
                true -> getRoundCornerDrawableRes(position, isSelected = false)
                else -> mLegacyBackgroundRes
            }

        val v = holder.itemView
        // Update padding
        if (SettingsThemeHelper.isExpressiveTheme(context)) {
            val (paddingStart, paddingEnd) = getStartEndPadding(position, backgroundRes)
            val drawableStateLayout = holder.itemView as? DrawableStateLayout
            if (drawableStateLayout != null) {
                drawableStateLayout.extraDrawableState = stateSetOf(mItemPositionStates[position])
            } else {
                val backgroundRes = getRoundCornerDrawableRes(position, isSelected = false)
                val (paddingStart, paddingEnd) = getStartEndPadding(position)
                v.setPaddingRelative(paddingStart, v.paddingTop, paddingEnd, v.paddingBottom)
                v.clipToOutline = backgroundRes != 0
        }
        // Update background
                v.setBackgroundResource(backgroundRes)
            }
        } else {
            v.setBackgroundResource(mLegacyBackgroundRes)
        }
    }

    private fun getStartEndPadding(position: Int, backgroundRes: Int): Pair<Int, Int> {
    private fun getStartEndPadding(position: Int): Pair<Int, Int> {
        val item = getItem(position)
        val positionState = mItemPositionStates[position]
        return when {
            // This item handles edge to edge itself
            item is NormalPaddingMixin && item is GroupSectionDividerMixin -> 0 to 0
@@ -231,11 +208,11 @@ open class SettingsPreferenceGroupAdapter(preferenceGroup: PreferenceGroup) :
                mNormalPaddingStart + extraPadding to mNormalPaddingEnd + extraPadding
            }

            // According to mappingPreferenceGroup(), backgroundRes == 0 means this item is
            // GroupSectionDividerMixin or PreferenceCategory, which is design to have normal
            // padding.
            // NormalPaddingMixin items are also designed to have normal padding.
            backgroundRes == 0 || item is NormalPaddingMixin ->
            // This item should have normal padding if either:
            // - this item's positionState == 0 (which denotes that it is a section divider item
            //   such as a GroupSectionDividerMixin or PreferenceCategory), or
            // - this preference is a NormalPaddingMixin.
            positionState == 0 || item is NormalPaddingMixin ->
                mNormalPaddingStart to mNormalPaddingEnd

            // Other items are suppose to have group padding.
@@ -244,60 +221,64 @@ open class SettingsPreferenceGroupAdapter(preferenceGroup: PreferenceGroup) :
    }

    @DrawableRes
    protected fun getRoundCornerDrawableRes(position: Int, isSelected: Boolean): Int {
        return getRoundCornerDrawableRes(position, isSelected, false)
    }

    @DrawableRes
    @JvmOverloads
    protected fun getRoundCornerDrawableRes(
        position: Int,
        isSelected: Boolean,
        isHighlighted: Boolean,
        isHighlighted: Boolean = false,
    ): Int {
        if (position !in mRoundCornerMappingList.indices) {
            return 0
        }

        val cornerType = mRoundCornerMappingList[position]

        if ((cornerType and ROUND_CORNER_CENTER) == 0) {
            return 0
        }

        return when {
            (cornerType and ROUND_CORNER_TOP) != 0 && (cornerType and ROUND_CORNER_BOTTOM) == 0 -> {
                // the first
        val positionState = mItemPositionStates[position]
        return when (positionState) {
            // preference is the first of the section
            android.R.attr.state_first -> {
                if (isSelected) R.drawable.settingslib_round_background_top_selected
                else if (isHighlighted) R.drawable.settingslib_round_background_top_highlighted
                else R.drawable.settingslib_round_background_top
            }

            (cornerType and ROUND_CORNER_BOTTOM) != 0 && (cornerType and ROUND_CORNER_TOP) == 0 -> {
                // the last
            // preference is in the center of the section
            android.R.attr.state_middle -> {
                if (isSelected) R.drawable.settingslib_round_background_center_selected
                else if (isHighlighted) R.drawable.settingslib_round_background_center_highlighted
                else R.drawable.settingslib_round_background_center
            }
            // preference is the last of the section
            android.R.attr.state_last -> {
                if (isSelected) R.drawable.settingslib_round_background_bottom_selected
                else if (isHighlighted) R.drawable.settingslib_round_background_bottom_highlighted
                else R.drawable.settingslib_round_background_bottom
            }

            (cornerType and ROUND_CORNER_TOP) != 0 && (cornerType and ROUND_CORNER_BOTTOM) != 0 -> {
                // the only one preference
            // preference is the only one in the section
            android.R.attr.state_single -> {
                if (isSelected) R.drawable.settingslib_round_background_selected
                else if (isHighlighted) R.drawable.settingslib_round_background_highlighted
                else R.drawable.settingslib_round_background
            }

            else -> {
                // in the center
                if (isSelected) R.drawable.settingslib_round_background_center_selected
                else if (isHighlighted) R.drawable.settingslib_round_background_center_highlighted
                else R.drawable.settingslib_round_background_center
            }
            // preference is not part of a section
            else -> 0
        }
    }

    protected fun isGroupDivider(preference: Preference) =
        preference is GroupSectionDividerMixin || preference is PreferenceCategory
                || preference is SpacePreference

    companion object {
        private const val ROUND_CORNER_CENTER: Int = 1
        private const val ROUND_CORNER_TOP: Int = 1 shl 1
        private const val ROUND_CORNER_BOTTOM: Int = 1 shl 2
        private val STATE_SET_NONE = intArrayOf()
        private val STATE_SET_SINGLE = intArrayOf(android.R.attr.state_single)
        private val STATE_SET_FIRST = intArrayOf(android.R.attr.state_first)
        private val STATE_SET_MIDDLE = intArrayOf(android.R.attr.state_middle)
        private val STATE_SET_LAST = intArrayOf(android.R.attr.state_last)

        private fun stateSetOf(
            positionState: Int
        ): IntArray =
            when {
                positionState == 0 -> STATE_SET_NONE
                positionState == android.R.attr.state_single -> STATE_SET_SINGLE
                positionState == android.R.attr.state_first -> STATE_SET_FIRST
                positionState == android.R.attr.state_middle -> STATE_SET_MIDDLE
                positionState == android.R.attr.state_last -> STATE_SET_LAST
                else -> error(positionState)
            }
    }
}