• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settingslib.widget
18 
19 import android.annotation.SuppressLint
20 import android.os.Handler
21 import android.os.Looper
22 import android.util.TypedValue
23 import androidx.annotation.DrawableRes
24 import androidx.preference.Preference
25 import androidx.preference.PreferenceCategory
26 import androidx.preference.PreferenceGroup
27 import androidx.preference.PreferenceGroupAdapter
28 import androidx.preference.PreferenceViewHolder
29 import com.android.settingslib.widget.theme.R
30 
31 /**
32  * A custom adapter for displaying settings preferences in a list, handling rounded corners for
33  * preference items within a group.
34  */
35 @SuppressLint("RestrictedApi")
36 open class SettingsPreferenceGroupAdapter(preferenceGroup: PreferenceGroup) :
37     PreferenceGroupAdapter(preferenceGroup) {
38 
39     private val mPreferenceGroup = preferenceGroup
40     private var mRoundCornerMappingList: ArrayList<Int> = ArrayList()
41 
42     private var mNormalPaddingStart = 0
43     private var mGroupPaddingStart = 0
44     private var mNormalPaddingEnd = 0
45     private var mGroupPaddingEnd = 0
46     @DrawableRes private var mLegacyBackgroundRes: Int
47 
48     private val mHandler = Handler(Looper.getMainLooper())
49 
<lambda>null50     private val syncRunnable = Runnable { updatePreferencesList() }
51 
52     init {
53         val context = preferenceGroup.context
54         mNormalPaddingStart =
55             context.resources.getDimensionPixelSize(R.dimen.settingslib_expressive_space_small1)
56         mGroupPaddingStart = mNormalPaddingStart * 2
57         mNormalPaddingEnd =
58             context.resources.getDimensionPixelSize(R.dimen.settingslib_expressive_space_small1)
59         mGroupPaddingEnd = mNormalPaddingEnd * 2
60         val outValue = TypedValue()
61         context.theme.resolveAttribute(
62             android.R.attr.selectableItemBackground,
63             outValue,
64             true, /* resolveRefs */
65         )
66         mLegacyBackgroundRes = outValue.resourceId
67         updatePreferencesList()
68     }
69 
70     @SuppressLint("RestrictedApi")
onPreferenceHierarchyChangenull71     override fun onPreferenceHierarchyChange(preference: Preference) {
72         super.onPreferenceHierarchyChange(preference)
73 
74         if (SettingsThemeHelper.isExpressiveTheme(preference.context)) {
75             // Post after super class has posted their sync runnable to update preferences.
76             mHandler.removeCallbacks(syncRunnable)
77             mHandler.post(syncRunnable)
78         }
79     }
80 
81     @SuppressLint("RestrictedApi")
onBindViewHoldernull82     override fun onBindViewHolder(holder: PreferenceViewHolder, position: Int) {
83         super.onBindViewHolder(holder, position)
84 
85         if (SettingsThemeHelper.isExpressiveTheme(holder.itemView.context)) {
86             updateBackground(holder, position)
87         }
88     }
89 
updatePreferencesListnull90     private fun updatePreferencesList() {
91         if (!SettingsThemeHelper.isExpressiveTheme(mPreferenceGroup.context)) {
92             return
93         }
94 
95         val oldList = ArrayList(mRoundCornerMappingList)
96         mRoundCornerMappingList = ArrayList()
97         mappingPreferenceGroup(mRoundCornerMappingList, mPreferenceGroup)
98         if (mRoundCornerMappingList != oldList) {
99             notifyDataSetChanged()
100         }
101     }
102 
103     @SuppressLint("RestrictedApi")
mappingPreferenceGroupnull104     private fun mappingPreferenceGroup(cornerStyles: MutableList<Int>, group: PreferenceGroup) {
105         cornerStyles.clear()
106         cornerStyles.addAll(MutableList(itemCount) { 0 })
107 
108         // the first item in to group
109         var startIndex = -1
110         // the last item in the group
111         var endIndex = -1
112         var currentParent: PreferenceGroup? = group
113         for (i in 0 until itemCount) {
114             when (val pref = getItem(i)) {
115                 // the preference has round corner background, so we don't need to handle it.
116                 is GroupSectionDividerMixin -> {
117                     cornerStyles[i] = 0
118                     startIndex = -1
119                     endIndex = -1
120                 }
121 
122                 // PreferenceCategory should not have round corner background.
123                 is PreferenceCategory -> {
124                     cornerStyles[i] = 0
125                     startIndex = -1
126                     endIndex = -1
127                     currentParent = pref
128                 }
129 
130                 // ExpandablePreference is PreferenceGroup but it should handle round corner
131                 is Expandable -> {
132                     // When ExpandablePreference is expanded, we treat is as the first item.
133                     if (pref.isExpanded()) {
134                         currentParent = pref as? PreferenceGroup
135                         startIndex = i
136                         cornerStyles[i] = cornerStyles[i] or ROUND_CORNER_TOP or ROUND_CORNER_CENTER
137                         endIndex = -1
138                     }
139                 }
140 
141                 else -> {
142                     val parent = pref?.parent
143 
144                     // item in the group should have round corner background.
145                     cornerStyles[i] = cornerStyles[i] or ROUND_CORNER_CENTER
146                     if (parent === currentParent) {
147                         // find the first item in the group
148                         if (startIndex == -1) {
149                             startIndex = i
150                             cornerStyles[i] = cornerStyles[i] or ROUND_CORNER_TOP
151                         }
152 
153                         // find the last item in the group, if we find the new last item, we should
154                         // remove the old last item round corner.
155                         if (endIndex == -1 || endIndex < i) {
156                             if (endIndex != -1) {
157                                 cornerStyles[endIndex] =
158                                     cornerStyles[endIndex] and ROUND_CORNER_BOTTOM.inv()
159                             }
160                             endIndex = i
161                             cornerStyles[i] = cornerStyles[i] or ROUND_CORNER_BOTTOM
162                         }
163                     } else {
164                         // this item is new group, we should reset the index.
165                         currentParent = parent
166                         startIndex = i
167                         cornerStyles[i] = cornerStyles[i] or ROUND_CORNER_TOP
168                         endIndex = i
169                         cornerStyles[i] = cornerStyles[i] or ROUND_CORNER_BOTTOM
170                     }
171                 }
172             }
173         }
174     }
175 
176     /** handle roundCorner background */
updateBackgroundnull177     private fun updateBackground(holder: PreferenceViewHolder, position: Int) {
178         val context = holder.itemView.context
179         @DrawableRes
180         val backgroundRes =
181             when (SettingsThemeHelper.isExpressiveTheme(context)) {
182                 true -> getRoundCornerDrawableRes(position, isSelected = false)
183                 else -> mLegacyBackgroundRes
184             }
185 
186         val v = holder.itemView
187         // Update padding
188         if (SettingsThemeHelper.isExpressiveTheme(context)) {
189             val (paddingStart, paddingEnd) = getStartEndPadding(position, backgroundRes)
190             v.setPaddingRelative(paddingStart, v.paddingTop, paddingEnd, v.paddingBottom)
191             v.clipToOutline = backgroundRes != 0
192         }
193         // Update background
194         v.setBackgroundResource(backgroundRes)
195     }
196 
getStartEndPaddingnull197     private fun getStartEndPadding(position: Int, backgroundRes: Int): Pair<Int, Int> {
198         val item = getItem(position)
199         return when {
200             // This item handles edge to edge itself
201             item is NormalPaddingMixin && item is GroupSectionDividerMixin -> 0 to 0
202 
203             // According to mappingPreferenceGroup(), backgroundRes == 0 means this item is
204             // GroupSectionDividerMixin or PreferenceCategory, which is design to have normal
205             // padding.
206             // NormalPaddingMixin items are also designed to have normal padding.
207             backgroundRes == 0 || item is NormalPaddingMixin ->
208                 mNormalPaddingStart to mNormalPaddingEnd
209 
210             // Other items are suppose to have group padding.
211             else -> mGroupPaddingStart to mGroupPaddingEnd
212         }
213     }
214 
215     @DrawableRes
getRoundCornerDrawableResnull216     protected fun getRoundCornerDrawableRes(position: Int, isSelected: Boolean): Int {
217         return getRoundCornerDrawableRes(position, isSelected, false)
218     }
219 
220     @DrawableRes
getRoundCornerDrawableResnull221     protected fun getRoundCornerDrawableRes(
222         position: Int,
223         isSelected: Boolean,
224         isHighlighted: Boolean,
225     ): Int {
226         val cornerType = mRoundCornerMappingList[position]
227 
228         if ((cornerType and ROUND_CORNER_CENTER) == 0) {
229             return 0
230         }
231 
232         return when {
233             (cornerType and ROUND_CORNER_TOP) != 0 && (cornerType and ROUND_CORNER_BOTTOM) == 0 -> {
234                 // the first
235                 if (isSelected) R.drawable.settingslib_round_background_top_selected
236                 else if (isHighlighted) R.drawable.settingslib_round_background_top_highlighted
237                 else R.drawable.settingslib_round_background_top
238             }
239 
240             (cornerType and ROUND_CORNER_BOTTOM) != 0 && (cornerType and ROUND_CORNER_TOP) == 0 -> {
241                 // the last
242                 if (isSelected) R.drawable.settingslib_round_background_bottom_selected
243                 else if (isHighlighted) R.drawable.settingslib_round_background_bottom_highlighted
244                 else R.drawable.settingslib_round_background_bottom
245             }
246 
247             (cornerType and ROUND_CORNER_TOP) != 0 && (cornerType and ROUND_CORNER_BOTTOM) != 0 -> {
248                 // the only one preference
249                 if (isSelected) R.drawable.settingslib_round_background_selected
250                 else if (isHighlighted) R.drawable.settingslib_round_background_highlighted
251                 else R.drawable.settingslib_round_background
252             }
253 
254             else -> {
255                 // in the center
256                 if (isSelected) R.drawable.settingslib_round_background_center_selected
257                 else if (isHighlighted) R.drawable.settingslib_round_background_center_highlighted
258                 else R.drawable.settingslib_round_background_center
259             }
260         }
261     }
262 
263     companion object {
264         private const val ROUND_CORNER_CENTER: Int = 1
265         private const val ROUND_CORNER_TOP: Int = 1 shl 1
266         private const val ROUND_CORNER_BOTTOM: Int = 1 shl 2
267     }
268 }
269