这节课是 Android 开发(入门)课程 的第三部分《访问网络》的最后一节课。这节课为 Quake Report App 添加一个偏好 (Preference) 页面,入口放在应用栏,使应用能够根据用户的偏好修改查询地震信息的最小震级,以及按震级大小或时间顺序排列显示地震信息列表。
关键词:SharedPreferences、PreferenceFragment、Menu、Uri.Builder、Preference.OnPreferenceChangeListener
应用的偏好使每个用户都能根据自身需要得到略微不同的应用体验。用户能够调整应用中的偏好,系统会记住用户选定的偏好。无论是重启应用,还是重启设备,当用户再次打开应用时,应用依然能够按照用户设置的偏好运行。这涉及到数据持久性 (Data Persistence),它是下一部分课程的主题。目前 Quake Report App 需要存储的数据较少,可以通过 Android 组件来完成此功能。
事实上,偏好是与原始类型、字符串或字符串集相关联的字符串键 (String Key)。即使关闭应用或设备,Android 也会保留该数据。SharedPreferences Class 就是一个存取偏好的接口,配合 PreferenceFragment Class 搭建的偏好列表使用户能够编辑各种偏好。
下面以 Quake Report App 为例,分步骤描述如何打造一个偏好页面。
Menu
在 Quake Report App 中将偏好页面的入口放在应用栏中,需要用到 Menus 组件。首先在 XML 中定义 Menu 资源:
In res/menu/main.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="com.example.android.quakereport.EarthquakeActivity">
<item
android:id="@+id/action_settings"
android:icon="@drawable/ic_filter"
android:title="@string/settings_menu_item"
android:orderInCategory="1"
app:showAsAction="ifRoom" />
</menu>
- 菜单项包含在
<menu>
内,因此<menu>
必须是根节点。 - 一个菜单项为一个
<item>
,若要搭建次级菜单,则在<item>
内嵌套<menu>
后添加更多<item>
菜单项。 -
android:id
:菜单项的 ID,在 Java 中通过 ID 来识别每个菜单项。 -
android:icon
:菜单项的图标,用户通过点击图标与菜单项交互。 -
android:title
:菜单项的标题,用户长按菜单项的图标时会弹出显示。 -
android:orderInCategory
:菜单项的排列顺序,数值越大,优先级越低。例如排列顺序为默认的从左到右时,数值为 1 的菜单项排在最左边。 -
app:showAsAction
:定义菜单项的显示方式,这里设置为 "ifRoom" 表示菜单项仅在有空间时显示,若无空间则按照orderInCategory
仅显示优先级最高(数值最小)的菜单项,其余项显示在溢出菜单 (Overflow Menu) 中。
更多 Menu 资源可以查看 Android Developers 文档。
接下来在 Menu 所在的 Activity 或 Fragment 中处理事件:
In EarthquakeActivity.java
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.action_settings) {
Intent settingsIntent = new Intent(this, SettingsActivity.class);
startActivity(settingsIntent);
return true;
}
return super.onOptionsItemSelected(item);
}
在 Android 中有三种 Menu: Options menu, Context menu, Popup menu,这里用的是 Options menu,主要放置一些对应用产生总体影响的菜单项。
- 首先通过 override
onCreateOptionsMenu
指定在 XML 中定义的 Menu 资源。 - 然后通过 override
onOptionsItemSelected
处理 Options Menu 的点击事件,输入参数为用户点击的 MenuItem 对象。通过getItemId()
获取菜单项的 ID,然后通过 if/else 语句判断是否为期望的菜单项,若是则进一步处理,在 Quake Report App 中即 Intent 打开偏好页面。如果有多个菜单项,可以通过 switch/case 语句匹配指令,使代码更高效。
偏好页面
构建一个如上描述的通过应用栏中菜单项进入的偏好页面。因为需要使用 PreferenceFragment 搭建偏好列表的 UI,所以偏好页面的 Layout 仅放置一个 Fragment:
In settings_activity.xml
<fragment
android:name="com.example.android.quakereport.SettingsActivity$EarthquakePreferenceFragment"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.android.quakereport.SettingsActivity">
</fragment>
并在 Java 中添加一个自定义 PreferenceFragment 类:
In SettingsActivity.java
public class SettingsActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.settings_activity);
}
public static class EarthquakePreferenceFragment extends PreferenceFragment {
}
}
另外,在 AndroidManifest 中添加 SettingsActivity 的 <meta-data>
定义偏好页面的 Parent Activity 为 EarthquakeActivity,相当于指定了其“向上”按钮的行为。
In AndroidManifest.xml
<activity
android:name=".SettingsActivity"
android:label="@string/settings_title">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.example.android.quakereport.EarthquakeActivity"/>
</activity>
PreferenceFragment
框架搭建好后,接下来使用 PreferenceFragment 构建 Preference 对象的列表,样式自动延续系统的风格,这些 Preference 对象会自动保存在 SharedPreferences 中。因此,用户修改 PreferenceFragment 偏好列表中的 Preference 对象后,参数会保存在 SharedPreferences 中,应用再通过操作 SharedPreferences 实现内容或结构的调整。
搭建一个 PreferenceFragment 偏好列表,可以通过 XML 文件完成:
In res/xml/settings_main.xml
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
android:title="@string/settings_title">
<EditTextPreference
android:inputType="numberDecimal"
android:selectAllOnFocus="true"
android:title="@string/settings_min_magnitude_label"
android:key="@string/settings_min_magnitude_key"
android:defaultValue="@string/settings_min_magnitude_default" />
</PreferenceScreen>
- 注意文件的目录为 res > xml,文件放在资源目录的 xml 目录下。
-
<PreferenceScreen>
必须为根节点,表示顶级 Preference 对象,所以在嵌套 Preference 对象时也需要由<PreferenceScreen>
包括。 - Preference Class 有很多用于不同情景的子类,例如 CheckBoxPreference、SwitchPreference、EditTextPreference、ListPreference 等。这里使用 EditTextPreference,它是一个允许用户输入值的偏好:用户点击会弹出一个含有 EditText 的对话框,用户输入值后会以字符串的形式保存到 SharedPreferences 中。
-
android:inputType
:指定用户输入的数据类型,属于 TextText 的属性。设置为 "numberDecimal" 表示将输入的数据类型限制为数字,允许小数。 -
android:selectAllOnFocus
:设置为 "true" 表示在弹出对话框和输入法时,自动全选 EditText 内的所有字符,方便用户直接修改值,无需先删除原有值。 -
android:title
:偏好的标题,出现在偏好列表中。 -
android:key
:偏好的键,正如前面提到的,偏好其实是与原始类型、字符串或字符串集相关联的字符串键。这个键是 SharedPreferences 识别每项偏好的唯一标识。 -
android:defaultValue
:偏好的默认值,用户修改的就是这个值。在这里虽然值被限制为数字,但是因为传给 SharedPreferences 的数据是字符串,所以这里也保持字符串的数据类型。
在 strings.xml 中定义上述偏好的键和值时,因为要保证唯一性,所以在 <string>
标签内添加 translatable="false"
表示该字符串不可翻译,应保持原样。
<string name="settings_min_magnitude_key" translatable="false">min_magnitude</string>
<string name="settings_min_magnitude_default" translatable="false">6</string>
在 XML 文件中构建好 PreferenceFragment 偏好列表后,在 Java 中 override 其自定义类的 onCreate
添加每项偏好。
In SettingsActivity.java
public static class EarthquakePreferenceFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.settings_main);
}
}
- 对于通过 XML 文件构建的 PreferenceFragment 偏好列表,调用
addPreferencesFromResource
添加;还存在另外两种构建偏好列表的方法,通过调用addPreferencesFromIntent
和setPreferenceScreen
添加,这里不作讨论。 - 与 drawable 目录的图像资源以及 Layout 文件类似,XML 文件的 ID 为文件名。
Uri.Builder
至此,Quake Report App 已经添加一个偏好页面,入口放在应用栏,用户修改偏好后会将值传递给 SharedPreferences。所以接下来就从 SharedPreferences 获取 EditTextPreference 的字符串,定制查询地震信息的 URL。这里引入 Uri.Builder Class 能够很方便地构造和修改 URI,其中 URI (Uniform Resource Identifier) 指统一资源标识符,URI 包含两个子类 URL 和 URN,两者的差别可简单理解为:URN 定义资源的属性,URL 提供查找该资源的方法。例如 URN 表示一个人的姓名,URL 表示那个人的住址。

private static final String USGS_REQUEST_URL = "http://earthquake.usgs.gov/fdsnws/event/1/query";
@Override
public Loader<List<Earthquake>> onCreateLoader(int i, Bundle bundle) {
SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
String minMagnitude = sharedPrefs.getString(
getString(R.string.settings_min_magnitude_key),
getString(R.string.settings_min_magnitude_default));
Uri baseUri = Uri.parse(USGS_REQUEST_URL);
Uri.Builder uriBuilder = baseUri.buildUpon();
uriBuilder.appendQueryParameter("format", "geojson");
uriBuilder.appendQueryParameter("limit", "10");
uriBuilder.appendQueryParameter("minmag", minMagnitude);
uriBuilder.appendQueryParameter("orderby", "time");
return new EarthquakeLoader(this, uriBuilder.toString());
}
-
将查询地震信息的 URL 的固定不变的头部定义为常量,注意添加
static
和final
等关键字。 -
通过
PreferenceManager.getDefaultSharedPreferences(this)
获取 SharedPreferences 对象。 -
调用
getString
获取偏好的键和值,传入的参数为对应的字符串资源 ID,返回值为偏好的值,若偏好不存在则返回传入的值,因此传入值可以为null
。 -
通过
Uri.parse
创建一个 Uri 对象,传入的参数为上面定义的 URL 头部,数据类型为 String。 -
通过
buildUpon()
创建一个已有 URI 的 Uri.Builder 对象。 -
Uri.Builder 对象可构造多种 URI,包括绝对层级 URI (Absolute Hierarchical URI)、相对 URI (Relative URI)、不透明 URI (Opaque URI)。这里用到的绝对层级 URI 遵循如下格式:
<scheme>://<authority><absolute path>?<query>#<fragment>
其中 Uri 对象已经从 URL 头部获取了前三个部分,所以剩下的查询参数 (query) 通过 appendQueryParameter
添加,输入参数按照键值对的形式传入。
Preference.OnPreferenceChangeListener
目前虽然 Quake Report App 的偏好已经能正常工作,但是在偏好页面仅显示每个偏好项的标题。如果能同时显示偏好项的标题和值,无需用户点击查看,这会是更好的用户体验。这里通过 OnPreferenceChangeListener 接口来实现这一功能。
public static class EarthquakePreferenceFragment extends PreferenceFragment
implements Preference.OnPreferenceChangeListener {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.settings_main);
Preference minMagnitude = findPreference(getString(R.string.settings_min_magnitude_key));
bindPreferenceSummaryToValue(minMagnitude);
}
@Override
public boolean onPreferenceChange(Preference preference, Object value) {
String stringValue = value.toString();
preference.setSummary(stringValue);
return true;
}
private void bindPreferenceSummaryToValue(Preference preference) {
preference.setOnPreferenceChangeListener(this);
SharedPreferences preferences =
PreferenceManager.getDefaultSharedPreferences(preference.getContext());
String preferenceString = preferences.getString(preference.getKey(), "");
onPreferenceChange(preference, preferenceString);
}
- 因为 OnPreferenceChangeListener 是一个接口,所以需要在 EarthquakePreferenceFragment 类名后添加
implements
表示在这个类内实现接口。 - 在
onCreate
内通过findPreference
找到要偏好项并传入辅助方法。 - 在
bindPreferenceSummaryToValue
辅助方法内,设置传入的偏好项的监听器,创建偏好项的 SharedPreferences 对象并通过getString
获取偏好项的值,最后传给监听器需要 override 的 method 处理。 - 在
onPreferenceChange
内将辅助方法传入的偏好项的值通过setSummary
显示在偏好项标题的下方,输入参数的数据类型为 CharSequence,由于 String 是 CharSequence 的扩展类,所以这里 CharSequence 作为输入参数时,可以传入 String。 - 在
onPreferenceChange
中,面对多个偏好项的情况,可以通过 if/else 语句判断每个偏好项,再进行处理。
if (preference instanceof EditTextPreference) {
preference.setSummary(stringValue);
} else {
// Handle another preference here.
}
return true;
网友评论