public class

PreferenceGroupAdapter

extends RecyclerView.Adapter<PreferenceViewHolder>

implements androidx.preference.Preference.OnPreferenceChangeInternalListener, PreferenceGroup.PreferencePositionCallback

 java.lang.Object

androidx.recyclerview.widget.RecyclerView.Adapter<PreferenceViewHolder>

↳androidx.preference.PreferenceGroupAdapter

Gradle dependencies

compile group: 'androidx.preference', name: 'preference', version: '1.2.1'

  • groupId: androidx.preference
  • artifactId: preference
  • version: 1.2.1

Artifact androidx.preference:preference:1.2.1 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.preference:preference com.android.support:preference-v7

Androidx class mapping:

androidx.preference.PreferenceGroupAdapter android.support.v7.preference.PreferenceGroupAdapter

Overview

An adapter that connects a RecyclerView to the Preferences contained in an associated PreferenceGroup. Used by Settings.

Summary

Constructors
publicPreferenceGroupAdapter(PreferenceGroup preferenceGroup)

Methods
public PreferencegetItem(int position)

Returns the Preference at the given position

public abstract intgetItemCount()

Returns the total number of items in the data set held by the adapter.

public longgetItemId(int position)

Return the stable ID for the item at position.

public intgetItemViewType(int position)

Return the view type of the item at position for the purposes of view recycling.

public intgetPreferenceAdapterPosition(Preference preference)

public intgetPreferenceAdapterPosition(java.lang.String key)

public abstract voidonBindViewHolder(RecyclerView.ViewHolder holder, int position)

Called by RecyclerView to display the data at the specified position.

public abstract RecyclerView.ViewHolderonCreateViewHolder(ViewGroup parent, int viewType)

Called when RecyclerView needs a new RecyclerView.ViewHolder of the given type to represent an item.

public voidonPreferenceChange(Preference preference)

public voidonPreferenceHierarchyChange(Preference preference)

public voidonPreferenceVisibilityChange(Preference preference)

from RecyclerView.Adapter<VH>bindViewHolder, createViewHolder, findRelativeAdapterPositionIn, getStateRestorationPolicy, hasObservers, hasStableIds, notifyDataSetChanged, notifyItemChanged, notifyItemChanged, notifyItemInserted, notifyItemMoved, notifyItemRangeChanged, notifyItemRangeChanged, notifyItemRangeInserted, notifyItemRangeRemoved, notifyItemRemoved, onAttachedToRecyclerView, onBindViewHolder, onDetachedFromRecyclerView, onFailedToRecycleView, onViewAttachedToWindow, onViewDetachedFromWindow, onViewRecycled, registerAdapterDataObserver, setHasStableIds, setStateRestorationPolicy, unregisterAdapterDataObserver
from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public PreferenceGroupAdapter(PreferenceGroup preferenceGroup)

Methods

public Preference getItem(int position)

Returns the Preference at the given position

Parameters:

position: The position of the Preference in the list displayed to the user

Returns:

The corresponding Preference, or null if the given position is out of bounds

public abstract int getItemCount()

Returns the total number of items in the data set held by the adapter.

Returns:

The total number of items in this adapter.

public long getItemId(int position)

Return the stable ID for the item at position. If RecyclerView.Adapter.hasStableIds() would return false this method should return RecyclerView.NO_ID. The default implementation of this method returns RecyclerView.NO_ID.

Parameters:

position: Adapter position to query

Returns:

the stable ID of the item at position

public void onPreferenceChange(Preference preference)

public void onPreferenceHierarchyChange(Preference preference)

public void onPreferenceVisibilityChange(Preference preference)

public int getItemViewType(int position)

Return the view type of the item at position for the purposes of view recycling.

The default implementation of this method returns 0, making the assumption of a single view type for the adapter. Unlike ListView adapters, types need not be contiguous. Consider using id resources to uniquely identify item view types.

Parameters:

position: position to query

Returns:

integer value identifying the type of the view needed to represent the item at position. Type codes need not be contiguous.

public abstract RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)

Called when RecyclerView needs a new RecyclerView.ViewHolder of the given type to represent an item.

This new ViewHolder should be constructed with a new View that can represent the items of the given type. You can either create a new View manually or inflate it from an XML layout file.

The new ViewHolder will be used to display items of the adapter using RecyclerView.Adapter. Since it will be re-used to display different items in the data set, it is a good idea to cache references to sub views of the View to avoid unnecessary View calls.

Parameters:

parent: The ViewGroup into which the new View will be added after it is bound to an adapter position.
viewType: The view type of the new View.

Returns:

A new ViewHolder that holds a View of the given view type.

See also: RecyclerView.Adapter.getItemViewType(int), RecyclerView.Adapter

public abstract void onBindViewHolder(RecyclerView.ViewHolder holder, int position)

Called by RecyclerView to display the data at the specified position. This method should update the contents of the RecyclerView.ViewHolder.itemView to reflect the item at the given position.

Note that unlike , RecyclerView will not call this method again if the position of the item changes in the data set unless the item itself is invalidated or the new position cannot be determined. For this reason, you should only use the position parameter while acquiring the related data item inside this method and should not keep a copy of it. If you need the position of an item later on (e.g. in a click listener), use RecyclerView.ViewHolder.getBindingAdapterPosition() which will have the updated adapter position. Override RecyclerView.Adapter instead if Adapter can handle efficient partial bind.

Parameters:

holder: The ViewHolder which should be updated to represent the contents of the item at the given position in the data set.
position: The position of the item within the adapter's data set.

public int getPreferenceAdapterPosition(java.lang.String key)

public int getPreferenceAdapterPosition(Preference preference)

Source

/*
 * 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.os.Looper;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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 final 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 final List<PreferenceResourceDescriptor> mPreferenceResourceDescriptors;

    private final Handler mHandler;

    private final Runnable mSyncRunnable = new Runnable() {
        @Override
        public void run() {
            updatePreferences();
        }
    };

    public PreferenceGroupAdapter(@NonNull PreferenceGroup preferenceGroup) {
        mPreferenceGroup = preferenceGroup;
        mHandler = new Handler(Looper.getMainLooper());

        // 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(@NonNull 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
     */
    @Nullable
    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(@NonNull 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(@NonNull Preference preference) {
        mHandler.removeCallbacks(mSyncRunnable);
        mHandler.post(mSyncRunnable);
    }

    @Override
    public void onPreferenceVisibilityChange(@NonNull 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(@NonNull 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(@NonNull 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(@NonNull 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;
        }
    }
}