本篇分析的庫是基于:androidx.preference.preference:[email protected]
先來看張微信中的頁面,這個頁面實作其來比較簡單,實作的方式也有很多,但按可擴張性和簡單程度來說,個人認為還是要數Preference了,基本就是xml中配置了。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnLkRGOkJjZlNjMkZDOxYDOihTMhRTYmVTOhJjM4cTY3UzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
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自己去處理了。