写在前面的话
学而不思则罔,思而不学则殆。最近在做设置页的UI调整,总会遇到需要自定义Preference的情况,现将自定义Preference的一些思考总结如下。既方面后续查阅,也与大家交流一下自己的看法,希望通过交流可以有更大的进步。
关于自定义Preference,一般会遇到两种场景:一种是可以复用原生Preference布局;另一种是layout完全无法复用,需要自己定义layout。下面分别针对这两种情况给出自己的总结。
1、可复用Preference布局
1.1 分析
针对可复用Preference布局的情况,首先我们看下preference/preference/res/layout/preference.xml 的布局如下所示,此处可以替换的布局id为widget_frame,Preference类提供了方法setWidgetLayoutResource(int widgetLayoutResid)来设置该布局。
<?xml version="1.0" encoding="utf-8"?>
<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>
这里我们用一个ButtonPreference来举例,用Button填充widget_frame来实现ButtonPreference,实现功能:设置ButtonPreference中Button上的Text,具体实现如下
- 定义要替换的Button布局文件 R.layout.preference_widget_button
<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/buttonWidget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
- 定义buttonText属性
<declare-styleable name="ButtonPreference">
<attr name="buttonText" format="string" />
</declare-styleable>
- 构造方法中通过setWidgetlayoutResource方法替换布局
- 继承Preference,实现onBindViewHolder方法,获取自定义布局中的控件
- 实现setButtonText方法和点击监听
class ButtonPreference : Preference {
private var button: Button? = null
private var buttonText: CharSequence? = ""
private var buttonClickListener: OnButtonClickListener? = null
interface OnButtonClickListener {
fun onButtonClick(view: View?)
}
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(
context,
attrs,
R.attr.PreferenceStyle
)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : this(
context,
attrs,
defStyleAttr,
0
)
constructor(
context: Context,
attrs: AttributeSet?,
defStyleAttr: Int,
defStyleRes: Int
) : super(context, attrs, defStyleAttr) {
//替换布局
widgetLayoutResource = R.layout.preference_widget_button
val a = context.obtainStyledAttributes(
attrs,
R.styleable.ButtonPreference,
defStyleAttr,
defStyleRes
)
//获取属性
buttonText = a.getText(R.styleable.ButtonPreference_buttonText)
a.recycle()
}
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
//初始化控件
button = holder.findViewById(R.id.buttonWidget) as? Button
button?.apply {
setOnClickListener { v -> buttonClickListener?.onButtonClick(v) }
if (!buttonText.isNullOrEmpty()) {
text = buttonText
}
}
}
fun setButtonText(text: CharSequence?) {
Log.d("setButtonText", "text=$text")
if (!TextUtils.equals(text, buttonText)) {
buttonText = text
notifyChanged()
}
}
fun setOnButtonClickListener(listener: OnButtonClickListener) {
buttonClickListener = listener
}
}
1.2 总结
看完上面的流程和代码,我们总结提炼一下实现过程五步法:
- 定义要替换的布局文件R.layout.preference_widget_button
- 定义所需的属性,如定义buttonText属性
- 构造方法中通过setWidgetlayoutResource方法替换布局
- 继承Preference,实现onBindViewHolder方法,获取自定义布局中的控件
- 实现setButtonText方法和点击监听
2、layout无法复用
2.2 分析
说完可以复用的场景,这里针对不能复用Preference布局的情况,这里我们以LoadingPreference举例进行说明,具体实现如下:
- 自定义layout为R.layout.loading_preference_layout:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:oppo="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:orientation="vertical"
android:background="@drawable/preference_bg_selector"
android:minHeight="@dimen/loading_preference_min_height"
android:paddingStart="@dimen/preference_titel_padding_start"
android:paddingEnd="@dimen/preference_titel_padding_end"
android:paddingTop="@dimen/preference_text_content_padding_top"
android:paddingBottom="@dimen/preference_text_content_padding_bottom">
<TextView
android:id="@android:id/title"
style="@style/PreferenceTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/list_view_item_text_size"/>
<TextView
android:id="@android:id/summary"
style="@style/PreferenceSummary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/preference_margin_between_line" />
<TextView
android:id="@+id/assignment"
style="@style/Assignment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/preference_margin_between_line" />
<LoadingView
android:id="@+id/loadingView"
style="@style/PreferenceLoadingView"
android:layout_width="@dimen/loading_preference_item_min_size"
android:layout_height="@dimen/loading_preference_item_min_size"
android:layout_marginTop="@dimen/preference_margin_between_line"
android:visibility="gone" />
</LinearLayout>
- PreferenceScreen中定义LoadingPreference,声明属性android:layout为自定义的布局
<LoadingPreference
android:key="version_update"
android:title="version_name"
android:layout="@layout/loading_preference_layout">
</LoadingPreference>
- 继承Preference,实现onBindViewHolder方法,初始化需要用到的控件
- 设置属性
class LoadingPreference : Preference {
private var loadingView: LoadingView? = null
private var assignment: TextView? = null
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?)
: this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
: this(context, attrs, defStyleAttr, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int)
: super(context, attrs, defStyleAttr)
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
loadingView = holder.findViewById(R.id.loadingView) as LoadingView
assignment = holder.findViewById(R.id.assignment) as TextView
}
fun setLoadingViewVisibility(visibility: Int){
loadingView?.visibility = visibility
}
fun updateFinish(){
setAssignment("已更新")
}
}
2.2 总结
看完上面的流程和代码,我们总结提炼一下实现过程四步法:
- 自定义layout
- xml中声明LoadingPreference属性android:layout为自定义的布局
- 继承Preference,实现onBindViewHolder方法,初始化需要用到的控件
- 设置属性,实现控件属性功能,需要更新执行notifyChaned方法
3. 扩展
3.1 扩展xml属性
以上面的ButtonPreference举例,上面定义buttonText属性后,在xml中即可以用到
属性名称 | 描述 | 类型 | 示例 |
---|---|---|---|
buttonText | 设置按钮的文字内容 | string | app:buttonText="上传" |
jump_mark | 资源设置 | reference | app:jump_mark="@drawable/next" |
3.2 扩展api
以上面的ButtonPreference举例,上面定义的fun setButtonText(text: Charsequence?)即为扩展api
4. Preference的种类
切记:在造轮子之前,我们一定要了解是否有该轮子,切莫乱造轮子,复用一堆垃圾代码,下面列出目前api中有的Preference
种类 |
---|
Preference |
CheckBoxPreference |
ListPreference |
SeekBarPreference |
DialogPreference |
SwitchPreference |
DropDownPreference |
EditTextPreference |
MultiSelectListPreference |
TwoStatePreference |