Android(12)浅析 偏好设置 Preference(一)
### 官方基本用法:https://developer.android.google.cn/guide/topics/ui/settings
效果演示:
源码解读路线:
Preference
需要配合PreferenceFragmentCompat
使用。
-
所以和平时在
Activity
中使用Fragment
没有区别,只是布局文件不是从layout
文件夹去渲染而是从xml
文件夹。 -
列表的展示是用了
RecyclerView
实现的,Apdater
是PreferenceGroupAdpater
,VH
是PreferenceViewHolder
-
-
Preference
要关心的地方,基本布局文件是什么?,扩展布局文件是什么,怎么设置点击事件的呢?,怎么监听值的变化的呢?,我该如何自定义呢?等…
准备动手!准备动手!
- 先来看一下
Preference
默认的布局:
使用AS定位到文件:R.layout.preference
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2015 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
-->
<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>
-
先从
UI
动手:由于是RecyclerView
实现的,还是很清晰的,先进入PreferenceGroupAdapter
先看数据源、三大重写大方法
onCreateViewHolder()
、onBindViewHolder()
、getItemCount()
public class PreferenceGroupAdapter extends RecyclerView.Adapter<PreferenceViewHolder> implements Preference.OnPreferenceChangeInternalListener, PreferenceGroup.PreferencePositionCallback { // ... // 数据集 private List<Preference> mPreferences; // 可见的Preference private List<Preference> mVisiblePreferences; @Override public int getItemCount() { return mVisiblePreferences.size(); } // onCreateViewHolder() @Override @NonNull public PreferenceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { // 先获取Preference的资源描述类,这个类内部存储了一个Preference的Class名,基本布局资源id,扩展布 // 局资源id 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(); // 构造VH,看,这里inflate的是decriptor.mLoutRestId,上面说了,这个值保存的是Prefercen的及颁布布局,也就是对应着xml中的layout属性 final View view = inflater.inflate(descriptor.mLayoutResId, parent, false); if (view.getBackground() == null) { ViewCompat.setBackground(view, background); } // 这里判断如果设置了widgetLayout属性,这里会同时渲染 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); } // 绑定数据-----------------> Preference.onBindViewHolder() @Override public void onBindViewHolder(@NonNull PreferenceViewHolder holder, int position) { final Preference preference = getItem(position); preference.onBindViewHolder(holder); } // ... }
走到这里,我们就明白了为什么自定义
Preference
需要重写的方法为什么是onBindViewHolder()
了,在适配器的onBindViewHHolder()
中是直接委托给了Preference
实现。接着走,去
Preference#onBindViewHolder()
瞅一瞅: -
Preference#onBindViewHolder()
: 翻译的好啊,好地方!
public void onBindViewHolder(PreferenceViewHolder holder) {
View itemView = holder.itemView;
Integer summaryTextColor = null;
// 设置点击事件,划重点 mClickListener
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)) {
summaryView.setText(summary);
summaryView.setVisibility(View.VISIBLE);
summaryTextColor = summaryView.getCurrentTextColor();
} else {
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 {
imageView.setVisibility(mIconSpaceReserved ? View.INVISIBLE : View.GONE);
}
}
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);
final boolean copyingEnabled = isCopyingEnabled();
if (copyingEnabled && mOnCopyListener == null) {
mOnCopyListener = new OnPreferenceCopyListener(this);
}
itemView.setOnCreateContextMenuListener(copyingEnabled ? mOnCopyListener : null);
itemView.setLongClickable(copyingEnabled);
// Remove touch ripple if the view isn't selectable
if (copyingEnabled && !selectable) {
ViewCompat.setBackground(itemView, null);
}
}
OKK,我们知道了点击事件是直接绑定在itemView
上的,我们来看看这个mClickListener
:
private final View.OnClickListener mClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
// 处理点击事件,执行performClick()方法
performClick(v);
}
};
继续走------------> Preference#performClick(View)
@RestrictTo(LIBRARY_GROUP_PREFIX)
protected void performClick(View view) {
// 继续分发套娃
performClick();
}
好家伙,经典套娃,这次一定是具体实现 -----> Preference#performClick()
@RestrictTo(LIBRARY_GROUP_PREFIX)
public void performClick() {
// 不可用或者不可点击的话直接return
if (!isEnabled() || !isSelectable()) {
return;
}
// 这个onClick()是一个空方法,可以在Preference设置了点击事件之前可以处理一些自己的逻辑
onClick();
// 朴实无华的点击分发,如果给Preference设置了点击事件(Preference$setOnPreferenceClickListerner)则调用接口方法处理自己的逻辑
if (mOnClickListener != null && mOnClickListener.onPreferenceClick(this)) {
return;
}
// 假如没有设置点击事件的话,这个点击事件没有完全消费掉(就是onPreferenceClick(this)为false)
// 这里会执行另一个接口方法 PreferenceManager.OnPreferenceTreeClickListener
// 这个方法的实现在 PreferenceFragmentCompat中,也就是我们可以通过这个方法得到当前点击的是哪一个
// Preference
PreferenceManager preferenceManager = getPreferenceManager();
if (preferenceManager != null) {
PreferenceManager.OnPreferenceTreeClickListener listener = preferenceManager
.getOnPreferenceTreeClickListener();
if (listener != null && listener.onPreferenceTreeClick(this)) {
return;
}
}
// 这里就是上面的点击事件,全局点击事件都没有完全消费的话,这里就是直接执行<intent/>节点
// 就是启动一个 Activity
if (mIntent != null) {
Context context = getContext();
context.start到此Activity(mIntent);
}
}
到此,onBindView()
就搞定了,UI也就在Fragment
中显示出来了
怎么自定义Preference
嘞?
- 通过上面的分析啊,我们知道俩个属性分别在哪里被用到了,
layout
、widgetLyaout
,那么我们自定义布局的时候如果还是采用原生那个排列的话,就是上面那个效果的话,就是直接把默认布局抄一遍,然后就可以改样式了,如果是完全非主流的样式就是自己搞个布局然后赋值给layout
或者在代码里赋值给layoutResource
- 布局搞好之后,就得需要对布局中的控件做一些配置,这个时候就得用到
Preference#onBindViewHolder()
这个方法了,用法和ReyclerView.Adapter.onBindViewHolder()
一样 - 同时,
Preference
也封装了notifyDataSetChanged()
方法:Preference#notifyChanged()