android framework13-settings[02 PreferenceFragmentCompat]

1,227 阅读23分钟

1.简介

设置里的选项卡页面,用的基本都是这个Fragment,平时接触少,所以这里研究下。 这里主要看下数据的加载,选项的排序问题

2.PreferenceFragmentCompat

通过PreferenceManager管理所有的preference数据,这个Fragment里有个recyclerView,用来显示列表。

2.1.onCreate

public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
//..
    mPreferenceManager = new PreferenceManager(requireContext());
    mPreferenceManager.setOnNavigateToScreenListener(this);
    final Bundle args = getArguments();
    //rootKey 外部传递的,一般为null
    if (args != null) {
        rootKey = getArguments().getString(ARG_PREFERENCE_ROOT);
    } else {
        rootKey = null;
    }
    //我们一般在这个抽象方法里加载xml
    onCreatePreferences(savedInstanceState, rootKey);
}

2.1.onCreateView

我们的fragment如果没有设置layoutResId,这里是有默认值的

//布局id是有默认值的
private int mLayoutResId = R.layout.preference_list_fragment;

public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
        @Nullable Bundle savedInstanceState) {
    //获取布局id,
    mLayoutResId = a.getResourceId(R.styleable.PreferenceFragmentCompat_android_layout,
            mLayoutResId);
    //可以看到,还可以自定义分割线,分割线高度,以及最后一个item是否允许divider
    final Drawable divider = a.getDrawable(
            R.styleable.PreferenceFragmentCompat_android_divider);
    final int dividerHeight = a.getDimensionPixelSize(
            R.styleable.PreferenceFragmentCompat_android_dividerHeight, -1);
    final boolean allowDividerAfterLastItem = a.getBoolean(
            R.styleable.PreferenceFragmentCompat_allowDividerAfterLastItem, true);

    a.recycle();

    final LayoutInflater themedInflater = inflater.cloneInContext(requireContext());
//加载布局
    final View view = themedInflater.inflate(mLayoutResId, container, false);
   //如果是自定义的layout,必须有个id为下边这个的容器,用来添加recyclerview的 
    final View rawListContainer = view.findViewById(AndroidResources.ANDROID_R_LIST_CONTAINER);
    if (!(rawListContainer instanceof ViewGroup)) {
        throw new IllegalStateException("Content has view with id attribute "
                + "'android.R.id.list_container' that is not a ViewGroup class");
    }

    final ViewGroup listContainer = (ViewGroup) rawListContainer;
    //创建一个rv
    final RecyclerView listView = onCreateRecyclerView(themedInflater, listContainer,
            savedInstanceState);
    if (listView == null) {
        throw new RuntimeException("Could not create RecyclerView");
    }

    mList = listView;
    //下边这堆都是divider相关的设置
    listView.addItemDecoration(mDividerDecoration);
    setDivider(divider);
    if (dividerHeight != -1) {
        setDividerHeight(dividerHeight);
    }
    mDividerDecoration.setAllowDividerAfterLastItem(allowDividerAfterLastItem);

    //把rv添加到容器里
    if (mList.getParent() == null) {
        listContainer.addView(mList);
    }
    mHandler.post(mRequestFocus);

    return view;
}

2.2.onPreferenceTreeClick

选项的点击事件处理,在onStart里注册监听,onPause里取消监听

public void onStart() {
    super.onStart();
    mPreferenceManager.setOnPreferenceTreeClickListener(this);
    mPreferenceManager.setOnDisplayPreferenceDialogListener(this);
}

触发条件是preference的performClick方法,后边再看

public boolean onPreferenceTreeClick(@NonNull Preference preference) {
    //有设置要跳转的fragment属性,就是xml里有 android:fragment="xxx" 这样的属性
    if (preference.getFragment() != null) {
        boolean handled = false;
        //先找下有没有自定义的处理类
        if (getCallbackFragment() instanceof OnPreferenceStartFragmentCallback) {
            handled = ((OnPreferenceStartFragmentCallback) getCallbackFragment())
                    .onPreferenceStartFragment(this, preference);
        }
        //上边没处理,下边看下自己能否处理,不能的话循环找上层的fragment看能否处理 
        Fragment callbackFragment = this;
        while (!handled && callbackFragment != null) {
            if (callbackFragment instanceof OnPreferenceStartFragmentCallback) {
                handled = ((OnPreferenceStartFragmentCallback) callbackFragment)
                        .onPreferenceStartFragment(this, preference);
            }
            callbackFragment = callbackFragment.getParentFragment();
        }
        //fragment查完还是没处理,这里再查下context有没有实现这个接口
        if (!handled && getContext() instanceof OnPreferenceStartFragmentCallback) {
            handled = ((OnPreferenceStartFragmentCallback) getContext())
                    .onPreferenceStartFragment(this, preference);
        }
        // 继续查activity
        if (!handled && getActivity() instanceof OnPreferenceStartFragmentCallback) {
            handled = ((OnPreferenceStartFragmentCallback) getActivity())
                    .onPreferenceStartFragment(this, preference);
        }
        if (!handled) {
        //都没人处理,那么获取preference里的fragment,实例化,并跳转到这个fragment
            final FragmentManager fragmentManager = getParentFragmentManager();
            final Bundle args = preference.getExtras();
            final Fragment fragment = fragmentManager.getFragmentFactory().instantiate(
                    requireActivity().getClassLoader(), preference.getFragment());
            fragment.setArguments(args);
            fragment.setTargetFragment(this, 0);
            fragmentManager.beginTransaction()
                    .replace(((View) requireView().getParent()).getId(), fragment)
                    .addToBackStack(null)
                    .commit();
        }
        return true;
    }
    return false;
}

>getCallbackFragment

默认返回是null,

public Fragment getCallbackFragment() {
    return null;
}

默认也没有实现OnPreferenceStartFragmentCallback这个接口

public abstract class PreferenceFragmentCompat extends Fragment implements
        PreferenceManager.OnPreferenceTreeClickListener,
        PreferenceManager.OnDisplayPreferenceDialogListener,
        PreferenceManager.OnNavigateToScreenListener,
        DialogPreference.TargetFragment {

2.3.addPreferencesFromResource

这种是把xml里解析的Preference添加到已有的preferenceScreen里(如果有的话),没有的话那就添加xml里的preferenceScreen

public void addPreferencesFromResource(@XmlRes int preferencesResId) {
    requirePreferenceManager();
    //inflate逻辑见 8.1
    setPreferenceScreen(mPreferenceManager.inflateFromResource(requireContext(),
            preferencesResId, getPreferenceScreen()));
}

2.4.setPreferencesFromResource

一般第二个参数都传的null,

public void setPreferencesFromResource(@XmlRes int preferencesResId, @Nullable String key) {
    requirePreferenceManager();

    final PreferenceScreen xmlRoot = mPreferenceManager.inflateFromResource(requireContext(),
            preferencesResId, null);

    final Preference root;
    if (key != null) {
        root = xmlRoot.findPreference(key);
        //这里给的key查找的必须是PreferenceScreen,否则异常
        if (!(root instanceof PreferenceScreen)) {
            throw new IllegalArgumentException("Preference object with key " + key
                    + " is not a PreferenceScreen");
        }
    } else {
    //设置页面的root为新解析出来的screen
        root = xmlRoot;
    }

    setPreferenceScreen((PreferenceScreen) root);
}

>setPreferenceScreen

这种是把旧的释放掉,替换成新的

public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
    if (mPreferenceManager.setPreferences(preferenceScreen) && preferenceScreen != null) {
        onUnbindPreferences();
        mHavePrefs = true;
        if (mInitDone) {
        //参看2.6
            postBindPreferences();
        }
    }
}

2.5.onNavigateToScreen

同样是找到可以处理的callback,这个一般不会走的,具体逻辑看 7.1,正常screen里边不可能没有东西。

public void onNavigateToScreen(@NonNull PreferenceScreen preferenceScreen) {
    boolean handled = false;
    if (getCallbackFragment() instanceof OnPreferenceStartScreenCallback) {
        handled = ((OnPreferenceStartScreenCallback) getCallbackFragment())
                .onPreferenceStartScreen(this, preferenceScreen);
    }
    //  If the callback fragment doesn't handle OnPreferenceStartScreenCallback, looks up
    //  its parent fragment in the hierarchy that implements the callback until the first
    //  one that returns true
    Fragment callbackFragment = this;
    while (!handled && callbackFragment != null) {
        if (callbackFragment instanceof OnPreferenceStartScreenCallback) {
            handled = ((OnPreferenceStartScreenCallback) callbackFragment)
                    .onPreferenceStartScreen(this, preferenceScreen);
        }
        callbackFragment = callbackFragment.getParentFragment();
    }
    if (!handled && getContext() instanceof OnPreferenceStartScreenCallback) {
        handled = ((OnPreferenceStartScreenCallback) getContext())
                .onPreferenceStartScreen(this, preferenceScreen);
    }
    // Check the Activity as well in case getContext was overridden to return something other
    // than the Activity.
    if (!handled && getActivity() instanceof OnPreferenceStartScreenCallback) {
        ((OnPreferenceStartScreenCallback) getActivity())
                .onPreferenceStartScreen(this, preferenceScreen);
    }
}

2.6.bindPreferences

就是把preference数据给到rv

private void postBindPreferences() {
    if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return;
    mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget();
}

//上边的handler会执行这个方法
void bindPreferences() {
    final PreferenceScreen preferenceScreen = getPreferenceScreen();
    if (preferenceScreen != null) {
    //rv设置adapter
        getListView().setAdapter(onCreateAdapter(preferenceScreen));
        preferenceScreen.onAttached();
    }
    onBindPreferences();
}

private void unbindPreferences() {
    getListView().setAdapter(null);
    final PreferenceScreen preferenceScreen = getPreferenceScreen();
    if (preferenceScreen != null) {
        preferenceScreen.onDetached();
    }
    onUnbindPreferences();
}

可以看到,每次都是new一个新的adapter

protected RecyclerView.Adapter onCreateAdapter(@NonNull PreferenceScreen preferenceScreen) {
    return new PreferenceGroupAdapter(preferenceScreen);
}

2.7.scrollToPreference

滚动到某个偏好的位置

public void scrollToPreference(@NonNull String key) {
    scrollToPreferenceInternal(null, key);
}

public void scrollToPreference(@NonNull Preference preference) {
    scrollToPreferenceInternal(preference, null);
}

private void scrollToPreferenceInternal(@Nullable final Preference preference,
        @Nullable final String key) {
//找到preference的位置,调用rv的滚动方法
if (position != RecyclerView.NO_POSITION) {
    mList.scrollToPosition(position);
} else {
    // Item not found, wait for an update and try again
    adapter.registerAdapterDataObserver(
            new ScrollToPreferenceObserver(adapter, mList, preference, key));
}

2.8.onDisplayPreferenceDialog

  • 这个是调用 getPreferenceManager().showDialog(this); 才会走这里的。
  • 默认的实现了DialogPreference的点击会走这里,见3.1
public void onDisplayPreferenceDialog(@NonNull Preference preference) {
    
    boolean handled = false;
    //找到相关的callback处理
    if (getCallbackFragment() instanceof OnPreferenceDisplayDialogCallback) {
        handled = ((OnPreferenceDisplayDialogCallback) getCallbackFragment())
                .onPreferenceDisplayDialog(this, preference);
    }
    Fragment callbackFragment = this;
    while (!handled && callbackFragment != null) {
        if (callbackFragment instanceof OnPreferenceDisplayDialogCallback) {
            handled = ((OnPreferenceDisplayDialogCallback) callbackFragment)
                    .onPreferenceDisplayDialog(this, preference);
        }
        callbackFragment = callbackFragment.getParentFragment();
    }
    if (!handled && getContext() instanceof OnPreferenceDisplayDialogCallback) {
        handled = ((OnPreferenceDisplayDialogCallback) getContext())
                .onPreferenceDisplayDialog(this, preference);
    }
    // Check the Activity as well in case getContext was overridden to return something other
    // than the Activity.
    if (!handled && getActivity() instanceof OnPreferenceDisplayDialogCallback) {
        handled = ((OnPreferenceDisplayDialogCallback) getActivity())
                .onPreferenceDisplayDialog(this, preference);
    }
    //自己处理了,那就返回
    if (handled) {
        return;
    }

    //判断dialog是否已经显示
    if (getParentFragmentManager().findFragmentByTag(DIALOG_FRAGMENT_TAG) != null) {
        return;
    }

    final DialogFragment f;
    下边3种preference 可以自动弹出对话框
    if (preference instanceof EditTextPreference) {
        f = EditTextPreferenceDialogFragmentCompat.newInstance(preference.getKey());
    } else if (preference instanceof ListPreference) {
        f = ListPreferenceDialogFragmentCompat.newInstance(preference.getKey());
    } else if (preference instanceof MultiSelectListPreference) {
        f = MultiSelectListPreferenceDialogFragmentCompat.newInstance(preference.getKey());
    } else {
        throw new IllegalArgumentException(
    }
    f.setTargetFragment(this, 0);
    f.show(getParentFragmentManager(), DIALOG_FRAGMENT_TAG);
}

2.9.默认的主题

  • 材料库下的,不同版本可能有差异
  • allowDividerAfterLastItem:就是最后一个选项底部是否有下划线
    <style name="PreferenceFragment.Material">
        <item name="android:divider">@drawable/preference_list_divider_material</item>
        <item name="allowDividerAfterLastItem">false</item>
    </style>

然后可以自己在theme.xml里重写下

    <style name="PreferenceFragment.Material">
        <item name="android:divider">@drawable/divider_normal_gray</item>
        <item name="android:dividerHeight">5dp</item>
        <item name="allowDividerAfterLastItem">true</item>
    </style>

3.常见的Preference

暂时就学习了继承DialogPreference的3种,就是点击会弹个对话框

  • EditTextPreference:弹框里带个编辑框,保存的就是这个值
  • ListPreference:单选
  • MultiSelectListPreference :多选
  • 还有上述3种preference对应的dialogFragment学习

3.0.常见容器继承关系

Preference基础构建单元

public class Preference implements Comparable<Preference> {

Preference容器,抽象的

public abstract class PreferenceGroup extends Preference {

Preference种类

public class PreferenceCategory extends PreferenceGroup {

Preference根容器

public final class PreferenceScreen extends PreferenceGroup {

如下图,蓝色的小字就是PreferenceCategory ,其他黑色的就是Preference image.png

3.1.DialogPreference

>支持的属性

就是对话框的标题,图标,message,按钮文字,编辑框自定义的layout

<declare-styleable name="DialogPreference">
    
    <attr format="string" name="dialogTitle"/>
    <attr name="android:dialogTitle"/>
    
    <attr format="string" name="dialogMessage"/>
    <attr name="android:dialogMessage"/>
   
    <attr format="reference" name="dialogIcon"/>
    <attr name="android:dialogIcon"/>
    
    <attr format="string" name="positiveButtonText"/>
    <attr name="android:positiveButtonText"/>
    
    <attr format="string" name="negativeButtonText"/>
    <attr name="android:negativeButtonText"/>
    <!-- 这个其实就是那个文本编辑框,主题里有默认值的,所以一般除非自定义,否则不需要-->
    <attr format="reference" name="dialogLayout"/>
    <attr name="android:dialogLayout"/>
</declare-styleable>

>onClick

protected void onClick() {
    getPreferenceManager().showDialog(this);
}

//下边的listener是fragment里设置的,具体看 2.8

public void showDialog(@NonNull Preference preference) {
    if (mOnDisplayPreferenceDialogListener != null) {
        mOnDisplayPreferenceDialogListener.onDisplayPreferenceDialog(preference);
    }
}

3.2.EditTextPreference

点击的时候会弹个带文本编辑的对话框。它本身会保存一个string值,默认编辑框里显示的就是这个值,可以修改

public class EditTextPreference extends DialogPreference {
//默认的style里有设置dialogLayout的值
public EditTextPreference(@NonNull Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, TypedArrayUtils.getAttr(context, R.attr.editTextPreferenceStyle,
            android.R.attr.editTextPreferenceStyle));
}

>setText

可以手动调用这个方法,修改选项默认的保存值

public void setText(@Nullable String text) {
    final boolean wasBlocking = shouldDisableDependents();

    mText = text;

    persistString(text);

    final boolean isBlocking = shouldDisableDependents();
    if (isBlocking != wasBlocking) {
        notifyDependencyChange(isBlocking);
    }

    notifyChanged();
}

>onSetInitialValue

可以看到,在preference初始化的时候会调用settext给text赋值

protected void onSetInitialValue(Object defaultValue) {
    setText(getPersistedString((String) defaultValue));
}

3.2.1.EditTextPreferenceDialogFragmentCompat

参考2.8 可以知道显示的对话框代码如下

EditTextPreferenceDialogFragmentCompat.newInstance(preference.getKey());

image.png

>onCreate

先拿到preference里的text值

public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (savedInstanceState == null) {
        mText = getEditTextPreference().getText();
    } else {
        mText = savedInstanceState.getCharSequence(SAVE_STATE_TEXT);
    }
}

>onBindDialogView

这里就是把text值设置给文本编辑器

protected void onBindDialogView(@NonNull View view) {
    super.onBindDialogView(view);

    mEditText = view.findViewById(android.R.id.edit);

    if (mEditText == null) {
        throw new IllegalStateException("Dialog view must contain an EditText with id" +
                " @android:id/edit");
    }

    mEditText.requestFocus();
    mEditText.setText(mText);
    // Place cursor at the end
    mEditText.setSelection(mEditText.getText().length());
    if (getEditTextPreference().getOnBindEditTextListener() != null) {
        getEditTextPreference().getOnBindEditTextListener().onBindEditText(mEditText);
    }
}

>onDialogClosed

public void onDialogClosed(boolean positiveResult) {
    if (positiveResult) {
    //点击的是ok按钮,保存文本编辑框里的内容
        String value = mEditText.getText().toString();
        final EditTextPreference preference = getEditTextPreference();
        if (preference.callChangeListener(value)) {
        //保存数据,参看3.2
            preference.setText(value);
        }
    }
}

// 默认的listener是null的

public boolean callChangeListener(Object newValue) {
    return mOnChangeListener == null || mOnChangeListener.onPreferenceChange(this, newValue);
}

看下父类的实现,可以知道,点击ok按钮的话,上边的参数为true

public void onClick(@NonNull DialogInterface dialog, int which) {
    mWhichButtonClicked = which;
}

@Override
public void onDismiss(@NonNull DialogInterface dialog) {
    super.onDismiss(dialog);
    onDialogClosed(mWhichButtonClicked == DialogInterface.BUTTON_POSITIVE);
}

>onCreateDialog

这是父类的方法,简单看下对话框的ui结构

Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
    mWhichButtonClicked = DialogInterface.BUTTON_NEGATIVE;
    //设置icon,title,和2个按钮
    final AlertDialog.Builder builder = new AlertDialog.Builder(requireContext())
            .setTitle(mDialogTitle)
            .setIcon(mDialogIcon)
            .setPositiveButton(mPositiveButtonText, this)
            .setNegativeButton(mNegativeButtonText, this);

    View contentView = onCreateDialogView(requireContext());
    if (contentView != null) {
    //默认的话,主题里有布局,所以会走这里,布局里带一个message文本框,以及一个文本编辑框
        onBindDialogView(contentView);//这个根据message为null与否,决定是否显示message文本框
        builder.setView(contentView);
    } else {
        builder.setMessage(mDialogMessage);
    }

    onPrepareDialogBuilder(builder);

    // Create the dialog
    final Dialog dialog = builder.create();
    if (needInputMethod()) {
    //需要弹输入法的话,直接弹
        requestInputMethod(dialog);
    }

    return dialog;
}

3.3.ListPreference

同样是点击以后弹个对话框,不过这次是显示一个列表供选择,如图

public class ListPreference extends DialogPreference {

public ListPreference(@NonNull Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, TypedArrayUtils.getAttr(context, R.attr.dialogPreferenceStyle,
            android.R.attr.dialogPreferenceStyle));
}

image.png

>核心属性

  • entries,字符串数组,用来显示的
  • entryValues,字符串数组,用来存储的,根据key,存储在sp里或者dataStore
  • defaultValue ,entryValues数组里的一个值
  • summary ,这个需要注意,这里是个格式化的字符串,用来显示我们选中的条目的,具体看后边的代码分析
<ListPreference
    android:defaultValue="0"
    android:entries="@array/sex"
    android:entryValues="@array/sexValue"
    android:key="mylist"
    android:summary="%s"
    android:title="性别" />

比如

<string-array name="sex">
    <item></item>
    <item></item>
</string-array>
<string-array name="sexValue">
    <item>1</item>
    <item>0</item>
</string-array>

>getSummary

  • mSummary 这玩意就是从android:summary读取的,或者手动调用setSummary方法设置的
  • summary super.getSummary()点进去可以看到,先获取provider提供的,没有provider,那就返回上边的mSummary
public CharSequence getSummary() {
    if (getSummaryProvider() != null) {
        return getSummaryProvider().provideSummary(this);
    }
    final CharSequence entry = getEntry();
    CharSequence summary = super.getSummary();
    //可以看到,如果mSummary为null,就返回summary了
    if (mSummary == null) {
        return summary;
    }
    //看到没,这里是格式化 mSummary 的值,
    String formattedString = String.format(mSummary, entry == null ? "" : entry);
    if (TextUtils.equals(formattedString, summary)) {
        return summary;
    }
    //注意这里的提示,推荐用provider,不推荐用格式化的字符串
    Log.w(TAG,
            "Setting a summary with a String formatting marker is no longer supported."
                    + " You should use a SummaryProvider instead.");
    return formattedString;
}

>SimpleSummaryProvider

这里看下provider吧,未来可能就得用这种了

默认是没有的

if (TypedArrayUtils.getBoolean(a, R.styleable.ListPreference_useSimpleSummaryProvider,
        R.styleable.ListPreference_useSimpleSummaryProvider, false)) {
    setSummaryProvider(SimpleSummaryProvider.getInstance());
}

可以手动设置

public final void setSummaryProvider(@Nullable SummaryProvider summaryProvider) {
    mSummaryProvider = summaryProvider;
    notifyChanged();
}

看下默认的实现

public static final class SimpleSummaryProvider implements SummaryProvider<ListPreference> {
//..
public CharSequence provideSummary(@NonNull ListPreference preference) {
    if (TextUtils.isEmpty(preference.getEntry())) {
    //没有选中的条目的话,返回 not set
        return (preference.getContext().getString(R.string.not_set));
    } else {
    //有的话,返回条目
        return preference.getEntry();
    }
}
>>SummaryProvider
public interface SummaryProvider<T extends Preference> {
    @Nullable
    CharSequence provideSummary(@NonNull T preference);
}

3.3.1.ListPreferenceDialogFragmentCompat

弹框效果见 3.3

public class ListPreferenceDialogFragmentCompat extends PreferenceDialogFragmentCompat {

>onCreate

获取2个数组以及当前选中的位置

public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (savedInstanceState == null) {
        final ListPreference preference = getListPreference();

        if (preference.getEntries() == null || preference.getEntryValues() == null) {
            throw new IllegalStateException(//
        }

        mClickedDialogEntryIndex = preference.findIndexOfValue(preference.getValue());
        mEntries = preference.getEntries();
        mEntryValues = preference.getEntryValues();
    } else {//..
    }
}

>onPrepareDialogBuilder

protected void onPrepareDialogBuilder(@NonNull AlertDialog.Builder builder) {
    super.onPrepareDialogBuilder(builder);
    //设置单选列表
    builder.setSingleChoiceItems(mEntries, mClickedDialogEntryIndex,
            new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    mClickedDialogEntryIndex = which;

                    // 点击某个条目,模拟ok按钮,方便处理结果
                    ListPreferenceDialogFragmentCompat.this.onClick(dialog,
                            DialogInterface.BUTTON_POSITIVE);
                    dialog.dismiss();
                }
            });

    // 不显示ok按钮
    builder.setPositiveButton(null, null);
}

>onDialogClosed

存储的是mEntryValues的值

public void onDialogClosed(boolean positiveResult) {
    if (positiveResult && mClickedDialogEntryIndex >= 0) {
        String value = mEntryValues[mClickedDialogEntryIndex].toString();
        final ListPreference preference = getListPreference();
        if (preference.callChangeListener(value)) {
            preference.setValue(value);
        }
    }
}

3.4.MultiSelectListPreference

这个点击弹个多选框

public class MultiSelectListPreference extends DialogPreference {

>相关参数

private CharSequence[] mEntries;//多选条目
private CharSequence[] mEntryValues;//多选条目对应的value
private Set<String> mValues = new HashSet<>();//选中的条目value集合

>onGetDefaultValue

这里重写了默认值的获取逻辑,默认值需要是个数组,我们转化成了Set存储

protected @Nullable Object onGetDefaultValue(@NonNull TypedArray a, int index) {
    final CharSequence[] defaultValues = a.getTextArray(index);
    final Set<String> result = new HashSet<>();

    for (final CharSequence defaultValue : defaultValues) {
        result.add(defaultValue.toString());
    }

    return result;
}


protected void onSetInitialValue(Object defaultValue) {
    setValues(getPersistedStringSet((Set<String>) defaultValue));
}

>getValues

下边是选中的值

public void setValues(Set<String> values) {
    mValues.clear();
    mValues.addAll(values);

    persistStringSet(values);
    notifyChanged();
}


public Set<String> getValues() {
    return mValues;
}

>summary显示选中的内容

举例如下,entry和entry value值不一样,我们存储的是value值,所以显示的时候还得转化一下,如果两种值一样,那就不用转化了。

<string-array name="colors">
    <item>red</item>
    <item>blue</item>
    <item>yellow</item>
    <item>green</item>
</string-array>
<string-array name="colorsValue">
    <item>0</item>
    <item>1</item>
    <item>2</item>
    <item>3</item>
</string-array>

//

preferenceScreen.findPreference<MultiSelectListPreference>("loveColors")?.apply {
    updateSummary(this, values)

    this.setOnPreferenceChangeListener { preference, newValue ->
        updateSummary(this, newValue as Set<String>)
        true
    }
}
fun updateSummary(preference: MultiSelectListPreference, values: Set<String>) {
    preference.summary = values.joinToString { preference.entries[preference.findIndexOfValue(it)] }
}

3.4.1.MultiSelectListPreferenceDialogFragmentCompat

public class MultiSelectListPreferenceDialogFragmentCompat extends PreferenceDialogFragmentCompat {

image.png

>onPrepareDialogBuilder

就是系统多选对话框的逻辑,没啥看的

protected void onPrepareDialogBuilder(@NonNull AlertDialog.Builder builder) {
    super.onPrepareDialogBuilder(builder);

    final int entryCount = mEntryValues.length;
    final boolean[] checkedItems = new boolean[entryCount];
    for (int i = 0; i < entryCount; i++) {
        checkedItems[i] = mNewValues.contains(mEntryValues[i].toString());
    }
    builder.setMultiChoiceItems(mEntries, checkedItems,
            new DialogInterface.OnMultiChoiceClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which, boolean isChecked) {
                    if (isChecked) {
                        mPreferenceChanged |= mNewValues.add(
                                mEntryValues[which].toString());
                    } else {
                        mPreferenceChanged |= mNewValues.remove(
                                mEntryValues[which].toString());
                    }
                }
            });
}

4.Preference.java

可比较的,比较规则见 4.2

public class Preference implements Comparable<Preference> {

4.1.performClick

public void performClick() {
    //不可用,不可以选中
    if (!isEnabled() || !isSelectable()) {
        return;
    }
    
    onClick();
    //有设置点击事件并处理的
    if (mOnClickListener != null && mOnClickListener.onPreferenceClick(this)) {
        return;
    }

    PreferenceManager preferenceManager = getPreferenceManager();
    if (preferenceManager != null) {
        PreferenceManager.OnPreferenceTreeClickListener listener = preferenceManager
                .getOnPreferenceTreeClickListener();
       //这个就交给2.2那边处理了        
        if (listener != null && listener.onPreferenceTreeClick(this)) {
            return;
        }
    }
    //最后判断有没有intent,有的话直接跳转intent
    if (mIntent != null) {
        Context context = getContext();
        context.startActivity(mIntent);
    }
}

设置点击事件

public void setOnPreferenceClickListener(
        @Nullable OnPreferenceClickListener onPreferenceClickListener) {
    mOnClickListener = onPreferenceClickListener;
}

4.2.compareTo

  • 先比较order
  • order一样,判断title是否一样,一样的话,位置不变
  • 自己title为null,返回1,往后排, 后者title为null,返回-1,往前移
  • order一样,title不空,则比较title字符串。
public int compareTo(@NonNull Preference another) {
    if (mOrder != another.mOrder) {
        // Do order comparison
        return mOrder - another.mOrder;
    } else if (mTitle == another.mTitle) {
        // If titles are null or share same object comparison
        return 0;
    } else if (mTitle == null) {
        return 1;
    } else if (another.mTitle == null) {
        return -1;
    } else {
        // Do name comparison
        return mTitle.toString().compareToIgnoreCase(another.mTitle.toString());
    }
}

4.3.支持的属性

<declare-styleable name="Preference">
    <!-- The optional icon for the preference -->
    <attr name="icon"/>
    <attr name="android:icon"/>
    <!-- The key to store the Preference value. -->
    <attr format="string" name="key"/>
    <attr name="android:key"/>
    <!-- The title for the Preference in a PreferenceActivity screen. -->
    <attr name="title"/>
    <attr name="android:title"/>
    <!-- The summary for the Preference in a PreferenceActivity screen. -->
    <attr format="string" name="summary"/>
    <attr name="android:summary"/>
    <!-- The order for the Preference (lower values are to be ordered first). If this is not
         specified, the default ordering will be alphabetic. -->
    <attr format="integer" name="order"/>
    <attr name="android:order"/>
    <!-- When used inside of a modern PreferenceActivity, this declares
         a new PreferenceFragment to be shown when the user selects this item. -->
    <attr format="string" name="fragment"/>
    <attr name="android:fragment"/>
    <!-- The layout for the Preference in a PreferenceActivity screen. This should
         rarely need to be changed, look at widgetLayout instead. -->
    <attr name="layout"/>
    <attr name="android:layout"/>
    <!-- The layout for the controllable widget portion of a Preference. This is inflated
         into the layout for a Preference and should be used more frequently than
         the layout attribute. For example, a checkbox preference would specify
         a custom layout (consisting of just the CheckBox) here. -->
    <attr format="reference" name="widgetLayout"/>
    <attr name="android:widgetLayout"/>
    <!-- Whether the Preference is enabled. -->
    <attr format="boolean" name="enabled"/>
    <attr name="android:enabled"/>
    <!-- Whether the Preference is selectable. -->
    <attr format="boolean" name="selectable"/>
    <attr name="android:selectable"/>
    <!-- The key of another Preference that this Preference will depend on.  If the other
         Preference is not set or is off, this Preference will be disabled. -->
    <attr format="string" name="dependency"/>
    <attr name="android:dependency"/>
    <!-- Whether the Preference stores its value to the shared preferences. -->
    <attr format="boolean" name="persistent"/>
    <attr name="android:persistent"/>
    <!-- The default value for the preference, which will be set either if persistence
         is off or persistence is on and the preference is not found in the persistent
         storage.  -->
    <attr format="string|boolean|integer|reference|float" name="defaultValue"/>
    <attr name="android:defaultValue"/>
    <!-- Whether the view of this Preference should be disabled when
         this Preference is disabled. -->
    <attr format="boolean" name="shouldDisableView"/>
    <attr name="android:shouldDisableView"/>

    <!-- Whether the preference allows displaying divider on top -->
    <attr format="boolean" name="allowDividerAbove"/>

    <!-- Whether the preference allows displaying divider below it -->
    <attr format="boolean" name="allowDividerBelow"/>

    <!-- Whether to use single line for the preference title text. By default, preference title
         will be constrained to one line, so the default value of this attribute is true. -->
    <attr format="boolean" name="singleLineTitle"/>
    <attr name="android:singleLineTitle"/>

    <!-- Whether the space for the preference icon view will be reserved. If set to true, the
         preference will be offset as if it would have the icon and thus aligned with other
         preferences having icons. By default, preference icon view visibility will be set to
         GONE when there is no icon provided, so the default value of this attribute is false.
         -->
    <attr format="boolean" name="iconSpaceReserved"/>
    <attr name="android:iconSpaceReserved"/>

    <!-- Whether the Preference is visible. By default, this is set to true. -->
    <attr format="boolean" name="isPreferenceVisible"/>

    <!-- Whether the summary of this preference can be copied to the clipboard by long pressing
         on the preference. By default, this is set to false. -->
    <attr format="boolean" name="enableCopying"/>
</declare-styleable>

4.4.onBindViewHolder

private final View.OnClickListener mClickListener = new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    //参考 4.1
        performClick(v);
    }
};
public void onBindViewHolder(@NonNull PreferenceViewHolder holder) {
    View itemView = holder.itemView;
    Integer summaryTextColor = null;
    //item的点击事件设置
    itemView.setOnClickListener(mClickListener);
    itemView.setId(mViewId);

    final TextView summaryView = (TextView) holder.findViewById(android.R.id.summary);
    if (summaryView != null) {
        final CharSequence summary = getSummary();
        if (!TextUtils.isEmpty(summary)) {
        //设置summary以及可见
            summaryView.setText(summary);
            summaryView.setVisibility(View.VISIBLE);
            summaryTextColor = summaryView.getCurrentTextColor();
        } else {
        //为null,不可见
            summaryView.setVisibility(View.GONE);
        }
    }

    final TextView titleView = (TextView) holder.findViewById(android.R.id.title);
    if (titleView != null) {
        final CharSequence title = getTitle();
        if (!TextUtils.isEmpty(title)) {
            titleView.setText(title);
            titleView.setVisibility(View.VISIBLE);
            if (mHasSingleLineTitleAttr) {
                titleView.setSingleLine(mSingleLineTitle);
            }
            // If this Preference is not selectable, but still enabled, we should set the
            // title text colour to the same colour used for the summary text
            if (!isSelectable() && isEnabled() && summaryTextColor != null) {
                titleView.setTextColor(summaryTextColor);
            }
        } else {
            titleView.setVisibility(View.GONE);
        }
    }

    final ImageView imageView = (ImageView) holder.findViewById(android.R.id.icon);
    if (imageView != null) {
        if (mIconResId != 0 || mIcon != null) {
            if (mIcon == null) {
                mIcon = AppCompatResources.getDrawable(mContext, mIconResId);
            }
            if (mIcon != null) {
                imageView.setImageDrawable(mIcon);
            }
        }
        if (mIcon != null) {
            imageView.setVisibility(View.VISIBLE);
        } else {
        //根据是否保留icon位置,设置为不可见或者隐藏
            imageView.setVisibility(mIconSpaceReserved ? View.INVISIBLE : View.GONE);
        }
    }
    //这个是上边icon的容器,
    View imageFrame = holder.findViewById(R.id.icon_frame);
    if (imageFrame == null) {
        imageFrame = holder.findViewById(AndroidResources.ANDROID_R_ICON_FRAME);
    }
    if (imageFrame != null) {
        if (mIcon != null) {
            imageFrame.setVisibility(View.VISIBLE);
        } else {
            imageFrame.setVisibility(mIconSpaceReserved ? View.INVISIBLE : View.GONE);
        }
    }

    if (mShouldDisableView) {
        setEnabledStateOnViews(itemView, isEnabled());
    } else {
        setEnabledStateOnViews(itemView, true);
    }

    final boolean selectable = isSelectable();
    itemView.setFocusable(selectable);
    itemView.setClickable(selectable);

    holder.setDividerAllowedAbove(mAllowDividerAbove);
    holder.setDividerAllowedBelow(mAllowDividerBelow);
    //是否可以复制summary,是的话设置listener,并设置长按事件
    final boolean copyingEnabled = isCopyingEnabled();

    if (copyingEnabled && mOnCopyListener == null) {
        mOnCopyListener = new OnPreferenceCopyListener(this);
    }
    itemView.setOnCreateContextMenuListener(copyingEnabled ? mOnCopyListener : null);
    itemView.setLongClickable(copyingEnabled);

    // Remove touch ripple if copying is enabled and the view isn't selectable. This is
    // needed as enabling copying requires the view to be `clickable`, but we only care about
    // long clicks, and not normal clicks.
    if (copyingEnabled && !selectable) {
        ViewCompat.setBackground(itemView, null);
    }
}

4.5.默认的主题

>1.构造方法

  • 可以看到默认用的主题是preferenceStyle,这个就得看我们应用或者activity的主题里这个值的默认配置了。
public Preference(@NonNull Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, TypedArrayUtils.getAttr(context, R.attr.preferenceStyle,
            android.R.attr.preferenceStyle));
}

public Preference(@NonNull Context context) {
    this(context, null);
}

>2.默认值

  • 如果不需要系统默认的主题设置,我们在自己的主题里添加如下代码,
<item name="preferenceStyle">@null</item>
  • 构造方法里如果主题未设置,那么默认值如下
public Preference(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
        int defStyleRes) {
//...
mEnabled = TypedArrayUtils.getBoolean(a, R.styleable.Preference_enabled,
        R.styleable.Preference_android_enabled, true);

mSelectable = TypedArrayUtils.getBoolean(a, R.styleable.Preference_selectable,
        R.styleable.Preference_android_selectable, true);

//
mAllowDividerAbove = TypedArrayUtils.getBoolean(a, R.styleable.Preference_allowDividerAbove,
        R.styleable.Preference_allowDividerAbove, mSelectable);

mAllowDividerBelow = TypedArrayUtils.getBoolean(a, R.styleable.Preference_allowDividerBelow,
        R.styleable.Preference_allowDividerBelow, mSelectable);

5.PreferenceGroup

5.1.addItemFromInflater

这个是解析xml的时候调用的

public void addItemFromInflater(@NonNull Preference preference) {
    addPreference(preference);
}

>1.addPreference

  • Preference本身是实现了comparable的,添加preference的时候是经过排序的。
  • 如果xml里的preference都没有设置order,因为会自动从0开始赋值order,所以最终显示顺序不变的
  • 如果有order一样的,那就得按照title排序了,比如xml里给某个preference设置order为0,其他的都不设置order,因为都不设置的会默认赋予order,从0开始,所以就会有2个order为0的了,排序的时候就会比较title的
private final List<Preference> mPreferences;

private int mCurrentPreferenceOrder = 0;

private boolean mOrderingAsAdded = true;

public boolean addPreference(@NonNull Preference preference) {
    if (mPreferences.contains(preference)) {
        return true;
    }
//..
    if (preference.getOrder() == DEFAULT_ORDER) {
        if (mOrderingAsAdded) {
        //如果没有设置order的话,这里给设置个order,从0开始自增
            preference.setOrder(mCurrentPreferenceOrder++);
        }

        if (preference instanceof PreferenceGroup) {
            ((PreferenceGroup) preference).setOrderingAsAdded(mOrderingAsAdded);
        }
    }
    //参看5.2
    int insertionIndex = Collections.binarySearch(mPreferences, preference);
    if (insertionIndex < 0) {
        insertionIndex = insertionIndex * -1 - 1;
    }

    if (!onPrepareAddPreference(preference)) {
        return false;
    }
    //根据preference的排序规则,插入某个位置。
    synchronized (this) {
        mPreferences.add(insertionIndex, preference);
    }

    final PreferenceManager preferenceManager = getPreferenceManager();
    final String key = preference.getKey();
    final long id;
    if (key != null && mIdRecycleCache.containsKey(key)) {
        id = mIdRecycleCache.get(key);
        mIdRecycleCache.remove(key);
    } else {
        id = preferenceManager.getNextId();
    }
    preference.onAttachedToHierarchy(preferenceManager, id);
    //设置父容器为this
    preference.assignParent(this);
    
    if (mAttachedToHierarchy) {
    //父容器已经attach了,那么child也可以调用attach了
        preference.onAttached();
    }

    notifyHierarchyChanged();

    return true;
}

>2.onPrepareAddPreference

默认实现返回的true,返回false的话这个preference就不会被添加到集合里了,

protected boolean onPrepareAddPreference(@NonNull Preference preference) {
    preference.onParentChanged(this, shouldDisableDependents());
    return true;
}

5.2.Collections.binarySearch

这里简单举例分析下:

  • 添加第一个preference,list是空的,所以high=-1,while不满足,返回结果 -1,根据规则,插入集合的index为0
  • 添加第二个preference,list的size是1,所以high=0,mid=0,第一个数据和第二数据比较结果有两种: 1.负的,说明第二个数据应该插在第一个数据后边,low=1,跳出循环,返回-2,根据规则,插入集合的index为1 2.正的,说明第二个数据应该插在第一个数据前边,high=-1,low还是0,跳出循环,返回-1,插入集合的index为0.
  • 添加第三个preference,list的size是2,high=1,mid=0,结果同样有两种,和第一个值,如果排在第一个前边,就跳出循环了,返回的还是-1,插入index为0,如果排在第一个后边,那就继续和第二比较。

总结一下:

int indexedBinarySearch(List<? extends Comparable<? super T>> list, T key) {
    int low = 0;
    int high = list.size()-1;

    while (low <= high) {
        int mid = (low + high) >>> 1;
        Comparable<? super T> midVal = list.get(mid);
        //排序规则参看4.2
        int cmp = midVal.compareTo(key);

        if (cmp < 0)
            low = mid + 1;
        else if (cmp > 0)
            high = mid - 1;
        else
            return mid; // key found
    }
    return -(low + 1);  // key not found
}

其实吧,我们把下边的代码和上边的返回结果整理一下

insertionIndex = insertionIndex * -1 - 1;

就相当于下边的公式,最终其实就是low的值。

-(low + 1)*-1 -1

low的值默认是0,不是0的话就是mid+1,结果要么插在第一个位置,要么就是插入在某个位置。

6.PreferenceCategory

用于对类似首选项进行分组的容器。PreferenceCategory显示一个类别标题,并在视觉上分隔了偏好组。

可以看到,这个默认是不可用的,一般就展示分组的标题

public boolean isEnabled() {
    return false;
}

7.PreferenceScreen

  • 表示设置屏幕的顶级容器。
  • 这是Preference层次结构的根组件。
  • PreferenceFragmentCompat指向该类的一个实例来显示首选项。
  • 要实例化这个类,使用PreferenceManager.createPreferenceScreen(Context)
public final class PreferenceScreen extends PreferenceGroup {

7.1.onClick

只有在这个screen既没有设置intent,也没有设置fragment,并且里边child为0的情况下,才会查看listener

protected void onClick() {

    if (getIntent() != null || getFragment() != null || getPreferenceCount() == 0) {
        return;
    }
    final PreferenceManager.OnNavigateToScreenListener listener =
            getPreferenceManager().getOnNavigateToScreenListener();
    if (listener != null) {
    //2.5处理
        listener.onNavigateToScreen(this);
    }
}

8.PreferenceManager

8.1.inflateFromResource

从资源文件加载数据,第一次加载的时候 rootPreferences 应该是null

public PreferenceScreen inflateFromResource(@NonNull Context context, int resId,
        @Nullable PreferenceScreen rootPreferences) {
    // Block commits
    setNoCommit(true);

    final PreferenceInflater inflater = new PreferenceInflater(context, this);
    rootPreferences = (PreferenceScreen) inflater.inflate(resId, rootPreferences);
    rootPreferences.onAttachedToHierarchy(this);

    // Unblock commits
    setNoCommit(false);

    return rootPreferences;
}

9.PreferenceInflater.java

Preference到时候是通过2个参数的构造函数实例化的,参数存储在下边的数组里,第一个参数是Context,第二个AttributeSet

private final Object[] mConstructorArgs = new Object[2];
//后边要实例的Preference就是这2个参数的构造函数
private static final Class<?>[] CONSTRUCTOR_SIGNATURE = new Class<?>[]{
        Context.class, AttributeSet.class};

9.1.inflate

public Preference inflate(int resource, @Nullable PreferenceGroup root) {
//生成一个xml解析器
    XmlResourceParser parser = getContext().getResources().getXml(resource);
    try {
        return inflate(parser, root);
    } finally {
        parser.close();
    }
}

//

public Preference inflate(XmlPullParser parser, @Nullable PreferenceGroup root) {
    synchronized (mConstructorArgs) {
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        mConstructorArgs[0] = mContext; //第一个参数赋值
        final Preference result;

        try {
            //找到根节点
            int type;
            do {
                type = parser.next();
            } while (type != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT);


            // 参看9.2
            Preference xmlRoot = createItemFromTag(parser.getName(),
                    attrs);
            //参看 9.4
            result = onMergeRoots(root, (PreferenceGroup) xmlRoot);

            //参看9.5
            rInflate(parser, result, attrs);

        } 

        return result;
    }
}

9.2.createItemFromTag

根据标签的名字分类处理,系统的应该不带点的,我们自定义的都带包名的,所以肯定带点的

private Preference createItemFromTag(String name,
        AttributeSet attrs) {
    try {
        final Preference item;
        if (-1 == name.indexOf('.')) {
        //这种是系统默认的preference
            item = onCreateItem(name, attrs);
        } else {
            item = createItem(name, null, attrs);
        }
        return item;
    }

比如

//这种直接实例化对象
   <com.android.settingslib.widget.LayoutPreference/>
//这种得在标签前加上包名才好实例化
    <PreferenceCategory/>

最终走的都是一个方法

protected Preference onCreateItem(String name, AttributeSet attrs)
        throws ClassNotFoundException {
        //给个默认的包名
    return createItem(name, mDefaultPackages, attrs);
}

9.3.onCreateItem

prefixes为null说明是自定义的,那么name就是完整的路径

prefixes不为null,说明是系统默认的,需要加上包名

private Preference createItem(@NonNull String name, @Nullable String[] prefixes,
        AttributeSet attrs)
        throws ClassNotFoundException, InflateException {
        //先看缓存里有没有
    Constructor<?> constructor = CONSTRUCTOR_MAP.get(name);

    try {
        if (constructor == null) {
            // 缓存里没有这个类的构造器
            final ClassLoader classLoader = mContext.getClassLoader();
            Class<?> clazz = null;
            if (prefixes == null || prefixes.length == 0) {
            //这种是自定义的类,name就是完整的路径,直接使用即可
                clazz = Class.forName(name, false, classLoader);
            } else {
                ClassNotFoundException notFoundException = null;
                for (final String prefix : prefixes) {
                    try {
                    //加上包名
                        clazz = Class.forName(prefix + name, false, classLoader);
                        break;
                    } 
                }
                if (clazz == null) {
                //走到这里说明没找到这个类,抛出异常
                }
            }
            //获取构造函数
            constructor = clazz.getConstructor(CONSTRUCTOR_SIGNATURE);
            constructor.setAccessible(true);
            //放到缓存里
            CONSTRUCTOR_MAP.put(name, constructor);
        }

        Object[] args = mConstructorArgs;
        args[1] = attrs;//第二个参数设置
        //创建新的对象
        return (Preference) constructor.newInstance(args);

    } 
}

9.4.onMergeRoots

  • 如果解析前给定了一个根容器,那就用这个根容器,新解析出来的根容器抛弃。
  • 解析前给定的是空的,那用解析出来的这个作为根容器
private @NonNull PreferenceGroup onMergeRoots(PreferenceGroup givenRoot,
        @NonNull PreferenceGroup xmlRoot) {
    // If we were given a Preferences, use it as the root (ignoring the root
    // Preferences from the XML file).
    if (givenRoot == null) {
        xmlRoot.onAttachedToHierarchy(mPreferenceManager);
        return xmlRoot;
    } else {
        return givenRoot;
    }
}

比如,我们已有PreferenceScreen A 根容器,之后通过inflate解析下边这个布局,解析出来的key为xxx的这个PreferenceScreen B就会被抛弃,会把B里边的child preference C D 加载到之前的A容器里。

<PreferenceScreen
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:settings="http://schemas.android.com/apk/res-auto"
    android:key="xxx"
    android:title="about_settings">
    <Preference D/>
    <Preference C/>

9.5.rInflate

Recursive 递归解析

private void rInflate(@NonNull XmlPullParser parser, Preference parent,
        final AttributeSet attrs)
        throws XmlPullParserException, IOException {
    final int depth = parser.getDepth();

    int type;
    
    while (((type = parser.next()) != XmlPullParser.END_TAG ||
            parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
         //找到开始标签   
        if (type != XmlPullParser.START_TAG) {
            continue;
        }

        final String name = parser.getName();

        if (INTENT_TAG_NAME.equals(name)) { //intent 标签
            final Intent intent;
            try {
                intent = Intent.parseIntent(getContext().getResources(), parser, attrs);
            } 

            parent.setIntent(intent);
        } else if (EXTRA_TAG_NAME.equals(name)) { //extra 标签
            //解析的数据存到parent.getExtras(),这东西是个bundle
            getContext().getResources().parseBundleExtra(EXTRA_TAG_NAME, attrs,
                    parent.getExtras());
            try {
                skipCurrentTag(parser);
            } 
        } else {
        //这又回到9.2了,继续解析下一个preference标签
            final Preference item = createItemFromTag(name, attrs);
            //添加到父容器里
            ((PreferenceGroup) parent).addItemFromInflater(item);
            //继续解析,看item还有没有child
            rInflate(parser, item, attrs);
        }
    }

}

>举例

Preference里还可以有 extra和intent标签

<Preference
    android:key="xxxx"
    android:order="17"
    android:title="xxx">
    <extra
        android:name="name"
        android:value="value" />
    <intent android:action="xxx" />
</Preference>

10.PreferenceGroupAdapter

rv的适配器

public class PreferenceGroupAdapter extends RecyclerView.Adapter<PreferenceViewHolder>
        implements Preference.OnPreferenceChangeInternalListener,
        PreferenceGroup.PreferencePositionCallback {

10.1.构造方法

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

    //添加删除preference的时候可以知道
    mPreferenceGroup.setOnPreferenceChangeInternalListener(this);

    mPreferences = new ArrayList<>();
    mVisiblePreferences = new ArrayList<>();
    mPreferenceResourceDescriptors = new ArrayList<>();

    // Initial sync to generate mPreferences and mVisiblePreferences and display the visible
    // preferences in the RecyclerView
    updatePreferences();
}

10.2.updatePreferences

void updatePreferences() {
    for (final Preference preference : mPreferences) {
    //清除旧的监听
        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);
    //把所有的preference添加到集合里,参考 10.3
    flattenPreferenceGroup(mPreferences, mPreferenceGroup);

    final List<Preference> oldVisibleList = mVisiblePreferences;

    //获取所有可见的Preference集合 ,参考 10.4
    final List<Preference> visiblePreferenceList = createVisiblePreferencesList(
            mPreferenceGroup);

    mVisiblePreferences = visiblePreferenceList;

    final PreferenceManager preferenceManager = mPreferenceGroup.getPreferenceManager();
    if (preferenceManager != null
            && preferenceManager.getPreferenceComparisonCallback() != null) {
        //如果有设置数据比较回调的话,那就用diff工具部分刷新
        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();//detached设置为false
    }
}

10.3.flattenPreferenceGroup

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);
        //这个描述符用来区分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()) {
            //上边的默认实现是true,所以如果是group的话,递归查找它的child添加到集合里。
                flattenPreferenceGroup(preferences, nestedGroup);
            }
        }
    //设置监听
        preference.setOnPreferenceChangeInternalListener(this);
    }
}

10.4.createVisiblePreferencesList

获取可见的preference集合

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不计入可见的偏好数里
        if (!(preference instanceof PreferenceGroup)) {
            visiblePreferenceCount++;
            continue;
        }

        PreferenceGroup innerGroup = (PreferenceGroup) preference;
        //默认情况if不满足
        if (!innerGroup.isOnSameScreenAsChildren()) {
            continue;
        }

        //外部group和里边的group都可以展开收缩,这种不允许
        if (isGroupExpandable(group) && isGroupExpandable(innerGroup)) {
            throw new IllegalStateException(
                    "Nesting an expandable group inside of another expandable group is not "
                            + "supported!");
        }

        //递归查找子group里的偏好
        final List<Preference> innerList = createVisiblePreferencesList(innerGroup);

        for (Preference inner : innerList) {
        //把 inner group里查到的数据添加到集合里
            if (!isGroupExpandable(group)
                    || visiblePreferenceCount < group.getInitialExpandedChildrenCount()) {
                visiblePreferences.add(inner);
            } else {
                collapsedPreferences.add(inner);
            }
            visiblePreferenceCount++;
        }
    }

    //这里处理下初始化显示的个数小于可见数,有部分不显示,所有增加一个展开的条目,点击的时候展开
    if (isGroupExpandable(group)
            && visiblePreferenceCount > group.getInitialExpandedChildrenCount()) {
        final ExpandButton expandButton = createExpandButton(group, collapsedPreferences);
        visiblePreferences.add(expandButton);
    }
    return visiblePreferences;
}

>isGroupExpandable

  • 默认返回是false,如果设置了初始化展开的child个数的话,那就是true
private boolean isGroupExpandable(PreferenceGroup preferenceGroup) {
    return preferenceGroup.getInitialExpandedChildrenCount() != Integer.MAX_VALUE;
}

默认值

private int mInitialExpandedChildrenCount = Integer.MAX_VALUE;

10.5.onPreferenceChange

修改对应的preference

public void onPreferenceChange(@NonNull Preference preference) {
    final int index = mVisiblePreferences.indexOf(preference);
    if (index != -1) {
        notifyItemChanged(index, preference);
    }
}

10.6.onPreferenceHierarchyChange

这个是直接刷新adapter了

public void onPreferenceHierarchyChange(@NonNull Preference preference) {
    mHandler.removeCallbacks(mSyncRunnable);
    mHandler.post(mSyncRunnable);
}

>mSyncRunnable

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

10.7.createExpandButton

  • 如果设置了mInitialExpandedChildrenCount的值,那么会根据child的可见数,以及初始化的值做比较。
  • 如果初始化显示的值小于可见child数,那就会自动添加一个新的ExpandButton,点击会展开所有的Preference。
  • 其实就是把mInitialExpandedChildrenCount的值又改成了max int
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);
            //这里会刷新adapter的
            onPreferenceHierarchyChange(preference);
            final PreferenceGroup.OnExpandButtonClickListener listener =
                    group.getOnExpandButtonClickListener();
            if (listener != null) {
                listener.onExpandButtonClick();
            }
            return true;
        }
    });
    return preference;
}

10.8.PreferenceResourceDescriptor

类名,布局id,小部件id 这三个一样,就认为是同一种类型

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);
}

10.9.ExpandButton

final class ExpandButton extends Preference {

默认效果如下,summary到时候会被未显示的preference的title替代 image.png

>1.initLayout

private void initLayout() {
    setLayoutResource(R.layout.expand_button);
    setIcon(R.drawable.ic_arrow_down_24dp);
    setTitle(R.string.expand_button_title);
    // Sets a high order so that the expand button will be placed at the bottom of the group
    setOrder(999);
}

>2.setSummary

private void setSummary(List<Preference> collapsedPreferences) {
    CharSequence summary = null;
    final List<PreferenceGroup> parents = new ArrayList<>();

    for (Preference preference : collapsedPreferences) {
        final CharSequence title = preference.getTitle();
    //..忽略group
        if (!TextUtils.isEmpty(title)) {
            if (summary == null) {
                summary = title;
            } else {
            //多个title的话,用格式化的字符串,依次往末尾加上新的title
                summary = getContext().getString(
                        R.string.summary_collapsed_preference_list, summary, title);
            }
        }
    }
    setSummary(summary);
}

@Override
public void onBindViewHolder(@NonNull PreferenceViewHolder holder) {
    super.onBindViewHolder(holder);
    //去掉分割线
    holder.setDividerAllowedAbove(false);
}

//格式化的字符串如下

<string name="summary_collapsed_preference_list"><ns1:g id="current_items">%1$s</ns1:g>, <ns1:g id="added_items">%2$s</ns1:g></string>

上边的字符串就相当于占位符,如下

${current_items},${added_items}

>3.expand_button.xml

和Preference默认的布局差不多,就是没有右侧那个widget容器了

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <include layout="@layout/image_frame"/>

    <RelativeLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:paddingTop="16dp"
        android:paddingBottom="16dp">

        <TextView
            android:id="@android:id/title"/>

        <TextView
            android:id="@android:id/summary"/>
    </RelativeLayout>
</LinearLayout>

10.10.getItemViewType

public int getItemViewType(int position) {
    final Preference preference = this.getItem(position);

    PreferenceResourceDescriptor descriptor = new PreferenceResourceDescriptor(preference);
    //描述符的三要素,见10.8 ,一样的话那布局类型应该是一样的
    int viewType = mPreferenceResourceDescriptors.indexOf(descriptor);
    if (viewType != -1) {
        return viewType;
    } else {
        viewType = mPreferenceResourceDescriptors.size();
        mPreferenceResourceDescriptors.add(descriptor);
        return viewType;
    }
}

至于onCreateViewHolder就不看了,就是加载preference里的布局

>onBindViewHolder

public void onBindViewHolder(@NonNull PreferenceViewHolder holder, int position) {
    final Preference preference = getItem(position);
    holder.resetState();
    //具体见4.4
    preference.onBindViewHolder(holder);
}

11.总结

  • PreferenceFragmentCompat 的布局结构,里边主要就是个rv
  • 一般情况调用addPreferencesFromResource方法添加xml数据
  • 也可以通过PreferenceManager的createPreferenceScreen方法创建一个PreferenceScreen,完事通过addPreference动态添加数据,最后setPreferenceScreen即可
  • PreferenceGroupAdapter简单查看,数据的获取,ExpandButton的添加
  • PreferenceInflater 解析xml文件,返回Preference
  • DialogPreference的3种实现类的简单学习
  • Preference的排序规则,先看order,再看title,可以看下about页面,用户头像显示在basic info下边的