1/* 2 * Copyright 2018 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 17package androidx.preference; 18 19import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; 20 21import android.content.res.TypedArray; 22import android.graphics.drawable.Drawable; 23import android.os.Handler; 24import android.text.TextUtils; 25import android.view.LayoutInflater; 26import android.view.View; 27import android.view.ViewGroup; 28 29import androidx.annotation.RestrictTo; 30import androidx.annotation.VisibleForTesting; 31import androidx.core.content.ContextCompat; 32import androidx.core.view.ViewCompat; 33import androidx.recyclerview.widget.DiffUtil; 34import androidx.recyclerview.widget.RecyclerView; 35 36import java.util.ArrayList; 37import java.util.List; 38 39/** 40 * An adapter that connects a RecyclerView to the {@link Preference} objects contained in the 41 * associated {@link PreferenceGroup}. 42 * 43 * @hide 44 */ 45@RestrictTo(LIBRARY_GROUP) 46public class PreferenceGroupAdapter extends RecyclerView.Adapter<PreferenceViewHolder> 47 implements Preference.OnPreferenceChangeInternalListener, 48 PreferenceGroup.PreferencePositionCallback { 49 50 /** 51 * The group that we are providing data from. 52 */ 53 private PreferenceGroup mPreferenceGroup; 54 55 /** 56 * Maps a position into this adapter -> {@link Preference}. These 57 * {@link Preference}s don't have to be direct children of this 58 * {@link PreferenceGroup}, they can be grand children or younger) 59 */ 60 private List<Preference> mPreferenceList; 61 62 /** 63 * Contains a sorted list of all preferences in this adapter regardless of visibility. This is 64 * used to construct {@link #mPreferenceList} 65 */ 66 private List<Preference> mPreferenceListInternal; 67 68 /** 69 * List of unique Preference and its subclasses' names and layouts. 70 */ 71 private List<PreferenceLayout> mPreferenceLayouts; 72 73 74 private PreferenceLayout mTempPreferenceLayout = new PreferenceLayout(); 75 76 private Handler mHandler; 77 78 private CollapsiblePreferenceGroupController mPreferenceGroupController; 79 80 private Runnable mSyncRunnable = new Runnable() { 81 @Override 82 public void run() { 83 syncMyPreferences(); 84 } 85 }; 86 87 private static class PreferenceLayout { 88 private int mResId; 89 private int mWidgetResId; 90 private String mName; 91 92 PreferenceLayout() {} 93 94 PreferenceLayout(PreferenceLayout other) { 95 mResId = other.mResId; 96 mWidgetResId = other.mWidgetResId; 97 mName = other.mName; 98 } 99 100 @Override 101 public boolean equals(Object o) { 102 if (!(o instanceof PreferenceLayout)) { 103 return false; 104 } 105 final PreferenceLayout other = (PreferenceLayout) o; 106 return mResId == other.mResId 107 && mWidgetResId == other.mWidgetResId 108 && TextUtils.equals(mName, other.mName); 109 } 110 111 @Override 112 public int hashCode() { 113 int result = 17; 114 result = 31 * result + mResId; 115 result = 31 * result + mWidgetResId; 116 result = 31 * result + mName.hashCode(); 117 return result; 118 } 119 } 120 121 public PreferenceGroupAdapter(PreferenceGroup preferenceGroup) { 122 this(preferenceGroup, new Handler()); 123 } 124 125 private PreferenceGroupAdapter(PreferenceGroup preferenceGroup, Handler handler) { 126 mPreferenceGroup = preferenceGroup; 127 mHandler = handler; 128 mPreferenceGroupController = 129 new CollapsiblePreferenceGroupController(preferenceGroup, this); 130 // If this group gets or loses any children, let us know 131 mPreferenceGroup.setOnPreferenceChangeInternalListener(this); 132 133 mPreferenceList = new ArrayList<>(); 134 mPreferenceListInternal = new ArrayList<>(); 135 mPreferenceLayouts = new ArrayList<>(); 136 137 if (mPreferenceGroup instanceof PreferenceScreen) { 138 setHasStableIds(((PreferenceScreen) mPreferenceGroup).shouldUseGeneratedIds()); 139 } else { 140 setHasStableIds(true); 141 } 142 143 syncMyPreferences(); 144 } 145 146 @VisibleForTesting 147 static PreferenceGroupAdapter createInstanceWithCustomHandler(PreferenceGroup preferenceGroup, 148 Handler handler) { 149 return new PreferenceGroupAdapter(preferenceGroup, handler); 150 } 151 152 private void syncMyPreferences() { 153 for (final Preference preference : mPreferenceListInternal) { 154 // Clear out the listeners in anticipation of some items being removed. This listener 155 // will be (re-)added to the remaining prefs when we flatten. 156 preference.setOnPreferenceChangeInternalListener(null); 157 } 158 final List<Preference> fullPreferenceList = new ArrayList<>(mPreferenceListInternal.size()); 159 flattenPreferenceGroup(fullPreferenceList, mPreferenceGroup); 160 161 final List<Preference> visiblePreferenceList = 162 mPreferenceGroupController.createVisiblePreferencesList(mPreferenceGroup); 163 164 final List<Preference> oldVisibleList = mPreferenceList; 165 mPreferenceList = visiblePreferenceList; 166 mPreferenceListInternal = fullPreferenceList; 167 168 final PreferenceManager preferenceManager = mPreferenceGroup.getPreferenceManager(); 169 if (preferenceManager != null 170 && preferenceManager.getPreferenceComparisonCallback() != null) { 171 final PreferenceManager.PreferenceComparisonCallback comparisonCallback = 172 preferenceManager.getPreferenceComparisonCallback(); 173 final DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() { 174 @Override 175 public int getOldListSize() { 176 return oldVisibleList.size(); 177 } 178 179 @Override 180 public int getNewListSize() { 181 return visiblePreferenceList.size(); 182 } 183 184 @Override 185 public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { 186 return comparisonCallback.arePreferenceItemsTheSame( 187 oldVisibleList.get(oldItemPosition), 188 visiblePreferenceList.get(newItemPosition)); 189 } 190 191 @Override 192 public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { 193 return comparisonCallback.arePreferenceContentsTheSame( 194 oldVisibleList.get(oldItemPosition), 195 visiblePreferenceList.get(newItemPosition)); 196 } 197 }); 198 199 result.dispatchUpdatesTo(this); 200 } else { 201 notifyDataSetChanged(); 202 } 203 204 for (final Preference preference : fullPreferenceList) { 205 preference.clearWasDetached(); 206 } 207 } 208 209 private void flattenPreferenceGroup(List<Preference> preferences, PreferenceGroup group) { 210 group.sortPreferences(); 211 212 final int groupSize = group.getPreferenceCount(); 213 for (int i = 0; i < groupSize; i++) { 214 final Preference preference = group.getPreference(i); 215 216 preferences.add(preference); 217 218 addPreferenceClassName(preference); 219 220 if (preference instanceof PreferenceGroup) { 221 final PreferenceGroup preferenceAsGroup = (PreferenceGroup) preference; 222 if (preferenceAsGroup.isOnSameScreenAsChildren()) { 223 flattenPreferenceGroup(preferences, preferenceAsGroup); 224 } 225 } 226 227 preference.setOnPreferenceChangeInternalListener(this); 228 } 229 } 230 231 /** 232 * Creates a string that includes the preference name, layout id and widget layout id. 233 * If a particular preference type uses 2 different resources, they will be treated as 234 * different view types. 235 */ 236 private PreferenceLayout createPreferenceLayout(Preference preference, PreferenceLayout in) { 237 PreferenceLayout pl = in != null ? in : new PreferenceLayout(); 238 pl.mName = preference.getClass().getName(); 239 pl.mResId = preference.getLayoutResource(); 240 pl.mWidgetResId = preference.getWidgetLayoutResource(); 241 return pl; 242 } 243 244 private void addPreferenceClassName(Preference preference) { 245 final PreferenceLayout pl = createPreferenceLayout(preference, null); 246 if (!mPreferenceLayouts.contains(pl)) { 247 mPreferenceLayouts.add(pl); 248 } 249 } 250 251 @Override 252 public int getItemCount() { 253 return mPreferenceList.size(); 254 } 255 256 public Preference getItem(int position) { 257 if (position < 0 || position >= getItemCount()) return null; 258 return mPreferenceList.get(position); 259 } 260 261 @Override 262 public long getItemId(int position) { 263 if (!hasStableIds()) { 264 return RecyclerView.NO_ID; 265 } 266 return this.getItem(position).getId(); 267 } 268 269 @Override 270 public void onPreferenceChange(Preference preference) { 271 final int index = mPreferenceList.indexOf(preference); 272 // If we don't find the preference, we don't need to notify anyone 273 if (index != -1) { 274 // Send the pref object as a placeholder to ensure the view holder is recycled in place 275 notifyItemChanged(index, preference); 276 } 277 } 278 279 @Override 280 public void onPreferenceHierarchyChange(Preference preference) { 281 mHandler.removeCallbacks(mSyncRunnable); 282 mHandler.post(mSyncRunnable); 283 } 284 285 @Override 286 public void onPreferenceVisibilityChange(Preference preference) { 287 if (!mPreferenceListInternal.contains(preference)) { 288 return; 289 } 290 if (mPreferenceGroupController.onPreferenceVisibilityChange(preference)) { 291 return; 292 } 293 if (preference.isVisible()) { 294 // The preference has become visible, we need to add it in the correct location. 295 296 // Index (inferred) in mPreferenceList of the item preceding the newly visible pref 297 int previousVisibleIndex = -1; 298 for (final Preference pref : mPreferenceListInternal) { 299 if (preference.equals(pref)) { 300 break; 301 } 302 if (pref.isVisible()) { 303 previousVisibleIndex++; 304 } 305 } 306 // Insert this preference into the active list just after the previous visible entry 307 mPreferenceList.add(previousVisibleIndex + 1, preference); 308 309 notifyItemInserted(previousVisibleIndex + 1); 310 } else { 311 // The preference has become invisible. Find it in the list and remove it. 312 313 int removalIndex; 314 final int listSize = mPreferenceList.size(); 315 for (removalIndex = 0; removalIndex < listSize; removalIndex++) { 316 if (preference.equals(mPreferenceList.get(removalIndex))) { 317 break; 318 } 319 } 320 mPreferenceList.remove(removalIndex); 321 notifyItemRemoved(removalIndex); 322 } 323 } 324 325 @Override 326 public int getItemViewType(int position) { 327 final Preference preference = this.getItem(position); 328 329 mTempPreferenceLayout = createPreferenceLayout(preference, mTempPreferenceLayout); 330 331 int viewType = mPreferenceLayouts.indexOf(mTempPreferenceLayout); 332 if (viewType != -1) { 333 return viewType; 334 } else { 335 viewType = mPreferenceLayouts.size(); 336 mPreferenceLayouts.add(new PreferenceLayout(mTempPreferenceLayout)); 337 return viewType; 338 } 339 } 340 341 @Override 342 public PreferenceViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 343 final PreferenceLayout pl = mPreferenceLayouts.get(viewType); 344 final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 345 TypedArray a 346 = parent.getContext().obtainStyledAttributes(null, R.styleable.BackgroundStyle); 347 Drawable background 348 = a.getDrawable(R.styleable.BackgroundStyle_android_selectableItemBackground); 349 if (background == null) { 350 background = ContextCompat.getDrawable(parent.getContext(), 351 android.R.drawable.list_selector_background); 352 } 353 a.recycle(); 354 355 final View view = inflater.inflate(pl.mResId, parent, false); 356 if (view.getBackground() == null) { 357 ViewCompat.setBackground(view, background); 358 } 359 360 final ViewGroup widgetFrame = (ViewGroup) view.findViewById(android.R.id.widget_frame); 361 if (widgetFrame != null) { 362 if (pl.mWidgetResId != 0) { 363 inflater.inflate(pl.mWidgetResId, widgetFrame); 364 } else { 365 widgetFrame.setVisibility(View.GONE); 366 } 367 } 368 369 return new PreferenceViewHolder(view); 370 } 371 372 @Override 373 public void onBindViewHolder(PreferenceViewHolder holder, int position) { 374 final Preference preference = getItem(position); 375 preference.onBindViewHolder(holder); 376 } 377 378 @Override 379 public int getPreferenceAdapterPosition(String key) { 380 final int size = mPreferenceList.size(); 381 for (int i = 0; i < size; i++) { 382 final Preference candidate = mPreferenceList.get(i); 383 if (TextUtils.equals(key, candidate.getKey())) { 384 return i; 385 } 386 } 387 return RecyclerView.NO_POSITION; 388 } 389 390 @Override 391 public int getPreferenceAdapterPosition(Preference preference) { 392 final int size = mPreferenceList.size(); 393 for (int i = 0; i < size; i++) { 394 final Preference candidate = mPreferenceList.get(i); 395 if (candidate != null && candidate.equals(preference)) { 396 return i; 397 } 398 } 399 return RecyclerView.NO_POSITION; 400 } 401} 402