這節(jié)課是 Android 開發(fā)(入門)課程 的第三部分《訪問網(wǎng)絡(luò)》的最后一節(jié)課。這節(jié)課為 Quake Report App 添加一個偏好 (Preference) 頁面,入口放在應(yīng)用欄,使應(yīng)用能夠根據(jù)用戶的偏好修改查詢地震信息的最小震級,以及按震級大小或時間順序排列顯示地震信息列表。
關(guān)鍵詞:SharedPreferences、PreferenceFragment、Menu、Uri.Builder、Preference.OnPreferenceChangeListener
應(yīng)用的偏好使每個用戶都能根據(jù)自身需要得到略微不同的應(yīng)用體驗。用戶能夠調(diào)整應(yīng)用中的偏好,系統(tǒng)會記住用戶選定的偏好。無論是重啟應(yīng)用,還是重啟設(shè)備,當(dāng)用戶再次打開應(yīng)用時,應(yīng)用依然能夠按照用戶設(shè)置的偏好運行。這涉及到數(shù)據(jù)持久性 (Data Persistence),它是下一部分課程的主題。目前 Quake Report App 需要存儲的數(shù)據(jù)較少,可以通過 Android 組件來完成此功能。
事實上,偏好是與原始類型、字符串或字符串集相關(guān)聯(lián)的字符串鍵 (String Key)。即使關(guān)閉應(yīng)用或設(shè)備,Android 也會保留該數(shù)據(jù)。SharedPreferences Class 就是一個存取偏好的接口,配合 PreferenceFragment Class 搭建的偏好列表使用戶能夠編輯各種偏好。
下面以 Quake Report App 為例,分步驟描述如何打造一個偏好頁面。
Menu
在 Quake Report App 中將偏好頁面的入口放在應(yīng)用欄中,需要用到 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>
內(nèi),因此<menu>
必須是根節(jié)點。 - 一個菜單項為一個
<item>
,若要搭建次級菜單,則在<item>
內(nèi)嵌套<menu>
后添加更多<item>
菜單項。 -
android:id
:菜單項的 ID,在 Java 中通過 ID 來識別每個菜單項。 -
android:icon
:菜單項的圖標(biāo),用戶通過點擊圖標(biāo)與菜單項交互。 -
android:title
:菜單項的標(biāo)題,用戶長按菜單項的圖標(biāo)時會彈出顯示。 -
android:orderInCategory
:菜單項的排列順序,數(shù)值越大,優(yōu)先級越低。例如排列順序為默認(rèn)的從左到右時,數(shù)值為 1 的菜單項排在最左邊。 -
app:showAsAction
:定義菜單項的顯示方式,這里設(shè)置為 "ifRoom" 表示菜單項僅在有空間時顯示,若無空間則按照orderInCategory
僅顯示優(yōu)先級最高(數(shù)值最小)的菜單項,其余項顯示在溢出菜單 (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,主要放置一些對應(yīng)用產(chǎn)生總體影響的菜單項。
- 首先通過 override
onCreateOptionsMenu
指定在 XML 中定義的 Menu 資源。 - 然后通過 override
onOptionsItemSelected
處理 Options Menu 的點擊事件,輸入?yún)?shù)為用戶點擊的 MenuItem 對象。通過getItemId()
獲取菜單項的 ID,然后通過 if/else 語句判斷是否為期望的菜單項,若是則進(jìn)一步處理,在 Quake Report App 中即 Intent 打開偏好頁面。如果有多個菜單項,可以通過 switch/case 語句匹配指令,使代碼更高效。
偏好頁面
構(gòu)建一個如上描述的通過應(yīng)用欄中菜單項進(jìn)入的偏好頁面。因為需要使用 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,相當(dāng)于指定了其“向上”按鈕的行為。
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 構(gòu)建 Preference 對象的列表,樣式自動延續(xù)系統(tǒng)的風(fēng)格,這些 Preference 對象會自動保存在 SharedPreferences 中。因此,用戶修改 PreferenceFragment 偏好列表中的 Preference 對象后,參數(shù)會保存在 SharedPreferences 中,應(yīng)用再通過操作 SharedPreferences 實現(xiàn)內(nèi)容或結(jié)構(gòu)的調(diào)整。
搭建一個 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>
必須為根節(jié)點,表示頂級 Preference 對象,所以在嵌套 Preference 對象時也需要由<PreferenceScreen>
包括。 - Preference Class 有很多用于不同情景的子類,例如 CheckBoxPreference、SwitchPreference、EditTextPreference、ListPreference 等。這里使用 EditTextPreference,它是一個允許用戶輸入值的偏好:用戶點擊會彈出一個含有 EditText 的對話框,用戶輸入值后會以字符串的形式保存到 SharedPreferences 中。
-
android:inputType
:指定用戶輸入的數(shù)據(jù)類型,屬于 TextText 的屬性。設(shè)置為 "numberDecimal" 表示將輸入的數(shù)據(jù)類型限制為數(shù)字,允許小數(shù)。 -
android:selectAllOnFocus
:設(shè)置為 "true" 表示在彈出對話框和輸入法時,自動全選 EditText 內(nèi)的所有字符,方便用戶直接修改值,無需先刪除原有值。 -
android:title
:偏好的標(biāo)題,出現(xiàn)在偏好列表中。 -
android:key
:偏好的鍵,正如前面提到的,偏好其實是與原始類型、字符串或字符串集相關(guān)聯(lián)的字符串鍵。這個鍵是 SharedPreferences 識別每項偏好的唯一標(biāo)識。 -
android:defaultValue
:偏好的默認(rèn)值,用戶修改的就是這個值。在這里雖然值被限制為數(shù)字,但是因為傳給 SharedPreferences 的數(shù)據(jù)是字符串,所以這里也保持字符串的數(shù)據(jù)類型。
在 strings.xml 中定義上述偏好的鍵和值時,因為要保證唯一性,所以在 <string>
標(biāo)簽內(nèi)添加 translatable="false"
表示該字符串不可翻譯,應(yīng)保持原樣。
<string name="settings_min_magnitude_key" translatable="false">min_magnitude</string>
<string name="settings_min_magnitude_default" translatable="false">6</string>
在 XML 文件中構(gòu)建好 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 文件構(gòu)建的 PreferenceFragment 偏好列表,調(diào)用
addPreferencesFromResource
添加;還存在另外兩種構(gòu)建偏好列表的方法,通過調(diào)用addPreferencesFromIntent
和setPreferenceScreen
添加,這里不作討論。 - 與 drawable 目錄的圖像資源以及 Layout 文件類似,XML 文件的 ID 為文件名。
Uri.Builder
至此,Quake Report App 已經(jīng)添加一個偏好頁面,入口放在應(yīng)用欄,用戶修改偏好后會將值傳遞給 SharedPreferences。所以接下來就從 SharedPreferences 獲取 EditTextPreference 的字符串,定制查詢地震信息的 URL。這里引入 Uri.Builder Class 能夠很方便地構(gòu)造和修改 URI,其中 URI (Uniform Resource Identifier) 指統(tǒng)一資源標(biāo)識符,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
等關(guān)鍵字。通過
PreferenceManager.getDefaultSharedPreferences(this)
獲取 SharedPreferences 對象。調(diào)用
getString
獲取偏好的鍵和值,傳入的參數(shù)為對應(yīng)的字符串資源 ID,返回值為偏好的值,若偏好不存在則返回傳入的值,因此傳入值可以為null
。通過
Uri.parse
創(chuàng)建一個 Uri 對象,傳入的參數(shù)為上面定義的 URL 頭部,數(shù)據(jù)類型為 String。通過
buildUpon()
創(chuàng)建一個已有 URI 的 Uri.Builder 對象。-
Uri.Builder 對象可構(gòu)造多種 URI,包括絕對層級 URI (Absolute Hierarchical URI)、相對 URI (Relative URI)、不透明 URI (Opaque URI)。這里用到的絕對層級 URI 遵循如下格式:
<scheme>://<authority><absolute path>?<query>#<fragment>
其中 Uri 對象已經(jīng)從 URL 頭部獲取了前三個部分,所以剩下的查詢參數(shù) (query) 通過 appendQueryParameter
添加,輸入?yún)?shù)按照鍵值對的形式傳入。
Preference.OnPreferenceChangeListener
目前雖然 Quake Report App 的偏好已經(jīng)能正常工作,但是在偏好頁面僅顯示每個偏好項的標(biāo)題。如果能同時顯示偏好項的標(biāo)題和值,無需用戶點擊查看,這會是更好的用戶體驗。這里通過 OnPreferenceChangeListener 接口來實現(xiàn)這一功能。
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
表示在這個類內(nèi)實現(xiàn)接口。 - 在
onCreate
內(nèi)通過findPreference
找到要偏好項并傳入輔助方法。 - 在
bindPreferenceSummaryToValue
輔助方法內(nèi),設(shè)置傳入的偏好項的監(jiān)聽器,創(chuàng)建偏好項的 SharedPreferences 對象并通過getString
獲取偏好項的值,最后傳給監(jiān)聽器需要 override 的 method 處理。 - 在
onPreferenceChange
內(nèi)將輔助方法傳入的偏好項的值通過setSummary
顯示在偏好項標(biāo)題的下方,輸入?yún)?shù)的數(shù)據(jù)類型為 CharSequence,由于 String 是 CharSequence 的擴(kuò)展類,所以這里 CharSequence 作為輸入?yún)?shù)時,可以傳入 String。 - 在
onPreferenceChange
中,面對多個偏好項的情況,可以通過 if/else 語句判斷每個偏好項,再進(jìn)行處理。
if (preference instanceof EditTextPreference) {
preference.setSummary(stringValue);
} else {
// Handle another preference here.
}
return true;