課程 4: 偏好

這節(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>
  1. 菜單項包含在 <menu> 內(nèi),因此 <menu> 必須是根節(jié)點。
  2. 一個菜單項為一個 <item>,若要搭建次級菜單,則在 <item> 內(nèi)嵌套 <menu> 后添加更多 <item> 菜單項。
  3. android:id:菜單項的 ID,在 Java 中通過 ID 來識別每個菜單項。
  4. android:icon:菜單項的圖標(biāo),用戶通過點擊圖標(biāo)與菜單項交互。
  5. android:title:菜單項的標(biāo)題,用戶長按菜單項的圖標(biāo)時會彈出顯示。
  6. android:orderInCategory:菜單項的排列順序,數(shù)值越大,優(yōu)先級越低。例如排列順序為默認(rèn)的從左到右時,數(shù)值為 1 的菜單項排在最左邊。
  7. 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)生總體影響的菜單項。

  1. 首先通過 override onCreateOptionsMenu 指定在 XML 中定義的 Menu 資源。
  2. 然后通過 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>
  1. 注意文件的目錄為 res > xml,文件放在資源目錄的 xml 目錄下。
  2. <PreferenceScreen> 必須為根節(jié)點,表示頂級 Preference 對象,所以在嵌套 Preference 對象時也需要由 <PreferenceScreen> 包括。
  3. Preference Class 有很多用于不同情景的子類,例如 CheckBoxPreference、SwitchPreference、EditTextPreference、ListPreference 等。這里使用 EditTextPreference,它是一個允許用戶輸入值的偏好:用戶點擊會彈出一個含有 EditText 的對話框,用戶輸入值后會以字符串的形式保存到 SharedPreferences 中。
  4. android:inputType:指定用戶輸入的數(shù)據(jù)類型,屬于 TextText 的屬性。設(shè)置為 "numberDecimal" 表示將輸入的數(shù)據(jù)類型限制為數(shù)字,允許小數(shù)。
  5. android:selectAllOnFocus:設(shè)置為 "true" 表示在彈出對話框和輸入法時,自動全選 EditText 內(nèi)的所有字符,方便用戶直接修改值,無需先刪除原有值。
  6. android:title:偏好的標(biāo)題,出現(xiàn)在偏好列表中。
  7. android:key:偏好的鍵,正如前面提到的,偏好其實是與原始類型、字符串或字符串集相關(guān)聯(lián)的字符串鍵。這個鍵是 SharedPreferences 識別每項偏好的唯一標(biāo)識。
  8. 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);
     }
}
  1. 對于通過 XML 文件構(gòu)建的 PreferenceFragment 偏好列表,調(diào)用 addPreferencesFromResource 添加;還存在另外兩種構(gòu)建偏好列表的方法,通過調(diào)用 addPreferencesFromIntentsetPreferenceScreen 添加,這里不作討論。
  2. 與 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());
}
  1. 將查詢地震信息的 URL 的固定不變的頭部定義為常量,注意添加 staticfinal 等關(guān)鍵字。

  2. 通過 PreferenceManager.getDefaultSharedPreferences(this) 獲取 SharedPreferences 對象。

  3. 調(diào)用 getString 獲取偏好的鍵和值,傳入的參數(shù)為對應(yīng)的字符串資源 ID,返回值為偏好的值,若偏好不存在則返回傳入的值,因此傳入值可以為 null

  4. 通過 Uri.parse 創(chuàng)建一個 Uri 對象,傳入的參數(shù)為上面定義的 URL 頭部,數(shù)據(jù)類型為 String。

  5. 通過 buildUpon() 創(chuàng)建一個已有 URI 的 Uri.Builder 對象。

  6. 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);
}
  1. 因為 OnPreferenceChangeListener 是一個接口,所以需要在 EarthquakePreferenceFragment 類名后添加 implements 表示在這個類內(nèi)實現(xiàn)接口。
  2. onCreate 內(nèi)通過 findPreference 找到要偏好項并傳入輔助方法。
  3. bindPreferenceSummaryToValue 輔助方法內(nèi),設(shè)置傳入的偏好項的監(jiān)聽器,創(chuàng)建偏好項的 SharedPreferences 對象并通過 getString 獲取偏好項的值,最后傳給監(jiān)聽器需要 override 的 method 處理。
  4. onPreferenceChange 內(nèi)將輔助方法傳入的偏好項的值通過 setSummary 顯示在偏好項標(biāo)題的下方,輸入?yún)?shù)的數(shù)據(jù)類型為 CharSequence,由于 String 是 CharSequence 的擴(kuò)展類,所以這里 CharSequence 作為輸入?yún)?shù)時,可以傳入 String。
  5. onPreferenceChange 中,面對多個偏好項的情況,可以通過 if/else 語句判斷每個偏好項,再進(jìn)行處理。
if (preference instanceof EditTextPreference) {
    preference.setSummary(stringValue);
} else {
    // Handle another preference here.
}
return true;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內(nèi)容