美文网首页暂时收藏学习androidAndroid Material Design
Android:详解如何创建Google风格的SettingsA

Android:详解如何创建Google风格的SettingsA

作者: 798fe2ac8685 | 来源:发表于2016-09-26 14:14 被阅读4879次

    Android:详解如何创建Google风格的SettingsActivity

    标签(空格分隔): android material-design
    作者:陈小默

    版本:3
    修改了部分错别字
    调整了部分语法顺序
    将Google API上的图片上传到了GitHub并重新显示(有些童鞋无法科学上网)
    

    话不多说先上图。从下面的效果我们可以看出在Android在5.0以上对布局进行了大规模的美化,在4.4上运行感觉就是相当的吃藕。

    图0-1 Android 7.0 模拟器上的效果
    图0-2 Android 4.4 荣耀上的效果

    如果你是第一次接触Android中的Settings,最好不要直接从Android Studio中快捷创建,就是下图这个快捷创建界面。如果你真的这么干了,你应该就能体会到什么叫做花样懵逼了。所以这篇博客目的就是新手入门手把手教学。

    图0-3 Android Studio快捷创建Activity的界面

    目录

    [toc]


    一、概念介绍

    1.1 Android设置界面的创建理念

    对于Android开发来说,程序的配置信息可以有不同的存储方式,比如文件、数据库、SharedPreferences(以下简称SP)、甚至是网络都可以用来存储配置。其中公认的最优存储方式就是SP文件了。所以Android中的Settings页面的所有操作的基础就是SP文件的操作。Android API定义了一个Preference类对SP文件的操作进行了封装。默认情况下,在程序的任意位置均可通过调用静态方法 PreferenceManager.getDefaultSharedPreferences() 方法获取存储设置信息的SP文件。

    val sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
    val value = sharedPref.getString(KEY, "NULL");
    

    1.2 布局元素

    我们先来看一下可以Preference类的所有子类

    • CheckBoxPreference
    • DialogPreference
    • EditTextPreference
    • ListPreference
    • MultiSelectListPreference
    • RingtonPreference
    • SwitchPreference
    • TowStatePreference

    后面我们会详细介绍和使用这些控件的


    二、开始创建

    一个设置页面的创建步骤分为三步:

    • 创建Xml文件
    • 创建PreferenceFragment,并在其中加载我们创建的Xml文件
    • 在Activity中显示PreferenceFragment

    下面我会将程序中的部分过程展示出来

    2.1 创建XML文件

    我们需要在/res/xml(如果没有就自己创建)创建一个根节点为PreferenceScreen的XML文件这个文件的名字就叫做pref_settings.xml(也可以叫其他的名字,这里Google推荐的命名方式是以pref开头),然后再其中添加以Preference及其子类的节点

    2.1.1 创建屏幕

    我们需要创建一个能够显示设置信息的页面,这个页面被称为屏幕。PreferenceScreen是一个屏幕的标签,表明其中的内容处于一个设置屏幕中,该标签可以嵌套,其效果就是能够打开一个设置的子屏幕。

    <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    </PreferenceScreen>
    

    2.1.2 添加分类

    我们向布局中添加第一个元素PreferenceCategory,该标签并不是一个实际的选项,而是代表了设置中的分类标签,可以参考顶部图0-1片上的红色字体,比如用户下的三个选项其实都是该PreferenceCategory标签的子标签。当然这个标签并不是必须的,如果我们设置比较少可以不使用此标签。注意:其中用到的字符串全都被定义在了string_pref.xml文件中。

        <PreferenceCategory
            android:key="@string/pref_key_user_settings"
            android:title="@string/pref_title_user_settings">
        </PreferenceCategory>
    

    我们看到PreferenceCategory中使用了两个属性值,一个是key一个是title

    • key:对应这个Preference存储在SP文件中的键值(key),我们可以在程序的任意位置通过SP文件使用key查找到该值,当然如果我们的设置项的目的不是存储数据(比如点击打开浏览器,点击退出登录等)也可以不添加key
    • defaultValue:表示当前键的默认值,比如第一次打开设置页面时,SP文件中并没有此项数据,就会显示这个默认值。可以不填。
    • title:这个title就是显示在设置项上的标题,不设置的话显示空白。

    2.1.3 添加第一个设置项

    现在我们需要在页面上创建一个设置项,这个项属于用户组,所以应该被添加在上面的组标签中。该项的作用就是在检查当前的登录状态,显示用户信息,可以看出该项是运行相关的,所以我们只需要添加一个空的Preference标签,剩下的操作全部在代码中由运行时完成,但是为了方便在代码中查找,我还是给它添加了一个key

            <Preference android:key="@string/pref_key_user_info" />
    

    2.1.4 添加一个带EditText的设置项

    我们看用户组中的开通会员这一项,我们现在模拟选择会员的开通,用户填写想要开通的月份,然后将月份写在SP文件中对应的Key上。为了完成这个效果,我们使用EditTextPreference

            <EditTextPreference
                android:defaultValue="0"
                android:inputType="number"
                android:key="@string/pref_key_user_vip"
                android:summary="@string/pref_summary_user_vip"
                android:title="@string/pref_title_user_vip" />
    
    图2.1.4-1 EditTextPreference
    这里我们解释一下,这个EditTextPreference从名字就能看出其内部封装了EditText,而且从效果上看,其中还封装了Dialog。并且我们从其源码中也能发现这些组件。所以我们可以这里使用EditText的属性(比如设置输入类型为数字)和Dialog的全部属性。summary属性的含义是摘要,也就是出现在标题下的小字,一般用于描述该设置的作用。当我们点击确定按钮时,其就会将我们键入的内容存储到SP文件中(虽然我们选择了键入类型为number但是其仍然会以String类型保存,其实是除了Switch以Boolean保存外其他都是String,所以我们在从SP中读取数据时一定要注意类型)

    2.1.5 添加一个SwitchPreference

    省略创建网络分类的过程。详情查看源码

            <SwitchPreference
                android:key="@string/pref_key_net_offline"
                android:summary="@string/pref_summary_net_offline"
                android:title="@string/pref_title_net_offline" />
    

    这个效果如图0-1离线模式一项,右边添加了一个SwitchButton,这个数据会以Boolean类型的形式添加进SP文件中。

    2.1.6 嵌套一个设置屏幕

    如果我们需要点击某一项去打开更加详细的设置屏幕时,可以通过嵌套的方式实现

    <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
            ...
            <PreferenceScreen
                android:key="@string/pref_key_net_flow"
                android:title="@string/pref_title_net_flow">
                ...
            </PreferenceScreen>
            ...
    </PreferenceScreen>
    

    这个屏幕对应了图0-1的网络选择按钮。其效果如图所示

    图2.1.6-1 EditTextPreference

    2.1.7 创建一个多选设置项

    多选在SP文件中对应的是StringSet格式,我们先看代码

                <MultiSelectListPreference
                    android:entries="@array/pref_entries_net_flow"
                    android:entryValues="@array/pref_entryValues_net_flow"
                    android:key="@string/pref_key_net_flow_change"
                    android:title="@string/pref_title_net_flow_change" />
    

    Android为多选的设置提供了一个控件MultiSelectListPreference,这个控件封装了Dialog,用来显示多选选项(其内部封装了什么控件,我们就能使用什么属性),其中主要的属性有

    • entries:对应一个数组,这个数组与要在布局上显示的选项名称相对应
    • entryValues:对应一个数组,这个数组存放的是与entries对应的值,该值会被存储到SP文件中

    注意:由于其对应的SP数据类型为StringSet,所以我们创建的必须是Array<String>

        <string-array name="pref_entries_net_flow">
            <item>2G</item>
            <item>3G</item>
            <item>4G</item>
        </string-array>
    

    演示示例:


    2.1.7-1 多选对话框

    2.1.8 选择铃声

    Android为选择铃声专门设计了一个类RingtonePreference,该类简化了铃声选择的过程,并且在选择铃声之后会返回一个代表该铃声的uri。

            <RingtonePreference
                android:key="@string/pref_key_notify_ring"
                android:title="@string/pref_title_notify_ring" />
    

    这个操作的效果依赖于系统,所以选择界面有可能是弹出对话框,也可能是打开一个新的Activity,这里我就不放图了。


    2.2 在代码中处理事件

    本示例中的代码已提交至GitHub:SettingsActivity.kt 。我们在2.1中只是创建了基本的设置页面布局,在2.2中我们将把这个xml布局通过Fragment显示出来,并且就其中某些Preference项的处理作出说明。

    2.2.1 创建PreferenceFragment

    对于Android 3.0以下的应用,我们需要在 Activity 中显示设置,应当扩展 PreferenceActivity 类。这是传统 Activity 类的扩展,该类根据 Preference 对象的层次结构显示设置列表。当用户进行更改时,PreferenceActivity 会自动保留与每个 Preference 相关的设置。[1]

    对 Android 3.0 及 更高版本系统的应用,则应改为使用 PreferenceFragment。与仅使用上述 Activity 相比,无论在构建何种 Activity,Fragment都可为应用提供一个更加灵活的体系结构。

    我们仅仅需要在onCreate()方法中使用addPreferencesFromResource()方法,将2.1中创建的xml文件引入即可。

        class SettingsFragment : PreferenceFragment() {
    
            override fun onCreate(savedInstanceState: Bundle?) {
                super.onCreate(savedInstanceState)
                addPreferencesFromResource(R.xml.pref_settings)
            }
        }
    

    2.2.2 将PreferenceFragment加入到我们的Activity中

    class SettingsActivity : BaseActivity<IPresenter>() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_settings)
            val settingsFragment = SettingsFragment()
            fragmentManager.beginTransaction()
                    .replace(R.id.settingsFrameLayout, settingsFragment)
                    .commit()
        }
    }
    

    2.2.3 刷新处理

    下述代码的含义是:在Fragment的显示时,监听网络状态,并且在网络状态改变时刷新界面,当Fragment不可见时注销状态监听。

            var netTag = ""
    
            override fun onStart() {
                super.onStart()
                register()
                netTag = NetState.subscribe {
                    flush()
                }
            }
    
            override fun onStop() {
                NetState.unSubscribe(netTag)
                super.onStop()
            }
    

    2.2.4 用户信息处理

    我们在2.1.3中添加了一个空的Preference项,其目的是用来显示用户的信息而非用作设置选项。其中我们定义了一个通过key的资源ID去查找一个Preference的方法。

            /**
             * 刷新用户信息
             */
            private fun flushUserInfo() {
                val pref = findPreference(R.string.pref_key_user_info)
                val user = UserHolder.getUser()
                if (user == null) {
                    findPreference(R.string.pref_key_user_logout).isEnabled = false
                    if (NetState.state.value > 0) {
                        pref.title = "未登录"
                        pref.summary = "点击登录"
                        pref.intent = Intent(activity, LoginActivity::class.java)
                    } else {
                        pref.title = "网络连接失败"
                        pref.summary = "点击打开网络连接"
                        pref.intent = Intent(Settings.ACTION_WIFI_SETTINGS)
                    }
                } else {
                    findPreference(R.string.pref_key_user_logout).isEnabled = true
                    pref.title = "您好 陈小默"
                    pref.summary = "您当前的积分:${user.score}"
                    pref.intent = null
                }
            }
    

    对于上面的例子,我们不需要去管其中的逻辑关系是什么,而其中的title和summary又和xml中的表述相同,我们在此需要详细介绍一下其中的intent属性。Preference在其中封装了一个intent属性,在我们没有给Preference设置点击监听的情况下,其默认的打开通过intent打开一个activity,以下是API 24中的方法源码(使用kotlin改写),可以看到,其主要步骤是

    • 1,判断状态可用。
    • 2,如果注册监听,并且其返回值为true的情况下退出,也就是说如果我们注册点击事件监听后还想让他能够向下进行的话,需要返回false
    • 3,如果当前Preference有层级关系的话,需要依次调用其点击事件方法
    • 4,只有前面几关都通过了的话,最后一步才是通过intent打开Activity
        fun performClick(preferenceScreen: PreferenceScreen?) {
            if (!isEnabled())return
            
            onClick()
            
            if (mOnClickListener != null && mOnClickListener.onPreferenceClick(this)) return
            
            val preferenceManager = getPreferenceManager()
            if (preferenceManager != null) {
                val listener = preferenceManager.getOnPreferenceTreeClickListener()
                if (preferenceScreen != null && listener != null
                        && listener.onPreferenceTreeClick(preferenceScreen, this)) return
            }
            
            if (mIntent != null) {
                val context = getContext()
                context.startActivity(mIntent)
            }
        }
    

    2.2.5 属性值改变的回调方法

    我们在2.1.4中创建了一个用于设置会员时长的选项,但是我们需要在值改变之后立即刷新怎么办?其实Preference中提供了两种监听类型

    • setOnPreferenceChangeListener:该方法注册了监听值改变的回调,如果我们两次设置的数据相同,则回调不发生。
    • setOnPreferenceClickListener:该方法注册了监听点击事件的回调,当isEnable为false时,回调不发生,当返回值为false时,intent可被执行(参看2.2.4源码)

    对于这里我们采用第一种监听

                val userVipPref = findPreference(R.string.pref_key_user_vip)
                userVipPref.setOnPreferenceChangeListener { pref, value ->
                    flushUserVip(pref,value)
                    true
                }
                
            /**
             * 刷新用户会员状态
             */
            private fun flushUserVip(preference: Preference? = null, value: Any? = null) {
                val pref = preference ?: findPreference(R.string.pref_key_user_vip)
                ...
                if (value != null) {
                    ...
                } else {
                    val v = get(R.string.pref_key_user_vip, "0").toInt()
                    ...
                }
            }
    

    这里我们需要考虑一个问题,为什么需要将值传递过来,而不是从SP文件中获取?这是因为当其回调时,这个新值并没有被存放到SP文件中,所以此时我们从SP文件中查找到的仍是上一次的旧数据。

    2.2.6 多选设置的处理

    在2.1.7中我们添加了一个能够使用多选的设置,其特殊之处就是其从SP文件中取出的格式既不是String也不是基本数据类型,而是Set<String>,所以我们还需要一个getSet的方法

            private fun getStringSet(resId: Int, defaultValue: Set<String>? = null): Set<String>? {
                val key = getString(resId)
                return preferenceManager.sharedPreferences.getStringSet(key, defaultValue)
            }
    

    2.2.7 铃声选项的处理

    铃声选择的回调结果是一个表示URI的字符串,如果我们需要得到铃声相关的信息,可以这么做

                val uri = Uri.parse(content ?: get(R.string.pref_key_notify_ring))//构建Uri
                val ring = RingtoneManager.getRingtone(activity, uri)//从Uri中解析铃声对象
                val name = ring.getTitle(activity)//获取铃声名称
                ring.play()//播放铃声
                ring.stop()//结束铃声
                ring.isPlaying//铃声的播放状态
    

    三、其他说明

    3.1 Intent

    上述示例已经讲述了在代码中使用Intent的过程,对于一些固定作用的Intent,我们可以在xml文件中对其进行定义

    <Preference android:title="@string/prefs_web_page" >
        <intent android:action="android.intent.action.VIEW"
                android:data="http://www.example.com" />
    </Preference>
    

    您可以使用以下属性创建隐式和显式 Intent:

    • android:action 要分配的操作(按照 setAction() 方法)。
    • android:data 要分配的数据(按照 setData() 方法)。
    • android:mimeType 要分配的 MIME 类型(按照 setType() 方法)。
    • android:targetClass 组件名称的类部分(按照 setComponent() 方法)。
    • android:targetPackage 组件名称的软件包部分(按照 setComponent() 方法)。

    3.2 对任首选项进行监听

    由于某些原因,你可能希望在用户更改任一选项时立即收到通知。通过 SharedPreference.OnSharedPreferenceChangeListener 接口我们可以在任意选项被点击时收到回调,并通过调用 registerOnSharedPreferenceChangeListener() 为 SharedPreferences 对象注册侦听器。

    注意:选项管理器并不会存储我们监听对象的强引用,所以如果我们采用匿名方式设置监听,很容易在未来的某一时刻被GC回收。

    例如,在以下代码中,调用方未保留对侦听器的引用。 因此,侦听器将容易被当作垃圾回收,并在将来某个不确定的时间失败:

    preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener { pref, key ->
        Log.e("TAG","key:$key")
    }
    

    所以,我们应该自己保存监听的强引用,并在需要的时候主动解除监听

            val l = SharedPreferences.OnSharedPreferenceChangeListener { pref, key ->
                Log.e("TAG", "key:$key")
            }
    
            override fun onStart() {
                super.onStart()
                preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(l)
            }
    
            override fun onStop() {
                preferenceScreen.sharedPreferences.unregisterOnSharedPreferenceChangeListener(l)
                super.onStop()
            }
    

    3.3 管理网络使用情况

    从 Android 4.0 开始,通过系统的“设置”应用,用户可以了解自己的应用在前台和后台使用的网络数据量。然后,用户可以据此禁止具体的应用使用后台数据。 为了避免用户禁止您的应用从后台访问数据,您应该有效地使用数据连接,并允许用户通过应用设置优化应用的数据使用。

    例如,您可以允许用户控制应用同步数据的频率,控制应用是否仅在有 Wi-Fi 时才执行上传/下载操作,以及控制应用能否在漫游时使用数据,等等。为用户提供这些控件后,即使数据使用量接近他们在系统“设置”中设置的限制,他们也不大可能禁止您的应用访问数据,因为他们可以精确地控制应用使用的数据量。

    在 PreferenceActivity 中添加必要的首选项来控制应用的数据使用习惯后,您应立即在清单文件中为 ACTION_MANAGE_NETWORK_USAGE 添加 Intent 过滤器。例如:

    <activity android:name="SettingsActivity" ... >
        <intent-filter>
           <action android:name="android.intent.action.MANAGE_NETWORK_USAGE" />
           <category android:name="android.intent.category.DEFAULT" />
        </intent-filter>
    </activity>
    

    当我们添加了此过滤器后,用于在“系统设置”中对App的网络设置都会打开此App的设置页面

    3.4 自定义Preference

    Android 框架包括各种 Preference 子类,您可以使用它们为各种不同类型的设置构建 UI。不过,您可能会发现自己需要的设置没有内置解决方案,例如,数字选取器或日期选取器。 在这种情况下,您将需要通过扩展 Preference 类或其他子类之一来创建自定义选项。

    扩展 Preference 类时,您需要执行以下几项重要操作:

    • 指定在用户选择设置时显示的用户界面。
    • 适时保存设置的值。
    • 使用显示的当前(默认)值初始化 Preference。
    • 在系统请求时提供默认值。
    • 如果 Preference 提供自己的 UI(例如对话框),请保存并恢复状态以处理生命周期变更(例如,用户旋转屏幕)。

    3.4.1 指定用户界面

    如果您要直接扩展 Preference 类,则需要实现 onClick() 来定义在用户选择该项时发生的操作。不过,大多数自定义设置都会扩展 DialogPreference 以显示对话框,从而简化这一过程。扩展 DialogPreference 时,必须在类构造函数中调用 setDialogLayoutResourcs() 来指定对话框的布局。

    例如,自定义 DialogPreference 可以使用下面的构造函数来声明布局并为默认的肯定和否定对话框按钮指定文本:

    class NumberPickerPreference(context: Context?,
                                 attrs: AttributeSet? = null,
                                 defStyleAttr: Int = 0,
                                 defStyleRes: Int = 0) : DialogPreference(context, attrs, defStyleAttr, defStyleRes) {
        init {
            dialogLayoutResource = R.layout.numberpicker_dialog
            setPositiveButtonText(android.R.string.ok)
            setNegativeButtonText(android.R.string.cancel)
            dialogIcon = null
        }
    }
    

    3.4.2 保存设置的值

    如果设置的值为整型数或是用于保存布尔值的 persistBoolean(),则可通过调用 Preference 类的一个 persist*() 方法(如 persistInt())随时保存该值。

    注:每个 Preference 均只能保存一种数据类型,因此您必须使用适合自定义 Preference 所用数据类型的 persist*() 方法。

    至于何时选择保留设置,则可能取决于要扩展的 Preference 类。如果扩展 DialogPreference,则只能在对话框因肯定结果(用户选择“确定”按钮)而关闭时保留该值。

    当 DialogPreference 关闭时,系统会调用 onDialogClosed() 方法。该方法包括一个布尔参数,用于指定用户结果是否为“肯定”;如果值为 true,则表示用户选择的是肯定按钮且您应该保存新值。 例如:

        var result: String? = null
        val editText: EditText = EditText(getContext())
    
        override fun onDialogClosed(positiveResult: Boolean) {
            if (positiveResult) {
                result = editText.text.toString()
                persistString(editText.text.toString())
            }
        }
    

    3.4.3 初始化当前值

    系统将 Preference 添加到屏幕时,会调用 onSetInitialValue() 来通知您设置是否具有保留值。如果没有保留值,则此调用将为您提供默认值。

    onSetInitialValue() 方法传递一个布尔值 (restorePersistedValue),以指示是否已为该设置保留值。 如果值为 true,则应通过调用 Preference 类的一个 getPersisted*() 方法(如整型值对应的 getPersistedInt())来检索保留值。通常,您会需要检索保留值,以便能够正确更新 UI 来反映之前保存的值。

    如果 restorePersistedValue 为 false,则应使用在第二个参数中传递的默认值。

        val DEFAULT_VALUE=""
        override fun onSetInitialValue(restorePersistedValue: Boolean, defaultValue: Any?) {
            if (restorePersistedValue) {
                // Restore existing state
                result = getPersistedString(DEFAULT_VALUE)
            } else {
                // Set default state from the XML attribute
                result = defaultValue as String
                persistString(result)
            }
        }
    

    每种 getPersisted*() 方法均采用一个参数,用于指定在实际上没有保留值或该键不存在时所要使用的默认值。在上述示例中,当 getPersistedInt() 不能返回保留值时,局部常量用于指定默认值。

    注意:不能使用 defaultValue 作为 getPersisted*() 方法中的默认值,因为当 restorePersistedValue 为 true 时,其值始终为 null。

    3.4.4 提供默认值

    如果 Preference 类的实例指定一个默认值(使用 android:defaultValue 属性),则在实例化对象以检索该值时,系统会调用 onGetDefaultValue()。您必须实现此方法,系统才能将默认值保存在 SharedPreferences 中。 例如:

        override fun onGetDefaultValue(a: TypedArray?, index: Int): Any {
            return a?.getString(index)?:DEFAULT_VALUE
        }
    

    方法参数可提供您所需的一切:属性的数组和 android:defaultValue(必须检索的值)的索引位置。之所以必须实现此方法以从该属性中提取默认值,是因为您必须为此属性指定在未定义属性值时所要使用的局部默认值。

    3.4.5 保存和恢复首选项的状态

    正如布局中的 View 一样,在重启 Activity 或片段时(例如,用户旋转屏幕),Preference 子类也负责保存并恢复其状态。要正确保存并恢复 Preference 类的状态,您必须实现生命周期回调方法 onSaveInstanceState() 和 onRestoreInstanceState()。

    Preference 的状态由实现 Parcelable 接口的对象定义。Android 框架为您提供此类对象,作为定义状态对象(Preference.BaseSavedState 类)的起点。

    要定义 Preference 类保存其状态的方式,您应该扩展 Preference.BaseSavedState 类。您只需重写几种方法并定义 CREATOR 对象。

    对于大多数应用,如果 Preference 子类保存除整型数以外的其他数据类型,则可复制下列实现并直接更改处理 value 的行。

        class SaveState : BaseSavedState {
            constructor(source: Parcel) : super(source) {
                value = source.readString()
            }
    
            constructor(superState: Parcelable) : super(superState)
    
            var value: String? = null
    
            override fun writeToParcel(dest: Parcel, flags: Int) {
                super.writeToParcel(dest, flags)
                dest.writeString(value)
            }
    
            companion object {
                @JvmStatic val CREATOR = object : Parcelable.Creator<SaveState>() {
                    override fun newArray(size: Int): Array<SaveState> {
                        return newArray(size)
                    }
    
                    override fun createFromParcel(source: Parcel): SaveState {
                        return SaveState(source)
                    }
                }
            }
        }
    

    如果将上述 Preference.BaseSavedState 实现添加到您的应用(通常,作为 Preference 子类的子类),则需要为 Preference 子类实现 onSaveInstanceState() 和 onRestoreInstanceState() 方法。

        override fun onSaveInstanceState(): Parcelable {
            val superState = super.onSaveInstanceState()
            if (isPersistent) {
                return superState
            }
            val myState = SaveState(superState)
            myState.value = result
            return myState
        }
    
        override fun onRestoreInstanceState(state: Parcelable?) {
            if (state == null || state.javaClass != SaveState::class.java) {
                super.onRestoreInstanceState(state)
                return
            }
            val myState = state as SaveState
            super.onRestoreInstanceState(myState.superState)
        }
    

    3.5 在大屏幕上使用分屏

    大屏幕上的分屏效果

    在极少数情况下,您可能需要设计设置,使第一个屏幕仅显示子屏幕的列表(例如在系统“设置”应用中,如图 4 和图 5 所示)。 在开发针对 Android 3.0 及更高版本系统的此类设计时,您应该使用 Android 3.0 中的新“标头”功能,而非使用嵌套的 PreferenceScreen 元素构建子屏幕。

    要使用标头构建设置,您需要:

    • 将每组设置分成单独的 PreferenceFragment 实例。即,每组设置均需要一个单独的 XML 文件。
    • 创建 XML 标头文件,其中列出每个设置组并声明哪个片段包含对应的设置列表。
    • 扩展 PreferenceActivity 类以托管设置。
    • 实现 onBuildHeaders() 回调以指定标头文件。

    使用此设计的一大好处是,在大屏幕上运行时,PreferenceActivity 会自动提供双窗格布局(如图 4 所示)。

    即使您的应用支持早于 3.0 的 Android 版本,您仍可将应用设计为使用 PreferenceFragment 在较新版本的设备上呈现双窗格布局,同时仍支持较旧版本设备上传统的多屏幕层次结构。

    1.标头用 XML 标头文件定义。
    2.每组设置均由 PreferenceFragment(通过标头文件中的 <header> 元素指定)定义。

    3.5.1 创建标头文件

    <?xml version="1.0" encoding="utf-8"?>
    <preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
        <header
            android:fragment="com.example.prefs.SettingsActivity$SettingsFragmentOne"
            android:title="@string/prefs_category_one"
            android:summary="@string/prefs_summ_category_one" />
        <header
            android:fragment="com.example.prefs.SettingsActivity$SettingsFragmentTwo"
            android:title="@string/prefs_category_two"
            android:summary="@string/prefs_summ_category_two" >
            <!-- key/value pairs can be included as arguments for the fragment. -->
            <extra android:name="someKey" android:value="someHeaderValue" />
        </header>
    </preference-headers>
    

    每个标头均可使用 android:fragment 属性声明在用户选择该标头时应打开的 PreferenceFragment 实例。

    <extras> 元素允许您使用 Bundle 将键值对传递给片段。该片段可以通过调用 getArguments() 检索参数。您向该片段传递参数的原因可能有很多,不过一个重要原因是,要对每个组重复使用 PreferenceFragment 的相同子类,而且要使用参数来指定该片段应加载哪些首选项 XML 文件。

    例如,当每个标头均使用 "settings" 键定义 <extra> 参数时,则可以对多个设置组重复使用以下片段:

    class SettingsFragment: PreferenceFragment() {
        
        override onCreate(savedInstanceState: Bundle) {
            super.onCreate(savedInstanceState)
    
            val settings = arguments.getString("settings")
            if ("notifications".equals(settings)) {
                addPreferencesFromResource(R.xml.settings_wifi)
            } else if ("sync".equals(settings)) {
                addPreferencesFromResource(R.xml.settings_sync)
            }
        }
    }
    

    3.5.2 显示标头

    要显示首选项标头,您必须实现 onBuildHeaders() 回调方法并调用 loadHeadersFromResource()。例如:

    class SettingsActivity extends PreferenceActivity() {
        
        override onBuildHeaders(target: List<Header>) {
            loadHeadersFromResource(R.xml.preference_headers, target)
        }
    }
    

    当用户从标头列表中选择一个项目时,系统会打开相关的 PreferenceFragment。

    注:使用首选项标头时,PreferenceActivity 的子类无需实现 onCreate() 方法,因为 Activity 唯一所需执行的任务就是加载标头。

    3.5.3 使用首选项标头支持旧版本

    如果您的应用支持早于 3.0 的 Android 版本,则在 Android 3.0 及更高版本系统上运行时,您仍可使用标头提供双窗格数据。为此,您只需另外创建 一个使用基本 <Preference> 元素的首选项 XML 文件即可,这些基本元素的行为方式与标头项目类似(供较旧版本的 Android 系统使用)。

    但是,每个 <Preference> 元素均会向 PreferenceActivity 发送一个 Intent,指定要加载哪个首选项 XML 文件,而不是打开新的 PreferenceScreen。

    例如,下面就是一个用于 Android 3.0 及更高版本系统的首选项标头 XML 文件 (res/xml/preference_headers.xml):

    <preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
        <header
            android:fragment="com.example.prefs.SettingsFragmentOne"
            android:title="@string/prefs_category_one"
            android:summary="@string/prefs_summ_category_one" />
        <header
            android:fragment="com.example.prefs.SettingsFragmentTwo"
            android:title="@string/prefs_category_two"
            android:summary="@string/prefs_summ_category_two" />
    </preference-headers>
    

    下面是为早于 Android 3.0 版本的系统提供相同标头的首选项文件 (res/xml/preference_headers_legacy.xml):

    <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
        <Preference
            android:title="@string/prefs_category_one"
            android:summary="@string/prefs_summ_category_one"  >
            <intent
                android:targetPackage="com.example.prefs"
                android:targetClass="com.example.prefs.SettingsActivity"
                android:action="com.example.prefs.PREFS_ONE" />
        </Preference>
        <Preference
            android:title="@string/prefs_category_two"
            android:summary="@string/prefs_summ_category_two" >
            <intent
                android:targetPackage="com.example.prefs"
                android:targetClass="com.example.prefs.SettingsActivity"
                android:action="com.example.prefs.PREFS_TWO" />
        </Preference>
    </PreferenceScreen>
    

    由于是从 Android 3.0 开始方添加对 <preference-headers> 的支持,因此只有在 Androd 3.0 或更高版本中运行时,系统才会在您的 PreferenceActivity 中调用 onBuildHeaders()。要加载“旧版”标头文件 (preference_headers_legacy.xml),您必须检查 Android 版本,如果版本低于 Android 3.0 (HONEYCOMB),请调用 addPreferencesFromResource() 来加载旧版标头文件。例如:

    overrided onCreate(savedInstanceState: Bundle) {
        super.onCreate(savedInstanceState)
        ...
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
            // Load the legacy preferences headers
            addPreferencesFromResource(R.xml.preference_headers_legacy)
        }
    }
    
    // Called only on Honeycomb and later
    override onBuildHeaders(target: List<Header>) {
       loadHeadersFromResource(R.xml.preference_headers, target)
    }
    

    最后要做的就是处理传入 Activity 的 Intent,以确定要加载的首选项文件。因此,请检索 Intent 的操作,并将其与在首选项 XML 的 <intent> 标记中使用的已知操作字符串进行比较。

    val ACTION_PREFS_ONE = "com.example.prefs.PREFS_ONE"
    ...
    
    override onCreate(savedInstanceState: Bundle) {
        super.onCreate(savedInstanceState)
    
        val action = intent.action;
        if (action != null && action.equals(ACTION_PREFS_ONE)) {
            addPreferencesFromResource(R.xml.preferences)
        }
        ...
    
        else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
            // Load the legacy preferences headers
            addPreferencesFromResource(R.xml.preference_headers_legacy)
        }
    }
    

    值得注意的是,连续调用 addPreferencesFromResource() 会将所有首选项堆叠在一个列表中,因此请将条件与 else-if 语句链接在一起,确保它只调用一次。


    1. https://developer.android.com/guide/topics/ui/settings.html#Activity

    相关文章

      网友评论

      • 随机取名的:有没有办法能够让4.0和5.0的界面表示一致?
        798fe2ac8685:@随机取名的 自定义没有统一标准,看你产品经理怎么设计就怎么实现
        随机取名的:@陈小默 有自定义的教程吗?
        798fe2ac8685:如果需要界面一致的话就不要用这种方法了,还是自定义来的靠谱
      • 梦龙DRAG0N:找了一大堆preference的文章,其他的都是不知所云,就你的详细😂
      • 叮宕:见过写的最详细的。
      • reezy:基本上不用,丑爆了,而且坑多
        自己写布局还更简单可控
        798fe2ac8685:@mezy 我在上面的回复就说过了,如果App是使用Material-Design风格的话,基本上只能使用这个方式去做设置页面,否则风格不统一。如果APP不是MD风格的话,也没人会去用这个方式实现
      • GEM的紫领巾:第一次知道这种用法,是不是很少人用呢??
        798fe2ac8685:@GEM的紫领巾 如果App选用的是Material-Design风格的话,为了保证风格一致,设置页面最好采用这种形式,如果App选用的是其他风格的话,可以另行实现
      • 433b60343a21:可是google为什么弃用这几个方法啊
        798fe2ac8685:@LeeYudE Android的API文档上并没有说它被弃用了呀
      • 433b60343a21:class SettingsFragment : PreferenceFragment() {

        override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        addPreferencesFromResource(R.xml.pref_settings)
        }
        }
        这是什么写法啊
        798fe2ac8685:@陈小默 不少公司已经使用它作为App开发语言了,比如魅族和豆瓣,你可以学习一下,相信你会爱上它的
        798fe2ac8685:@LeeYudE kotlin语言,基于JVM的
      • 十方天仪君:写的不错,先码估计以后会用到

      本文标题:Android:详解如何创建Google风格的SettingsA

      本文链接:https://www.haomeiwen.com/subject/essqyttx.html