【UI篇1】自定义Preference

写在前面的话
学而不思则罔,思而不学则殆。最近在做设置页的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,具体实现如下

  1. 定义要替换的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" />
  1. 定义buttonText属性
<declare-styleable name="ButtonPreference">
    <attr name="buttonText" format="string" />
</declare-styleable>
  1. 构造方法中通过setWidgetlayoutResource方法替换布局
  2. 继承Preference,实现onBindViewHolder方法,获取自定义布局中的控件
  3. 实现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 总结

看完上面的流程和代码,我们总结提炼一下实现过程五步法:

  1. 定义要替换的布局文件R.layout.preference_widget_button
  2. 定义所需的属性,如定义buttonText属性
  3. 构造方法中通过setWidgetlayoutResource方法替换布局
  4. 继承Preference,实现onBindViewHolder方法,获取自定义布局中的控件
  5. 实现setButtonText方法和点击监听

2、layout无法复用

2.2 分析

说完可以复用的场景,这里针对不能复用Preference布局的情况,这里我们以LoadingPreference举例进行说明,具体实现如下:

  1. 自定义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>
  1. PreferenceScreen中定义LoadingPreference,声明属性android:layout为自定义的布局
<LoadingPreference
    android:key="version_update"
    android:title="version_name"
    android:layout="@layout/loading_preference_layout">
</LoadingPreference>
  1. 继承Preference,实现onBindViewHolder方法,初始化需要用到的控件
  2. 设置属性
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 总结

看完上面的流程和代码,我们总结提炼一下实现过程四步法:

  1. 自定义layout
  2. xml中声明LoadingPreference属性android:layout为自定义的布局
  3. 继承Preference,实现onBindViewHolder方法,初始化需要用到的控件
  4. 设置属性,实现控件属性功能,需要更新执行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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容