先来看张微信中的页面,这个页面实现其来比较简单,实现的方式也有很多,但按可扩张性和简单程度来说,个人认为还是要数Preference了,基本就是xml中配置了。
android中有提供给我们专门用作设置处理的库Preference(支持的控件可直接在该库下查看),对于怎么使用,android studio有提供模板Settings Activity,接下来就看下它的实现。
先来简单看下Preference定义的xml:
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory app:title="@string/sync_header">
<SwitchPreferenceCompat
app:key="sync"
app:title="@string/sync_title" />
<SwitchPreferenceCompat
app:dependency="sync"
app:key="attachment"
app:switchTextOn="kai"
app:switchTextOff="guan"
app:summaryOff="@string/attachment_summary_off"
app:summaryOn="@string/attachment_summary_on"
app:title="@string/attachment_title" />
</PreferenceCategory>
</PreferenceScreen>
在来看下在Fragment中的使用:
public static class SettingsFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.root_preferences, rootKey);
}
}
这里就是调用了setPreferencesFromResource()把xml设置进去,接着就是使用这个Fragment就可以了。
咋一看,这里并没有view啊,那是设置页面是怎么显示的呢,带着这个疑问,一起来看下PreferenceFragmentCompat的源码,Fragment创建view是在onCreateView这个方法中:
private int mLayoutResId = R.layout.preference_list_fragment;
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
TypedArray a = getContext().obtainStyledAttributes(null,
R.styleable.PreferenceFragmentCompat,
R.attr.preferenceFragmentCompatStyle,
0);
mLayoutResId = a.getResourceId(R.styleable.PreferenceFragmentCompat_android_layout,
mLayoutResId);
... ...
a.recycle();
final LayoutInflater themedInflater = inflater.cloneInContext(getContext());
final View view = themedInflater.inflate(mLayoutResId, container, false);
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;
final RecyclerView listView = onCreateRecyclerView(themedInflater, listContainer,
savedInstanceState);
if (listView == null) {
throw new RuntimeException("Could not create RecyclerView");
}
mList = listView;
listView.addItemDecoration(mDividerDecoration);
setDivider(divider);
if (dividerHeight != -1) {
setDividerHeight(dividerHeight);
}
mDividerDecoration.setAllowDividerAfterLastItem(allowDividerAfterLastItem);
// If mList isn't present in the view hierarchy, add it. mList is automatically inflated
// on an Auto device so don't need to add it.
if (mList.getParent() == null) {
listContainer.addView(mList);
}
mHandler.post(mRequestFocus);
return view;
}
这里的style/declare-styleable/attr以及后面用到的都是定义在该库的res/values/values.xml中。可以配合着来看,在values.xml中并没有定义layout,所以使用的是默认的R.layout.preference_list_fragment,这是系统定义的资源文件,
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="NewApi"
android:orientation="vertical"
android:layout_height="match_parent"
android:layout_width="match_parent" >
<FrameLayout
android:id="@android:id/list_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<TextView android:id="@android:id/empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"
android:gravity="center"
android:visibility="gone" />
</LinearLayout>
配合着对应的xml看就简单多了,这里用到的就是一个id为list_container的FrameLayout,接着就是创建一个RecyclerView加入到这个FrameLayout中去,这里再来看下RecyclerView的创建:
public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent,
Bundle savedInstanceState) {
// If device detected is Auto, use Auto's custom layout that contains a custom ViewGroup
// wrapping a RecyclerView
if (getContext().getPackageManager().hasSystemFeature(PackageManager
.FEATURE_AUTOMOTIVE)) {
RecyclerView recyclerView = parent.findViewById(R.id.recycler_view);
if (recyclerView != null) {
return recyclerView;
}
}
// 通常是执行这里
RecyclerView recyclerView = (RecyclerView) inflater
.inflate(R.layout.preference_recyclerview, parent, false);
recyclerView.setLayoutManager(onCreateLayoutManager());
recyclerView.setAccessibilityDelegateCompat(
new PreferenceRecyclerViewAccessibilityDelegate(recyclerView));
return recyclerView;
}
对应的xml是:
<androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/recycler_view"
style="?attr/preferenceFragmentListStyle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="0dp"
android:paddingBottom="0dp"
android:clipToPadding="false"/>
都比较简单,总结就是创建了一个RecyclerView加入到了FrameLayout中,有了RecyclerView,显示自然需要用到adapter了,设置adapter如下:
void bindPreferences() {
final PreferenceScreen preferenceScreen = getPreferenceScreen();
if (preferenceScreen != null) {
getListView().setAdapter(onCreateAdapter(preferenceScreen));
preferenceScreen.onAttached();
}
onBindPreferences();
}
创建adapter传入的是PreferenceScreen对象,这个对象是怎么来的呢?回到一开始的setPreferencesFromResource()方法:
public void setPreferencesFromResource(@XmlRes int preferencesResId, @Nullable String key) {
requirePreferenceManager();
final PreferenceScreen xmlRoot = mPreferenceManager.inflateFromResource(getContext(),
preferencesResId, null);
final Preference root;
// key默认为null
if (key != null) {
root = xmlRoot.findPreference(key);
if (!(root instanceof PreferenceScreen)) {
throw new IllegalArgumentException("Preference object with key " + key
+ " is not a PreferenceScreen");
}
} else {
root = xmlRoot;
}
setPreferenceScreen((PreferenceScreen) root);
}
创建adapter传入的PreferenceScreen就是在这里创建的了,preferencesResId就是我们一开始传入的xml文件,这里创建PreferenceScreen和创建view很像,这里就不跟进去看了,无非就是拿到xml中配置的信息通过反射创建对象,接着回到adapter创建:
protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) {
return new PreferenceGroupAdapter(preferenceScreen);
}
PreferenceGroupAdapter实现了RecyclerView.Adapter,那就来看下它的onCreateViewHolder方法:
public PreferenceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
... ...
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);
}
这里的descriptor对象封装的是preference相关的布局文件,到这里会有一个疑惑,我们在xml中明明没有配置layout,那这里创建view的layout是从哪里来的呢?那就得先来看看Preference这个类的构造函数了:
public Preference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.Preference, defStyleAttr, defStyleRes);
... ...
mLayoutResId = TypedArrayUtils.getResourceId(a, R.styleable.Preference_layout,
R.styleable.Preference_android_layout, R.layout.preference);
mWidgetLayoutResId = TypedArrayUtils.getResourceId(a, R.styleable.Preference_widgetLayout,
R.styleable.Preference_android_widgetLayout, 0);
... ...
a.recycle();
}
这里就是获取layoutId,如果我们的xml中没有配置,那么就使用默认的,mWidgetLayoutResId是设置项里面的控制按钮,那这里的id是如何根据不同的preference来确定不同的id呢?这里以SwitchPreferenceCompat为例:
public SwitchPreferenceCompat(Context context, AttributeSet attrs) {
this(context, attrs, R.attr.switchPreferenceCompatStyle);
}
这里指定了主题中使用的值为switchPreferenceCompatStyle,再来看下主题定义:
<style name="PreferenceThemeOverlay">
... ...
<item name="switchPreferenceCompatStyle">@style/Preference.SwitchPreferenceCompat.Material</item>
... ...
</style>
<style name="Preference.SwitchPreferenceCompat.Material">
<item name="android:layout">@layout/preference_material</item>
<item name="allowDividerAbove">false</item>
<item name="allowDividerBelow">true</item>
<item name="iconSpaceReserved">@bool/config_materialPreferenceIconSpaceReserved</item>
</style>
<style name="Preference.SwitchPreferenceCompat">
<item name="android:widgetLayout">@layout/preference_widget_switch_compat</item>
<item name="android:switchTextOn">@string/v7_preference_on</item>
<item name="android:switchTextOff">@string/v7_preference_off</item>
</style>
<style name="Preference">
<item name="android:layout">@layout/preference</item>
</style>
可以看到,SwitchPreferenceCompat默认使用的是preference_widget_switch_compat.xml,这里就只是定义了一个SwitchCompat控制,这里来看下主布局mLayoutResId(preference.xml):
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight"
android:gravity="center_vertical"
android:paddingEnd="?android:attr/scrollbarSize"
android:paddingRight="?android:attr/scrollbarSize"
android:background="?android:attr/selectableItemBackground">
<FrameLayout
android:id="@+id/icon_frame"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<androidx.preference.internal.PreferenceImageView
android:id="@android:id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:maxWidth="48dp"
app:maxHeight="48dp" />
</FrameLayout>
<RelativeLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="15dip"
android:layout_marginLeft="15dip"
android:layout_marginEnd="6dip"
android:layout_marginRight="6dip"
android:layout_marginTop="6dip"
android:layout_marginBottom="6dip"
android:layout_weight="1">
<TextView android:id="@android:id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="?android:attr/textColorPrimary"
android:ellipsize="marquee"
android:fadingEdge="horizontal" />
<TextView android:id="@android:id/summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@android:id/title"
android:layout_alignStart="@android:id/title"
android:layout_alignLeft="@android:id/title"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"
android:maxLines="4" />
</RelativeLayout>
<!-- Preference should place its actual preference widget here. -->
<LinearLayout android:id="@android:id/widget_frame"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="vertical" />
</LinearLayout>
到这,布局来源的事就ok了,接着回到PreferenceGroupAdapter.onCreateViewHolder(),这里主要就是填充出上面这个布局,如果有widgetLayout的话,在加到上面id为widget_frame的LinearLayout布局中去,执行完后就看看onBindViewHolder()了:
public void onBindViewHolder(@NonNull PreferenceViewHolder holder, int position) {
final Preference preference = getItem(position);
preference.onBindViewHolder(holder);
}
view的处理又回到具体的preference了,至此,页面的显示的逻辑就差不多了。熟悉了这整个流程,想要定制自己的设置界面,那就比较简单了。比如原生的SwitchPreferenceCompat样式太丑,想要修改switch的样式,那就可以在xml布局中定义widgetLayout,如果想要整体替换,那就定义layout属性了,这里要注意一点,如果要使用xml中配置的title等属性,View使用的id就要和preference.xml中一样,不然就需要继承Preference自己去处理了。