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