| /* | |
| * 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<PreferenceViewHolder> | |
| 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<Preference> mPreferenceList; | |
| /** | |
| * Contains a sorted list of all preferences in this adapter regardless of visibility. This is | |
| * used to construct {@link #mPreferenceList} | |
| */ | |
| private List<Preference> mPreferenceListInternal; | |
| /** | |
| * List of unique Preference and its subclasses' names and layouts. | |
| */ | |
| private List<PreferenceLayout> 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<Preference> fullPreferenceList = new ArrayList<>(mPreferenceListInternal.size()); | |
| flattenPreferenceGroup(fullPreferenceList, mPreferenceGroup); | |
| final List<Preference> visiblePreferenceList = | |
| mPreferenceGroupController.createVisiblePreferencesList(mPreferenceGroup); | |
| final List<Preference> 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<Preference> 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; | |
| } | |
| } |