[go: nahoru, domu]

blob: 2dbb91a98544a4e8c4640c873a86c0bef4087921 [file] [log] [blame]
/*
* 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_PREFIX;
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.NonNull;
import androidx.annotation.RestrictTo;
import androidx.appcompat.content.res.AppCompatResources;
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 {@link RecyclerView} to the {@link Preference}s contained in
* an associated {@link PreferenceGroup}.
*
* Used by Settings.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public class PreferenceGroupAdapter extends RecyclerView.Adapter<PreferenceViewHolder>
implements Preference.OnPreferenceChangeInternalListener,
PreferenceGroup.PreferencePositionCallback {
/**
* The {@link PreferenceGroup} that we build a list of preferences from. This should
* typically be the root {@link PreferenceScreen} managed by a {@link PreferenceFragmentCompat}.
*/
private PreferenceGroup mPreferenceGroup;
/**
* Contains a sorted list of all {@link Preference}s in this adapter regardless of visibility.
* This is used to construct {@link #mVisiblePreferences}.
*/
private List<Preference> mPreferences;
/**
* Contains a sorted list of all {@link Preference}s in this adapter that are visible to the
* user and hence displayed in the attached {@link RecyclerView}. The position of
* {@link Preference}s in this list corresponds to the position of the {@link Preference}s
* displayed on screen. These {@link Preference}s don't have to be direct children of
* {@link #mPreferenceGroup}, they can be children of other nested {@link PreferenceGroup}s.
*/
private List<Preference> mVisiblePreferences;
/**
* List of unique {@link PreferenceResourceDescriptor}s, used to cache item view types for
* {@link RecyclerView}.
*/
private List<PreferenceResourceDescriptor> mPreferenceResourceDescriptors;
private Handler mHandler;
private Runnable mSyncRunnable = new Runnable() {
@Override
public void run() {
updatePreferences();
}
};
@SuppressWarnings("deprecation")
public PreferenceGroupAdapter(PreferenceGroup preferenceGroup) {
mPreferenceGroup = preferenceGroup;
mHandler = new Handler();
// This adapter should be notified when preferences are added or removed from the group
mPreferenceGroup.setOnPreferenceChangeInternalListener(this);
mPreferences = new ArrayList<>();
mVisiblePreferences = new ArrayList<>();
mPreferenceResourceDescriptors = new ArrayList<>();
if (mPreferenceGroup instanceof PreferenceScreen) {
setHasStableIds(((PreferenceScreen) mPreferenceGroup).shouldUseGeneratedIds());
} else {
setHasStableIds(true);
}
// Initial sync to generate mPreferences and mVisiblePreferences and display the visible
// preferences in the RecyclerView
updatePreferences();
}
/**
* Updates {@link #mPreferences} and {@link #mVisiblePreferences} as well as notifying
* {@link RecyclerView} of any changes.
*/
@SuppressWarnings("WeakerAccess") /* synthetic access */
void updatePreferences() {
for (final Preference preference : mPreferences) {
// Clear out the listeners in anticipation of some items being removed. This listener
// will be set again on any remaining preferences when we flatten the group.
preference.setOnPreferenceChangeInternalListener(null);
}
// Attempt to reuse the current array size when creating the new array for efficiency
final int size = mPreferences.size();
mPreferences = new ArrayList<>(size);
flattenPreferenceGroup(mPreferences, mPreferenceGroup);
final List<Preference> oldVisibleList = mVisiblePreferences;
// Create a new variable so we can pass into DiffUtil without using a synthetic accessor
// to access the private mVisiblePreferences
final List<Preference> visiblePreferenceList = createVisiblePreferencesList(
mPreferenceGroup);
mVisiblePreferences = visiblePreferenceList;
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 : mPreferences) {
preference.clearWasDetached();
}
}
/**
* Recursively builds a list containing a flattened representation of a given
* {@link PreferenceGroup}, which may itself contain nested {@link PreferenceGroup}s with
* their own {@link Preference}s.
*
* @param preferences The list to add the flattened {@link Preference}s to
* @param group The {@link PreferenceGroup} to generate the list for
*/
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);
final PreferenceResourceDescriptor descriptor = new PreferenceResourceDescriptor(
preference);
if (!mPreferenceResourceDescriptors.contains(descriptor)) {
mPreferenceResourceDescriptors.add(descriptor);
}
if (preference instanceof PreferenceGroup) {
final PreferenceGroup nestedGroup = (PreferenceGroup) preference;
if (nestedGroup.isOnSameScreenAsChildren()) {
flattenPreferenceGroup(preferences, nestedGroup);
}
}
preference.setOnPreferenceChangeInternalListener(this);
}
}
/**
* Recursively generates a list of {@link Preference}s visible to the user.
*
* @param group The root preference group to be processed
* @return The flattened and visible section of the preference group
*/
private List<Preference> createVisiblePreferencesList(PreferenceGroup group) {
int visiblePreferenceCount = 0;
final List<Preference> visiblePreferences = new ArrayList<>();
final List<Preference> collapsedPreferences = new ArrayList<>();
final int groupSize = group.getPreferenceCount();
for (int i = 0; i < groupSize; i++) {
final Preference preference = group.getPreference(i);
if (!preference.isVisible()) {
continue;
}
if (!isGroupExpandable(group)
|| visiblePreferenceCount < group.getInitialExpandedChildrenCount()) {
visiblePreferences.add(preference);
} else {
collapsedPreferences.add(preference);
}
// PreferenceGroups do not count towards the maximal number of preferences to show
if (!(preference instanceof PreferenceGroup)) {
visiblePreferenceCount++;
continue;
}
PreferenceGroup innerGroup = (PreferenceGroup) preference;
if (!innerGroup.isOnSameScreenAsChildren()) {
continue;
}
if (isGroupExpandable(group) && isGroupExpandable(innerGroup)) {
throw new IllegalStateException(
"Nesting an expandable group inside of another expandable group is not "
+ "supported!");
}
// Recursively generate nested list of visible preferences
final List<Preference> innerList = createVisiblePreferencesList(innerGroup);
for (Preference inner : innerList) {
if (!isGroupExpandable(group)
|| visiblePreferenceCount < group.getInitialExpandedChildrenCount()) {
visiblePreferences.add(inner);
} else {
collapsedPreferences.add(inner);
}
visiblePreferenceCount++;
}
}
// If there are any visible preferences being hidden, add an expand button to show the rest
// of the preferences. Clicking the expand button will show all the visible preferences.
if (isGroupExpandable(group)
&& visiblePreferenceCount > group.getInitialExpandedChildrenCount()) {
final ExpandButton expandButton = createExpandButton(group, collapsedPreferences);
visiblePreferences.add(expandButton);
}
return visiblePreferences;
}
/**
* Creates a {@link ExpandButton} for a given {@link PreferenceGroup} that will expand the
* group when clicked, showing preferences previously collapsed by the group.
*
* @param group The {@link PreferenceGroup} to create the {@link ExpandButton}
* for
* @param collapsedPreferences The {@link Preference}s that have been collapsed by the group.
* These are used to generate the summary for the
* {@link ExpandButton}
* @return An {@link ExpandButton} to be displayed as the last item in a {@link PreferenceGroup}
*/
private ExpandButton createExpandButton(final PreferenceGroup group,
List<Preference> collapsedPreferences) {
final ExpandButton preference = new ExpandButton(
group.getContext(),
collapsedPreferences,
group.getId()
);
preference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
group.setInitialExpandedChildrenCount(Integer.MAX_VALUE);
onPreferenceHierarchyChange(preference);
final PreferenceGroup.OnExpandButtonClickListener listener =
group.getOnExpandButtonClickListener();
if (listener != null) {
listener.onExpandButtonClick();
}
return true;
}
});
return preference;
}
/**
* Helper method to return whether a group allows hiding some of its preferences into an
* {@link ExpandButton}.
*
* @param preferenceGroup The group to be checked
* @return {@code true} if the group is expandable
*/
private boolean isGroupExpandable(PreferenceGroup preferenceGroup) {
return preferenceGroup.getInitialExpandedChildrenCount() != Integer.MAX_VALUE;
}
/**
* Returns the {@link Preference} at the given position
*
* @param position The position of the {@link Preference} in the list displayed to the user
* @return The corresponding {@link Preference}, or {@code null} if the given position is out
* of bounds
*/
public Preference getItem(int position) {
if (position < 0 || position >= getItemCount()) return null;
return mVisiblePreferences.get(position);
}
@Override
public int getItemCount() {
return mVisiblePreferences.size();
}
@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 = mVisiblePreferences.indexOf(preference);
// If we don't find the preference, we don't need to notify anyone
if (index != -1) {
// Send the preference 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) {
onPreferenceHierarchyChange(preference);
}
@Override
public int getItemViewType(int position) {
final Preference preference = this.getItem(position);
PreferenceResourceDescriptor descriptor = new PreferenceResourceDescriptor(preference);
int viewType = mPreferenceResourceDescriptors.indexOf(descriptor);
if (viewType != -1) {
return viewType;
} else {
viewType = mPreferenceResourceDescriptors.size();
mPreferenceResourceDescriptors.add(descriptor);
return viewType;
}
}
@Override
@NonNull
public PreferenceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
final PreferenceResourceDescriptor descriptor = mPreferenceResourceDescriptors.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 = AppCompatResources.getDrawable(parent.getContext(),
android.R.drawable.list_selector_background);
}
a.recycle();
final View view = inflater.inflate(descriptor.mLayoutResId, parent, false);
if (view.getBackground() == null) {
ViewCompat.setBackground(view, background);
}
final ViewGroup widgetFrame = view.findViewById(android.R.id.widget_frame);
if (widgetFrame != null) {
if (descriptor.mWidgetLayoutResId != 0) {
inflater.inflate(descriptor.mWidgetLayoutResId, widgetFrame);
} else {
widgetFrame.setVisibility(View.GONE);
}
}
return new PreferenceViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull PreferenceViewHolder holder, int position) {
final Preference preference = getItem(position);
holder.resetState();
preference.onBindViewHolder(holder);
}
@Override
public int getPreferenceAdapterPosition(String key) {
final int size = mVisiblePreferences.size();
for (int i = 0; i < size; i++) {
final Preference candidate = mVisiblePreferences.get(i);
if (TextUtils.equals(key, candidate.getKey())) {
return i;
}
}
return RecyclerView.NO_POSITION;
}
@Override
public int getPreferenceAdapterPosition(Preference preference) {
final int size = mVisiblePreferences.size();
for (int i = 0; i < size; i++) {
final Preference candidate = mVisiblePreferences.get(i);
if (candidate != null && candidate.equals(preference)) {
return i;
}
}
return RecyclerView.NO_POSITION;
}
/**
* Describes a unique combination of layout resource, widget layout resource, and class name.
* This is needed as different instances of {@link Preference} subclasses can have different
* layout / widget layout resources set, and so we can only reuse the same cached
* {@link PreferenceViewHolder} if the class name, layout resource, and widget layout
* resource are identical.
*/
private static class PreferenceResourceDescriptor {
int mLayoutResId;
int mWidgetLayoutResId;
String mClassName;
PreferenceResourceDescriptor(Preference preference) {
mClassName = preference.getClass().getName();
mLayoutResId = preference.getLayoutResource();
mWidgetLayoutResId = preference.getWidgetLayoutResource();
}
@Override
public boolean equals(Object o) {
if (!(o instanceof PreferenceResourceDescriptor)) {
return false;
}
final PreferenceResourceDescriptor other = (PreferenceResourceDescriptor) o;
return mLayoutResId == other.mLayoutResId
&& mWidgetLayoutResId == other.mWidgetLayoutResId
&& TextUtils.equals(mClassName, other.mClassName);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + mLayoutResId;
result = 31 * result + mWidgetLayoutResId;
result = 31 * result + mClassName.hashCode();
return result;
}
}
}