Android:详解如何创建Google风格的SettingsActivity
Julio5468
8年前
<p>话不多说先上图。从下面的效果我们可以看出在Android在5.0以上对布局进行了大规模的美化,在4.4上运行感觉就是相当的吃藕。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/cf5fb3e921b18e5029bcd90603a36bc6.png"></p> <p style="text-align:center">图0-1 Android 7.0 模拟器上的效果</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/1cb0abd2a583a7169f9766e07e5a8469.jpg"></p> <p style="text-align:center">图0-2 Android 4.4 荣耀上的效果</p> <p>如果你是第一次接触Android中的Settings,最好不要直接从Android Studio中快捷创建,就是下图这个快捷创建界面。如果你真的这么干了,你应该就能体会到什么叫做花样懵逼了。所以这篇博客目的就是新手入门手把手教学。本博客所有内容源自毕业设计,代码已提交到Github,点这里 <a href="/misc/goto?guid=4959717288465169341" rel="nofollow,noindex">任意门</a> 穿越到Github查看源码</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/783de1d2a401bb01104b4a70abdd6284.png"></p> <p style="text-align:center">图0-3 Android Studio快捷创建Activity的界面</p> <h2><strong>目录</strong></h2> <p>[toc]</p> <h2><strong>一、概念介绍</strong></h2> <h3><strong>1.1 Android设置界面的创建理念</strong></h3> <p>对于Android开发来说,程序的配置信息可以有不同的存储方式,比如文件、数据库、SharedPreferences(以下简称SP)、甚至是网络都可以用来存储配置。其中公认的最优存储方式就是SP文件了。所以Android中的Settings页面的所有操作的基础就是SP文件的操作。所以Android API定义了一个Preference类封装了对SP文件的操作。</p> <h3><strong>1.2 布局元素</strong></h3> <p>我们先来看一下可以Preference类的所有子类</p> <ul> <li>CheckBoxPreference</li> <li>DialogPreference</li> <li>EditTextPreference</li> <li>ListPreference</li> <li>MultiSelectListPreference</li> <li>RingtonPreference</li> <li>SwitchPreference</li> <li>TowStatePreference</li> </ul> <p>后面我们会详细介绍和使用这些控件的</p> <h2><strong>二、开始创建</strong></h2> <p>一个设置页面的创建步骤分为三步:</p> <ul> <li>创建Xml文件</li> <li>创建PreferenceFragment,并在其中加载我们创建的Xml文件</li> <li>在Activity中显示PreferenceFragment</li> </ul> <p>下面我会将程序中的部分过程展示出来,其他的详细代码请参阅 <a href="/misc/goto?guid=4959717288465169341" rel="nofollow,noindex">GitHub</a></p> <h3><strong>2.1 创建XML文件</strong></h3> <p>我们需要在 /res/xml (如果没有就自己创建)创建一个根节点为 PreferenceScreen 的XML文件这个文件的名字就叫做 pref_settings.xml (也可以叫其他的名字,这里Google推荐的命名方式是以pref开头),然后再其中添加以Preference及其子类的节点</p> <p><strong>2.1.1 创建屏幕</strong></p> <p>我们需要创建一个能够显示设置信息的页面,这个页面被称为屏幕。 PreferenceScreen 是一个屏幕的标签,表明其中的内容处于一个设置屏幕中,该标签可以嵌套,其效果就是能够打开一个设置的子屏幕。</p> <pre> <code class="language-java"><PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> </PreferenceScreen></code></pre> <p><strong>2.1.2 添加分类</strong></p> <p>我们向布局中添加第一个元素 PreferenceCategory ,该标签并不是一个实际的选项,而是代表了设置中的分类标签,可以参考顶部 图0-1 片上的红色字体,比如 用户 下的三个选项其实都是该 PreferenceCategory 标签的子标签。当然这个标签并不是必须的,如果我们设置比较少可以不使用此标签。注意:其中用到的字符串全都被定义在了 <a href="/misc/goto?guid=4959717288592296222" rel="nofollow,noindex">string_pref.xml</a> 文件中。</p> <pre> <code class="language-java"><PreferenceCategory android:key="@string/pref_key_user_settings" android:title="@string/pref_title_user_settings"> </PreferenceCategory></code></pre> <p>我们看到 PreferenceCategory 中使用了两个属性值,一个是 key 一个是 title</p> <ul> <li>key:对应这个Preference存储在SP文件中的键值(key),我们可以在程序的任意位置在SP文件中使用这个key查找到该值,当然如果我们的设置项的目的不是存储数据(比如点击打开浏览器,点击退出登录等)也可以不添加key</li> <li>defaultValue:表示当前键的默认值,比如第一次打开设置页面时,SP文件中并没有此项数据,就会显示这个默认值。可以不填。</li> <li>title:这个title就是显示在设置项上的标题,不设置的话显示空白</li> </ul> <p><strong>2.1.3 添加第一个设置项</strong></p> <p>现在我们需要在页面上创建一个设置项,这个项属于用户组,所以应该被添加在上面的组标签中,该项的作用就是在检查当前的登录状态,显示用户信息,可以看出该项是运行相关的,所以我们只需要添加一个空的 Preference 标签,剩下的操作全部在代码中由运行时完成,但是为了方便在代码中查找,我还是给它添加了一个key</p> <pre> <code class="language-java"><Preference android:key="@string/pref_key_user_info" /></code></pre> <p><strong>2.1.4 添加一个带EditText的设置项</strong></p> <p>我们看用户组中的 开通会员 这一项,我们现在模拟选择会员的开通,用户填写想要开通的月份,然后将月份写在SP文件中对应的Key上。为了完成这个效果,我们使用 EditTextPreference</p> <pre> <code class="language-java"><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" /></code></pre> <p style="text-align:center"><img src="https://simg.open-open.com/show/d613a8f2f168475afaa40f959379feca.png"></p> <p style="text-align:center">图2.1.4-1 EditTextPreference</p> <p>这里我们解释一下,这个 EditTextPreference 从名字就能看出其内部封装了EditText,而且从效果上看,其中还封装了Dialog。并且我们从其源码中也能发现这些组件。所以我们可以这里使用EditText的属性(比如设置输入类型为数字)和Dialog的全部属性。summary属性的含义是 摘要 ,也就是出现在标题下的小字,一般用于描述该设置的作用。当我们点击确定按钮时,其就会将我们键入的内容存储到SP文件中(虽然我们选择了键入类型为number但是其仍然会以String类型保存,其实是除了Switch已Boolean保存外其他都是String,所以我们在从SP中读取数据时一定要注意)</p> <p><strong>2.1.5 添加一个SwitchPreference</strong></p> <p>省略创建网络分类的过程。详情查看 <a href="/misc/goto?guid=4959717288692465927" rel="nofollow,noindex">源码</a></p> <pre> <code class="language-java"><SwitchPreference android:key="@string/pref_key_net_offline" android:summary="@string/pref_summary_net_offline" android:title="@string/pref_title_net_offline" /></code></pre> <p>这个效果如 图0-1 离线模式一项,右边添加了一个SwitchButton,这个数据会以Boolean类型的形式添加进SP文件中。</p> <p><strong>2.1.6 嵌套一个设置屏幕</strong></p> <p>如果我们需要点击某一项去打开更加详细的设置屏幕时,可以通过嵌套的方式实现</p> <pre> <code class="language-java"><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></code></pre> <p>这个屏幕对应了 图0-1 的网络选择按钮。其效果如图所示</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/cf769fdf8f577f706fc9b51249559812.gif"></p> <p style="text-align:center">图2.1.6-1 EditTextPreference</p> <p><strong>2.1.7 创建一个多选设置项</strong></p> <p>多选在SP文件中对应的是StringSet格式,我们先看代码</p> <pre> <code class="language-java"><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" /></code></pre> <p>Android为多选的设置提供了一个控件MultiSelectListPreference,这个控件封装了Dialog,用来显示多选选项(其内部封装了什么控件,我们就能使用什么属性),其中主要的属性有</p> <ul> <li>entries:对应一个数组,这个数组对应要在布局上显示的选项名称</li> <li>entryValues:对应一个数组,这个数组存放的是与entries对应的值,该值会被存储到SP文件中</li> </ul> <p>注意:由于其对应的SP数据类型为StringSet,所以我们创建的必须是 Array<String></p> <pre> <code class="language-java"><string-array name="pref_entries_net_flow"> <item>2G</item> <item>3G</item> <item>4G</item> </string-array></code></pre> <p>演示示例:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/eb416d8ac5533fd241c0664dd8f050b9.png"></p> <p style="text-align:center">2.1.7-1 多选对话框</p> <p><strong>2.1.8 选择铃声</strong></p> <p>Android为选择铃声专门设计了一个类RingtonePreference,该类简化了铃声选择的过程,并且在选择铃声之后会返回一个代表该铃声的uri。</p> <pre> <code class="language-java"><RingtonePreference android:key="@string/pref_key_notify_ring" android:title="@string/pref_title_notify_ring" /></code></pre> <p>这个操作的效果依赖于系统,所以选择界面有可能是弹出对话框,也可能是打开一个新的Activity,这里我就不放图了。</p> <h3><strong>2.2 在代码中处理事件</strong></h3> <p>本示例中的代码已提交至 <a href="/misc/goto?guid=4959717288788655115" rel="nofollow,noindex">GitHub:SettingsActivity.kt</a> 。我们在2.1中只是创建了基本的设置页面布局,在2.2中我们将把这个xml布局通过Fragment显示出来,并且就其中某些Preference项的处理作出说明。</p> <p><strong>2.2.1 创建PreferenceFragment</strong></p> <p>对于Android 3.0以下的应用,我们需要在 Activity 中显示您的设置,请扩展 PreferenceActivity 类。这是传统 Activity 类的扩展,该类根据 Preference 对象的层次结构显示设置列表。当用户进行更改时,PreferenceActivity 会自动保留与每个 Preference 相关的设置。 <sup> [1] </sup></p> <p>对 Android 3.0 及 更高版本系统的应用,则应改为使用 PreferenceFragment。与仅使用上述 Activity 相比,无论您在构建何种 Activity,片段都可为应用提供一个更加灵活的体系结构。</p> <p>我们仅仅需要在onCreate()方法中使用addPreferencesFromResource()方法,将2.1中创建的xml文件引入即可。</p> <pre> <code class="language-java">class SettingsFragment : PreferenceFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) addPreferencesFromResource(R.xml.pref_settings) } }</code></pre> <p><strong>2.2.2 将PreferenceFragment加入到我们的Activity中</strong></p> <pre> <code class="language-java">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() } }</code></pre> <p><strong>2.2.3 刷新处理</strong></p> <p>下述代码的含义是:在Fragment的显示时,监听网络状态,并且在网络状态改变时刷新界面,当Fragment不可见时注销状态监听。</p> <pre> <code class="language-java">var netTag = "" override fun onStart() { super.onStart() register() netTag = NetState.subscribe { flush() } } override fun onStop() { NetState.unSubscribe(netTag) super.onStop() }</code></pre> <p><strong>2.2.4 用户信息处理</strong></p> <p>我们在2.1.3中添加了一个空的Preference项,其目的是用来显示用户的信息而非用作设置选项。其中我们定义了一个通过key的资源ID去查找一个Preference的方法。</p> <pre> <code class="language-java">/** * 刷新用户信息 */ 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 } }</code></pre> <p>对于上面的例子,我们不需要去管其中的逻辑关系是什么,而其中的title和summary又和xml中的表述相同,我们在此需要详细介绍一下其中的intent属性。Preference在其中封装了一个intent属性,在我们没有给Preference设置点击监听的情况下,其默认的打开通过intent打开一个activity,以下是API 24中的方法源码(使用kotlin改写),可以看到,其主要步骤是</p> <ul> <li>1,判断状态可用。</li> <li>2,如果注册监听,并且其返回值为true的情况下退出,也就是说如果我们注册点击事件监听后还想让他能够向下进行的话,需要返回false</li> <li>3,如果当前Preference有层级关系的话,需要依次调用其点击事件方法</li> <li> <p>4,只有前面几关都通过了的话,最后一步才是通过intent打开Activity</p> <pre> <code class="language-java">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) } }</code></pre> </li> </ul> <p><strong>2.2.5 属性值改变的回调方法</strong></p> <p>我们在2.1.4中创建了一个用于设置会员时长的选项,但是我们需要在值改变之后立即刷新怎么办?其实Preference中提供了两种监听类型</p> <ul> <li>setOnPreferenceChangeListener:该方法注册了监听值改变的回调,如果我们两次设置的数据相同,则回调不发生。</li> <li>setOnPreferenceClickListener:该方法注册了监听点击事件的回调,当isEnable为false时,回调不发生,当返回值为false时,intent可被执行(参看2.2.4源码)</li> </ul> <p>对于这里我们采用第一种监听</p> <pre> <code class="language-java">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() ... } }</code></pre> <p>这里我们需要考虑一个问题,为什么需要将值传递过来,而不是从SP文件中获取?这是因为当其回调时,这个新值并没有被存放到SP文件中,所以此时我们从SP文件中查找到的仍是上一次的旧数据。</p> <p><strong>2.2.6 多选设置的处理</strong></p> <p>在2.1.7中我们添加了一个能够使用多选的设置,其特殊之处就是其从SP文件中取出的格式既不是 String 也不是基本数据类型,而是 Set<String> ,所以我们还需要一个getSet的方法</p> <pre> <code class="language-java">private fun getStringSet(resId: Int, defaultValue: Set<String>? = null): Set<String>? { val key = getString(resId) return preferenceManager.sharedPreferences.getStringSet(key, defaultValue) }</code></pre> <p><strong>2.2.7 铃声选项的处理</strong></p> <p>铃声选择的回调结果是一个表示URI的字符串,如果我们需要得到铃声相关的信息,可以这么做</p> <pre> <code class="language-java">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//铃声的播放状态</code></pre> <h2><strong>三、其他说明</strong></h2> <h3><strong>3.1 Intent</strong></h3> <p>上述示例已经讲述了在代码中使用Intent的过程,对于一些固定作用的Intent,我们可以在xml文件中对其进行定义</p> <pre> <code class="language-java"><Preference android:title="@string/prefs_web_page" > <intent android:action="android.intent.action.VIEW" android:data="http://www.example.com" /> </Preference></code></pre> <p>您可以使用以下属性创建隐式和显式 Intent:</p> <ul> <li>android:action 要分配的操作(按照 setAction() 方法)。</li> <li>android:data 要分配的数据(按照 setData() 方法)。</li> <li>android:mimeType 要分配的 MIME 类型(按照 setType() 方法)。</li> <li>android:targetClass 组件名称的类部分(按照 setComponent() 方法)。</li> <li>android:targetPackage 组件名称的软件包部分(按照 setComponent() 方法)。</li> </ul> <h3><strong>3.2 对任意首选项进行监听</strong></h3> <p>出于某些原因,您可能希望在用户更改任一首选项时立即收到通知。要在任一首选项发生更改时收到回调,请实现 SharedPreference.OnSharedPreferenceChangeListener 接口,并通过调用 registerOnSharedPreferenceChangeListener() 为 SharedPreferences 对象注册侦听器。</p> <p>注意:目前,首选项管理器不会在您调用 registerOnSharedPreferenceChangeListener() 时存储对侦听器的强引用。但是,您必须存储对侦听器的强引用,否则它将很容易被当作垃圾回收。 我们建议您将对侦听器的引用保存在只要您需要侦听器就会存在的对象的实例数据中。</p> <p>例如,在以下代码中,调用方未保留对侦听器的引用。 因此,侦听器将容易被当作垃圾回收,并在将来某个不确定的时间失败:</p> <pre> <code class="language-java">preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener { pref, key -> Log.e("TAG","key:$key") }</code></pre> <p>有鉴于此,请将对侦听器的引用存储在只要需要侦听器就会存在的对象的实例数据字段中,并在合适的位置接触监听:</p> <pre> <code class="language-java">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() }</code></pre> <h3><strong>3.3 管理网络使用情况</strong></h3> <p>从 Android 4.0 开始,通过系统的“设置”应用,用户可以了解自己的应用在前台和后台使用的网络数据量。然后,用户可以据此禁止具体的应用使用后台数据。 为了避免用户禁止您的应用从后台访问数据,您应该有效地使用数据连接,并允许用户通过应用设置优化应用的数据使用。</p> <p>例如,您可以允许用户控制应用同步数据的频率,控制应用是否仅在有 Wi-Fi 时才执行上传/下载操作,以及控制应用能否在漫游时使用数据,等等。为用户提供这些控件后,即使数据使用量接近他们在系统“设置”中设置的限制,他们也不大可能禁止您的应用访问数据,因为他们可以精确地控制应用使用的数据量。</p> <p>在 PreferenceActivity 中添加必要的首选项来控制应用的数据使用习惯后,您应立即在清单文件中为 ACTION_MANAGE_NETWORK_USAGE 添加 Intent 过滤器。例如:</p> <pre> <code class="language-java"><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></code></pre> <p>此 Intent 过滤器指示系统此 Activity 控制应用的数据使用情况。因此,当用户从系统的“设置”应用检查应用所使用的数据量时,可以使用“查看应用设置”按钮启动 PreferenceActivity,这样,用户就能够优化应用使用的数据量。</p> <h3><strong>3.4 自定义Preference</strong></h3> <p>Android 框架包括各种 Preference 子类,您可以使用它们为各种不同类型的设置构建 UI。不过,您可能会发现自己需要的设置没有内置解决方案,例如,数字选取器或日期选取器。 在这种情况下,您将需要通过扩展 Preference 类或其他子类之一来创建自定义首选项。</p> <p>扩展 Preference 类时,您需要执行以下几项重要操作:</p> <ul> <li>指定在用户选择设置时显示的用户界面。</li> <li>适时保存设置的值。</li> <li>使用显示的当前(默认)值初始化 Preference。</li> <li>在系统请求时提供默认值。</li> <li>如果 Preference 提供自己的 UI(例如对话框),请保存并恢复状态以处理生命周期变更(例如,用户旋转屏幕)。</li> </ul> <p><strong>3.4.1 指定用户界面</strong></p> <p>如果您要直接扩展 Preference 类,则需要实现 onClick() 来定义在用户选择该项时发生的操作。不过,大多数自定义设置都会扩展 DialogPreference 以显示对话框,从而简化这一过程。扩展 DialogPreference 时,必须在类构造函数中调用 setDialogLayoutResourcs() 来指定对话框的布局。</p> <p>例如,自定义 DialogPreference 可以使用下面的构造函数来声明布局并为默认的肯定和否定对话框按钮指定文本:</p> <pre> <code class="language-java">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 } }</code></pre> <p><strong>3.4.2 保存设置的值</strong></p> <p>如果设置的值为整型数或是用于保存布尔值的 persistBoolean(),则可通过调用 Preference 类的一个 persist*() 方法(如 persistInt())随时保存该值。</p> <p>注:每个 Preference 均只能保存一种数据类型,因此您必须使用适合自定义 Preference 所用数据类型的 persist*() 方法。</p> <p>至于何时选择保留设置,则可能取决于要扩展的 Preference 类。如果扩展 DialogPreference,则只能在对话框因肯定结果(用户选择“确定”按钮)而关闭时保留该值。</p> <p>当 DialogPreference 关闭时,系统会调用 onDialogClosed() 方法。该方法包括一个布尔参数,用于指定用户结果是否为“肯定”;如果值为 true,则表示用户选择的是肯定按钮且您应该保存新值。 例如:</p> <pre> <code class="language-java">var result: String? = null val editText: EditText = EditText(getContext()) override fun onDialogClosed(positiveResult: Boolean) { if (positiveResult) { result = editText.text.toString() persistString(editText.text.toString()) } }</code></pre> <p><strong>3.4.3 初始化当前值</strong></p> <p>系统将 Preference 添加到屏幕时,会调用 onSetInitialValue() 来通知您设置是否具有保留值。如果没有保留值,则此调用将为您提供默认值。</p> <p>onSetInitialValue() 方法传递一个布尔值 (restorePersistedValue),以指示是否已为该设置保留值。 如果值为 true,则应通过调用 Preference 类的一个 getPersisted*() 方法(如整型值对应的 getPersistedInt())来检索保留值。通常,您会需要检索保留值,以便能够正确更新 UI 来反映之前保存的值。</p> <p>如果 restorePersistedValue 为 false,则应使用在第二个参数中传递的默认值。</p> <pre> <code class="language-java">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) } }</code></pre> <p>每种 getPersisted*() 方法均采用一个参数,用于指定在实际上没有保留值或该键不存在时所要使用的默认值。在上述示例中,当 getPersistedInt() 不能返回保留值时,局部常量用于指定默认值。</p> <p>注意:不能使用 defaultValue 作为 getPersisted*() 方法中的默认值,因为当 restorePersistedValue 为 true 时,其值始终为 null。</p> <p><strong>3.4.4 提供默认值</strong></p> <p>如果 Preference 类的实例指定一个默认值(使用 android:defaultValue 属性),则在实例化对象以检索该值时,系统会调用 onGetDefaultValue()。您必须实现此方法,系统才能将默认值保存在 SharedPreferences 中。 例如:</p> <pre> <code class="language-java">override fun onGetDefaultValue(a: TypedArray?, index: Int): Any { return a?.getString(index)?:DEFAULT_VALUE }</code></pre> <p>方法参数可提供您所需的一切:属性的数组和 android:defaultValue(必须检索的值)的索引位置。之所以必须实现此方法以从该属性中提取默认值,是因为您必须为此属性指定在未定义属性值时所要使用的局部默认值。</p> <p><strong>3.4.5 保存和恢复首选项的状态</strong></p> <p>正如布局中的 View 一样,在重启 Activity 或片段时(例如,用户旋转屏幕),Preference 子类也负责保存并恢复其状态。要正确保存并恢复 Preference 类的状态,您必须实现生命周期回调方法 onSaveInstanceState() 和 onRestoreInstanceState()。</p> <p>Preference 的状态由实现 Parcelable 接口的对象定义。Android 框架为您提供此类对象,作为定义状态对象(Preference.BaseSavedState 类)的起点。</p> <p>要定义 Preference 类保存其状态的方式,您应该扩展 Preference.BaseSavedState 类。您只需重写几种方法并定义 CREATOR 对象。</p> <p>对于大多数应用,如果 Preference 子类保存除整型数以外的其他数据类型,则可复制下列实现并直接更改处理 value 的行。</p> <pre> <code class="language-java">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) } } } }</code></pre> <p>如果将上述 Preference.BaseSavedState 实现添加到您的应用(通常,作为 Preference 子类的子类),则需要为 Preference 子类实现 onSaveInstanceState() 和 onRestoreInstanceState() 方法。</p> <pre> <code class="language-java">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) }</code></pre> <h3><strong>3.5 在大屏幕上使用分屏</strong></h3> <p style="text-align:center"><img src="https://simg.open-open.com/show/750380d5680641364d03c2658fafdc9a.png"></p> <p style="text-align:center">大屏幕上的分屏效果</p> <p>在极少数情况下,您可能需要设计设置,使第一个屏幕仅显示子屏幕的列表(例如在系统“设置”应用中,如图 4 和图 5 所示)。 在开发针对 Android 3.0 及更高版本系统的此类设计时,您应该使用 Android 3.0 中的新“标头”功能,而非使用嵌套的 PreferenceScreen 元素构建子屏幕。</p> <p>要使用标头构建设置,您需要:</p> <ul> <li>将每组设置分成单独的 PreferenceFragment 实例。即,每组设置均需要一个单独的 XML 文件。</li> <li>创建 XML 标头文件,其中列出每个设置组并声明哪个片段包含对应的设置列表。</li> <li>扩展 PreferenceActivity 类以托管设置。</li> <li>实现 onBuildHeaders() 回调以指定标头文件。</li> </ul> <p>使用此设计的一大好处是,在大屏幕上运行时,PreferenceActivity 会自动提供双窗格布局(如图 4 所示)。</p> <p>即使您的应用支持早于 3.0 的 Android 版本,您仍可将应用设计为使用 PreferenceFragment 在较新版本的设备上呈现双窗格布局,同时仍支持较旧版本设备上传统的多屏幕层次结构。</p> <p>1.标头用 XML 标头文件定义。</p> <p>2.每组设置均由 PreferenceFragment(通过标头文件中的 <header> 元素指定)定义。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/361a984afd732c4db05a5ee8f5a771b5.png"></p> <p><strong>3.5.1 创建标头文件</strong></p> <pre> <code class="language-java"><?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></code></pre> <p>每个标头均可使用 android:fragment 属性声明在用户选择该标头时应打开的 PreferenceFragment 实例。</p> <p><extras> 元素允许您使用 Bundle 将键值对传递给片段。该片段可以通过调用 getArguments() 检索参数。您向该片段传递参数的原因可能有很多,不过一个重要原因是,要对每个组重复使用 PreferenceFragment 的相同子类,而且要使用参数来指定该片段应加载哪些首选项 XML 文件。</p> <p>例如,当每个标头均使用 "settings" 键定义 <extra> 参数时,则可以对多个设置组重复使用以下片段:</p> <pre> <code class="language-java">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) } } }</code></pre> <p><strong>3.5.2 显示标头</strong></p> <p>要显示首选项标头,您必须实现 onBuildHeaders() 回调方法并调用 loadHeadersFromResource()。例如:</p> <pre> <code class="language-java">class SettingsActivity extends PreferenceActivity() { override onBuildHeaders(target: List<Header>) { loadHeadersFromResource(R.xml.preference_headers, target) } }</code></pre> <p>当用户从标头列表中选择一个项目时,系统会打开相关的 PreferenceFragment。</p> <p>注:使用首选项标头时,PreferenceActivity 的子类无需实现 onCreate() 方法,因为 Activity 唯一所需执行的任务就是加载标头。</p> <p><strong>3.5.3 使用首选项标头支持旧版本</strong></p> <p>如果您的应用支持早于 3.0 的 Android 版本,则在 Android 3.0 及更高版本系统上运行时,您仍可使用标头提供双窗格数据。为此,您只需另外创建 一个使用基本 <Preference> 元素的首选项 XML 文件即可,这些基本元素的行为方式与标头项目类似(供较旧版本的 Android 系统使用)。</p> <p>但是,每个 <Preference> 元素均会向 PreferenceActivity 发送一个 Intent,指定要加载哪个首选项 XML 文件,而不是打开新的 PreferenceScreen。</p> <p>例如,下面就是一个用于 Android 3.0 及更高版本系统的首选项标头 XML 文件 (res/xml/preference_headers.xml):</p> <pre> <code class="language-java"><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></code></pre> <p>下面是为早于 Android 3.0 版本的系统提供相同标头的首选项文件 (res/xml/preference_headers_legacy.xml):</p> <pre> <code class="language-java"><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></code></pre> <p>由于是从 Android 3.0 开始方添加对 <preference-headers> 的支持,因此只有在 Androd 3.0 或更高版本中运行时,系统才会在您的 PreferenceActivity 中调用 onBuildHeaders()。要加载“旧版”标头文件 (preference_headers_legacy.xml),您必须检查 Android 版本,如果版本低于 Android 3.0 (HONEYCOMB),请调用 addPreferencesFromResource() 来加载旧版标头文件。例如:</p> <pre> <code class="language-java">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) }</code></pre> <p>最后要做的就是处理传入 Activity 的 Intent,以确定要加载的首选项文件。因此,请检索 Intent 的操作,并将其与在首选项 XML 的 <intent> 标记中使用的已知操作字符串进行比较。</p> <pre> <code class="language-java">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) } }</code></pre> <p>值得注意的是,连续调用 addPreferencesFromResource() 会将所有首选项堆叠在一个列表中,因此请将条件与 else-if 语句链接在一起,确保它只调用一次。</p> <p> </p> <p>来自:http://www.jianshu.com/p/f5f8834ee9af</p> <p> </p>