/* * Copyright 2018 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 androidx.preference; import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.os.Handler; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.RestrictTo; import androidx.annotation.VisibleForTesting; import androidx.core.content.ContextCompat; import androidx.core.view.ViewCompat; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; /** * An adapter that connects a RecyclerView to the {@link Preference} objects contained in the * associated {@link PreferenceGroup}. * * @hide */ @RestrictTo(LIBRARY_GROUP) public class PreferenceGroupAdapter extends RecyclerView.Adapter implements Preference.OnPreferenceChangeInternalListener, PreferenceGroup.PreferencePositionCallback { /** * The group that we are providing data from. */ private PreferenceGroup mPreferenceGroup; /** * Maps a position into this adapter -> {@link Preference}. These * {@link Preference}s don't have to be direct children of this * {@link PreferenceGroup}, they can be grand children or younger) */ private List mPreferenceList; /** * Contains a sorted list of all preferences in this adapter regardless of visibility. This is * used to construct {@link #mPreferenceList} */ private List mPreferenceListInternal; /** * List of unique Preference and its subclasses' names and layouts. */ private List mPreferenceLayouts; private PreferenceLayout mTempPreferenceLayout = new PreferenceLayout(); private Handler mHandler; private CollapsiblePreferenceGroupController mPreferenceGroupController; private Runnable mSyncRunnable = new Runnable() { @Override public void run() { syncMyPreferences(); } }; private static class PreferenceLayout { private int mResId; private int mWidgetResId; private String mName; PreferenceLayout() {} PreferenceLayout(PreferenceLayout other) { mResId = other.mResId; mWidgetResId = other.mWidgetResId; mName = other.mName; } @Override public boolean equals(Object o) { if (!(o instanceof PreferenceLayout)) { return false; } final PreferenceLayout other = (PreferenceLayout) o; return mResId == other.mResId && mWidgetResId == other.mWidgetResId && TextUtils.equals(mName, other.mName); } @Override public int hashCode() { int result = 17; result = 31 * result + mResId; result = 31 * result + mWidgetResId; result = 31 * result + mName.hashCode(); return result; } } public PreferenceGroupAdapter(PreferenceGroup preferenceGroup) { this(preferenceGroup, new Handler()); } private PreferenceGroupAdapter(PreferenceGroup preferenceGroup, Handler handler) { mPreferenceGroup = preferenceGroup; mHandler = handler; mPreferenceGroupController = new CollapsiblePreferenceGroupController(preferenceGroup, this); // If this group gets or loses any children, let us know mPreferenceGroup.setOnPreferenceChangeInternalListener(this); mPreferenceList = new ArrayList<>(); mPreferenceListInternal = new ArrayList<>(); mPreferenceLayouts = new ArrayList<>(); if (mPreferenceGroup instanceof PreferenceScreen) { setHasStableIds(((PreferenceScreen) mPreferenceGroup).shouldUseGeneratedIds()); } else { setHasStableIds(true); } syncMyPreferences(); } @VisibleForTesting static PreferenceGroupAdapter createInstanceWithCustomHandler(PreferenceGroup preferenceGroup, Handler handler) { return new PreferenceGroupAdapter(preferenceGroup, handler); } private void syncMyPreferences() { for (final Preference preference : mPreferenceListInternal) { // Clear out the listeners in anticipation of some items being removed. This listener // will be (re-)added to the remaining prefs when we flatten. preference.setOnPreferenceChangeInternalListener(null); } final List fullPreferenceList = new ArrayList<>(mPreferenceListInternal.size()); flattenPreferenceGroup(fullPreferenceList, mPreferenceGroup); final List visiblePreferenceList = mPreferenceGroupController.createVisiblePreferencesList(mPreferenceGroup); final List oldVisibleList = mPreferenceList; mPreferenceList = visiblePreferenceList; mPreferenceListInternal = fullPreferenceList; final PreferenceManager preferenceManager = mPreferenceGroup.getPreferenceManager(); if (preferenceManager != null && preferenceManager.getPreferenceComparisonCallback() != null) { final PreferenceManager.PreferenceComparisonCallback comparisonCallback = preferenceManager.getPreferenceComparisonCallback(); final DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() { @Override public int getOldListSize() { return oldVisibleList.size(); } @Override public int getNewListSize() { return visiblePreferenceList.size(); } @Override public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { return comparisonCallback.arePreferenceItemsTheSame( oldVisibleList.get(oldItemPosition), visiblePreferenceList.get(newItemPosition)); } @Override public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { return comparisonCallback.arePreferenceContentsTheSame( oldVisibleList.get(oldItemPosition), visiblePreferenceList.get(newItemPosition)); } }); result.dispatchUpdatesTo(this); } else { notifyDataSetChanged(); } for (final Preference preference : fullPreferenceList) { preference.clearWasDetached(); } } private void flattenPreferenceGroup(List preferences, PreferenceGroup group) { group.sortPreferences(); final int groupSize = group.getPreferenceCount(); for (int i = 0; i < groupSize; i++) { final Preference preference = group.getPreference(i); preferences.add(preference); addPreferenceClassName(preference); if (preference instanceof PreferenceGroup) { final PreferenceGroup preferenceAsGroup = (PreferenceGroup) preference; if (preferenceAsGroup.isOnSameScreenAsChildren()) { flattenPreferenceGroup(preferences, preferenceAsGroup); } } preference.setOnPreferenceChangeInternalListener(this); } } /** * Creates a string that includes the preference name, layout id and widget layout id. * If a particular preference type uses 2 different resources, they will be treated as * different view types. */ private PreferenceLayout createPreferenceLayout(Preference preference, PreferenceLayout in) { PreferenceLayout pl = in != null ? in : new PreferenceLayout(); pl.mName = preference.getClass().getName(); pl.mResId = preference.getLayoutResource(); pl.mWidgetResId = preference.getWidgetLayoutResource(); return pl; } private void addPreferenceClassName(Preference preference) { final PreferenceLayout pl = createPreferenceLayout(preference, null); if (!mPreferenceLayouts.contains(pl)) { mPreferenceLayouts.add(pl); } } @Override public int getItemCount() { return mPreferenceList.size(); } public Preference getItem(int position) { if (position < 0 || position >= getItemCount()) return null; return mPreferenceList.get(position); } @Override public long getItemId(int position) { if (!hasStableIds()) { return RecyclerView.NO_ID; } return this.getItem(position).getId(); } @Override public void onPreferenceChange(Preference preference) { final int index = mPreferenceList.indexOf(preference); // If we don't find the preference, we don't need to notify anyone if (index != -1) { // Send the pref object as a placeholder to ensure the view holder is recycled in place notifyItemChanged(index, preference); } } @Override public void onPreferenceHierarchyChange(Preference preference) { mHandler.removeCallbacks(mSyncRunnable); mHandler.post(mSyncRunnable); } @Override public void onPreferenceVisibilityChange(Preference preference) { if (!mPreferenceListInternal.contains(preference)) { return; } if (mPreferenceGroupController.onPreferenceVisibilityChange(preference)) { return; } if (preference.isVisible()) { // The preference has become visible, we need to add it in the correct location. // Index (inferred) in mPreferenceList of the item preceding the newly visible pref int previousVisibleIndex = -1; for (final Preference pref : mPreferenceListInternal) { if (preference.equals(pref)) { break; } if (pref.isVisible()) { previousVisibleIndex++; } } // Insert this preference into the active list just after the previous visible entry mPreferenceList.add(previousVisibleIndex + 1, preference); notifyItemInserted(previousVisibleIndex + 1); } else { // The preference has become invisible. Find it in the list and remove it. int removalIndex; final int listSize = mPreferenceList.size(); for (removalIndex = 0; removalIndex < listSize; removalIndex++) { if (preference.equals(mPreferenceList.get(removalIndex))) { break; } } mPreferenceList.remove(removalIndex); notifyItemRemoved(removalIndex); } } @Override public int getItemViewType(int position) { final Preference preference = this.getItem(position); mTempPreferenceLayout = createPreferenceLayout(preference, mTempPreferenceLayout); int viewType = mPreferenceLayouts.indexOf(mTempPreferenceLayout); if (viewType != -1) { return viewType; } else { viewType = mPreferenceLayouts.size(); mPreferenceLayouts.add(new PreferenceLayout(mTempPreferenceLayout)); return viewType; } } @Override public PreferenceViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final PreferenceLayout pl = mPreferenceLayouts.get(viewType); final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); TypedArray a = parent.getContext().obtainStyledAttributes(null, R.styleable.BackgroundStyle); Drawable background = a.getDrawable(R.styleable.BackgroundStyle_android_selectableItemBackground); if (background == null) { background = ContextCompat.getDrawable(parent.getContext(), android.R.drawable.list_selector_background); } a.recycle(); final View view = inflater.inflate(pl.mResId, parent, false); if (view.getBackground() == null) { ViewCompat.setBackground(view, background); } final ViewGroup widgetFrame = (ViewGroup) view.findViewById(android.R.id.widget_frame); if (widgetFrame != null) { if (pl.mWidgetResId != 0) { inflater.inflate(pl.mWidgetResId, widgetFrame); } else { widgetFrame.setVisibility(View.GONE); } } return new PreferenceViewHolder(view); } @Override public void onBindViewHolder(PreferenceViewHolder holder, int position) { final Preference preference = getItem(position); preference.onBindViewHolder(holder); } @Override public int getPreferenceAdapterPosition(String key) { final int size = mPreferenceList.size(); for (int i = 0; i < size; i++) { final Preference candidate = mPreferenceList.get(i); if (TextUtils.equals(key, candidate.getKey())) { return i; } } return RecyclerView.NO_POSITION; } @Override public int getPreferenceAdapterPosition(Preference preference) { final int size = mPreferenceList.size(); for (int i = 0; i < size; i++) { final Preference candidate = mPreferenceList.get(i); if (candidate != null && candidate.equals(preference)) { return i; } } return RecyclerView.NO_POSITION; } }