1. 程式人生 > >android 7.0中Settings新功能全面解析

android 7.0中Settings新功能全面解析

Settings N預覽

imageimage

android N 在Settings中作了一些調整,如上面的截圖。

  • 增加了側滑選單,採用v4下的DrawerLayout來實現;
  • 在Settings主介面增加了Condition,能夠在設定列表中顯示狀態;
  • 在Settings主介面增加了Suggestion。

Dashboard category資料的載入

首先來看下Settings的Dashboard category,dashboard的中文意思指的是儀表板,在Settings中指的是Settings中顯示的選項,如WLAN,Bluetooth這樣的,參見上面的預覽圖片。

在android M中,dashboard的載入是放在SettingsActivity中,而且Settings/res/xml/dashboard_categories.xml這個檔案專門用來描述dashboard的整體結構,參見下圖。
image

在Settings N中,則將dashboard這部分的邏輯抽取了出來,放在/frameworks/base/packages/SettingsLib/目錄下。N中不再使用dashboard_categories.xml這個檔案來描述Settings各選項的架構,而且將Dashboard的初始化放在SettingsLib中來處理,首先看下面的圖片:

Settings AndroidManifest.xml
image

SettingsLib/src/com/android/settingslib/drawer/TileUtils.java

在TileUtils中定義的Actions,用於標記Activity屬於哪一個Dashboard category

/**
 * Same as #EXTRA_SETTINGS_ACTION but used for the platform Settings activities.
 */
private static final String SETTINGS_ACTION =
        "com.android.settings.action.SETTINGS";

private static final String OPERATOR_SETTINGS =
        "com.android.settings.OPERATOR_APPLICATION_SETTING";

private static final String OPERATOR_DEFAULT_CATEGORY =
        "com.android.settings.category.wireless";

private static final String MANUFACTURER_SETTINGS =
        "com.android.settings.MANUFACTURER_APPLICATION_SETTING";

private static final String MANUFACTURER_DEFAULT_CATEGORY =
        "com.android.settings.category.device";

Categories定義在Settings/res/values/donottranslate.xml中,分為四個大的Category,如下程式碼

Settings/res/values/donottranslate.xml

<string name="category_key_wireless">com.android.settings.category.wireless</string>
<string name="category_key_device">com.android.settings.category.device</string>
<string name="category_key_personal">com.android.settings.category.personal</string>
<string name="category_key_system">com.android.settings.category.system</string>

TileUtils.java中定義的Meta Data

Name of the meta-data item that should be set in the AndroidManifest.xml
to specify the icon、the title、the summary that should be displayed for the preference.

 public static final String META_DATA_PREFERENCE_ICON = "com.android.settings.icon";

 public static final String META_DATA_PREFERENCE_TITLE = "com.android.settings.title";

  public static final String META_DATA_PREFERENCE_SUMMARY = "com.android.settings.summary";

image

Meta data會在AndroidManifest.xml進行配置,在TileUtils.java中載入Dashboard Category的時候,會通過PackageManager獲得各個Activity的資訊後,再動態的更新到頁面上。(另外,我發現對於這些Dashboard的icon,title和Summary有的在AndroidManifest.xml中有配置meta-data有的卻沒有,我感覺這裡應該用的是Activity節點下的icon,title(lablel),這部分如果要徹底搞清楚需要看PackageManager解析AndroidManifest.xml的邏輯,這裡不作深究)。

接下來看在TileUtils.java程式碼中是對於Dashboard是如何處理的
image

上面的getCategories方法主要分為兩個部分來看,首先通過PackageManager獲得各個Category的資訊儲存到ArrayList中,接著對ArrayList中的資料按照優先順序進行排序,這樣主介面拿到這些資料就可以顯示了。

Dashboard category的整體佈局

image
看上面這幅圖,從上而下分別是Condition,Suggestion和各個顯示的Item項。

接下來我們來看這部分在程式碼中是如何構建的?

DashboardAdapter.java中的recountItems方法
private void recountItems() {
        reset();
        boolean hasConditions = false;
        for (int i = 0; mConditions != null && i < mConditions.size(); i++) {
            boolean shouldShow = mConditions.get(i).shouldShow();
            hasConditions |= shouldShow;
            //(1)condition_card.xml
            countItem(mConditions.get(i), R.layout.condition_card, shouldShow, NS_CONDITION);
        }
        boolean hasSuggestions = mSuggestions != null && mSuggestions.size() != 0;
        //(2)dashboard_spacer.xml
        countItem(null, R.layout.dashboard_spacer, hasConditions && hasSuggestions, NS_SPACER);
        //(3)suggestion_header.xml
        countItem(null, R.layout.suggestion_header, hasSuggestions, NS_SPACER);
        resetCount();
        if (mSuggestions != null) {
            int maxSuggestions = getDisplayableSuggestionCount();
            for (int i = 0; i < mSuggestions.size(); i++) {
                 //(3)suggestion_tile.xml
                countItem(mSuggestions.get(i), R.layout.suggestion_tile, i < maxSuggestions,
                        NS_SUGGESTION);
            }
        }
        resetCount();
        for (int i = 0; mCategories != null && i < mCategories.size(); i++) {
            DashboardCategory category = mCategories.get(i);
            //(4)dashboard_category.xml
            countItem(category, R.layout.dashboard_category, mIsShowingAll, NS_ITEMS);
            for (int j = 0; j < category.tiles.size(); j++) {
                Tile tile = category.tiles.get(j);
                //(5)dashboard_tile.xml
                countItem(tile, R.layout.dashboard_tile, mIsShowingAll
                        || ArrayUtils.contains(DashboardSummary.INITIAL_ITEMS,
                        tile.intent.getComponent().getClassName()), NS_ITEMS);
            }
        }
        notifyDataSetChanged();
    }

recountItems方法會在構建佈局的時候多次呼叫,這個方法裡面會在這裡加入多個layout佈局檔案。

如上面的程式碼的註釋標明部分

//(1)condition_card.xml
//(2)dashboard_spacer.xml
//(3)suggestion_header.xml
//(4)dashboard_category.xml
//(5)dashboard_tile.xml
 

這裡使用countItem方法將各個佈局加入到List中去,分別是下面三個集合

private final List<Object> mItems = new ArrayList<>();
private final List<Integer> mTypes = new ArrayList<>();
private final List<Integer> mIds = new ArrayList<>();

在將這些佈局檔案加入到List中去後,然後在onBindViewHolder去獲取List中的內容,從而展示在頁面上,這部分的邏輯就不再介紹了,大家有興趣的可以去看看。

Settings Drawer的實現

image

N中的Settings使用DrawerLayout為Settings介面加入了側滑選單的功能。我們對比下M平臺和N平臺的Settings Activity的結構就大概明白了。
image

android N在在SettingsActivity上面構建了一個SettingsDrawerActivity,側滑的功能則是在SettingsDrawerActivity中實現的,SettingsActivity位於SettingsLib下面。

接下來我們看看SettingsDrawerActivity這個類:
image
在SettingsDrawerActivity的onCreate方法中會載入settings_with_drawer這個檔案。這個檔案則是對左側Drawer的佈局檔案的描述。如下code:

<android.support.v4.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="?android:attr/colorPrimaryDark">
    <!-- The main content view -->
    <LinearLayout
        android:id="@+id/content_parent"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:fitsSystemWindows="true" >
        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            style="?android:attr/actionBarStyle">
            <Toolbar
                xmlns:android="http://schemas.android.com/apk/res/android"
                android:id="@+id/action_bar"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:navigationContentDescription="@*android:string/action_bar_up_description"
                android:theme="?android:attr/actionBarTheme"
                style="?android:attr/toolbarStyle"
                android:background="?android:attr/colorPrimary" />
        </FrameLayout>
        <FrameLayout
            android:id="@+id/content_header_container"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            style="?android:attr/actionBarStyle" />
        <FrameLayout
            android:id="@+id/content_frame"
            android:layout_width="match_parent"
            android:layout_height="fill_parent"
            android:background="?android:attr/windowBackground" />
    </LinearLayout>
    <!-- The navigation drawer -->
    <ListView android:id="@+id/left_drawer"
        android:layout_width="300dp"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:choiceMode="singleChoice"
        android:divider="@android:color/transparent"
        android:dividerHeight="0dp"
        android:background="?android:attr/colorBackground" />
</android.support.v4.widget.DrawerLayout>

接著來看左側Drawer的ListView的資料是如何載入的,這部分的邏輯由SettingsDrawerAdapter來實現。
image

如上截圖,在SettingsDrawerAdapter的updateCategories方法中,新增最上面的home的圖片和檔案後,然後遍歷裝有DashboardCategory的集合,取出裡面的DashboardCategory和其中的Tile存放到對應的集合中去,用於顯示到頁面上去。

Settings中的Condition

7.0中的Settings加入的Condition可以顯示設定有些item的狀態,並且提供快捷開關,在單擊後,可以跳轉到相應的Settings 頁面。

在上文介紹DashboardCategory的整體佈局的時候,介紹了Condition部分載入的檔案是condition_card.xml檔案
image

如上圖和xml檔案相對應,分別表明了各個控制元件的id。
condition_card.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:clipChildren="false"
   android:clipToPadding="false">
   <LinearLayout
       android:id="@+id/content"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:orientation="vertical"
       android:background="?android:attr/colorAccent"
       android:elevation="2dp"
       android:clickable="true"
       android:focusable="true">

       <LinearLayout
           android:id="@+id/collapsed_group"
           android:layout_width="match_parent"
           android:layout_height="56dp"
           android:background="?android:attr/selectableItemBackground"
           android:orientation="horizontal"
           android:gravity="center">

           <ImageView
               android:id="@android:id/icon"
               android:layout_width="24dp"
               android:layout_height="wrap_content"
               android:layout_marginStart="16dp"
               android:layout_marginEnd="32dp"
               android:tint="?android:attr/textColorPrimaryInverse" />

           <TextView
               android:id="@android:id/title"
               android:layout_width="0dp"
               android:layout_height="wrap_content"
               android:layout_weight="1"
               android:textAppearance="?android:attr/textAppearanceMedium"
               android:textColor="?android:attr/textColorPrimaryInverse" />

           <ImageView
               android:id="@+id/expand_indicator"
               android:layout_width="wrap_content"
               android:layout_height="match_parent"
               android:padding="16dp"
               android:tint="?android:attr/textColorPrimaryInverse"/>

       </LinearLayout>

       <LinearLayout
           android:id="@+id/detail_group"
           android:layout_width="match_parent"
           android:layout_height="0dp"
           android:paddingStart="72dp"
           android:visibility="gone"
           android:orientation="vertical">

           <!-- TODO: Don't set alpha here, and do proper themeing that
                handles night mode -->
           <TextView
               android:id="@android:id/summary"
               android:layout_width="match_parent"
               android:layout_height="wrap_content"
               android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
               android:paddingBottom="16dp"
               android:textAppearance="?android:attr/textAppearanceListItemSecondary"
               android:alpha=".7"
               android:textColor="?android:attr/textColorPrimaryInverse" />

           <!-- TODO: Better background -->
           <View
               android:id="@+id/divider"
               android:layout_width="match_parent"
               android:layout_height=".25dp"
               android:background="@android:color/white" />

           <com.android.internal.widget.ButtonBarLayout
               android:id="@+id/buttonBar"
               android:layout_width="match_parent"
               android:layout_height="wrap_content"
               android:paddingTop="8dp"
               android:paddingBottom="8dp"
               style="?attr/buttonBarStyle"
                android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">

                <Button
                    android:id="@+id/first_action"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:paddingStart="0dp"
                    android:alpha=".8"
                    android:textAlignment="viewStart"
                    android:textColor="?android:attr/textColorPrimaryInverse"
                    style="?android:attr/buttonBarButtonStyle" />

                <Button
                    android:id="@+id/second_action"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:alpha=".8"
                    android:textAlignment="viewStart"
                    android:textColor="?android:attr/textColorPrimaryInverse"
                    style="?android:attr/buttonBarButtonStyle" />

            </com.android.internal.widget.ButtonBarLayout>

        </LinearLayout>

    </LinearLayout>
</FrameLayout>

接著來看看Condition的繼承層次:
image

我們拿AirplaneModeCondition來舉例,在Settings的AndroidManifest.xml中註冊瞭如下的Receiver:

<receiver
    android:name=".dashboard.conditional.AirplaneModeCondition$Receiver"
    android:enabled="false">
    <intent-filter>
         <action android:name="android.intent.action.AIRPLANE_MODE" />
    </intent-filter>
</receiver>

預設情況下這些Condition是關閉的,即enabled的。在這個Receiver中,會去接收這個廣播,當Condition的狀態改變的時候會去更新狀態。

//AirplaneModeCondition.java
@Override
public void refreshState() {
    setActive(WirelessUtils.isAirplaneModeOn(mManager.getContext()));
}

public static class Receiver extends BroadcastReceiver {
   @Override
   public void onReceive(Context context, Intent intent) {
       if (Intent.ACTION_AIRPLANE_MODE_CHANGED.equals(intent.getAction())) {
           ConditionManager.get(context).getCondition(AirplaneModeCondition.class)
                   .refreshState();
       }
   }
}

Settings中的Suggestion

suggestion能夠在設定的主頁面顯示一些建議項,相當於為一些常用的功能介面新增入口,使用者通過點選這些建議項可以跳到相應的頁面進行操作,並且使用者可以手動移除這些建議項。

如下面的截圖,Suggestion頁面分為兩個
suggestion_header.xml和suggestion_tile.xml兩個佈局組成。
image

關於Suggestion的配置資訊:
Suggestion預設的數量為2個,如上圖所示,這個常量的設定是在DashboardAdapter.java裡面。

 private static final int DEFAULT_SUGGESTION_COUNT = 2;

另外這些Suggestion是以一種順序來顯示的,這個部分的配置是在suggestion_ordering.xml中配置的。

<optional-steps>
    <step category="com.android.settings.suggested.category.LOCK_SCREEN" />
    <step category="com.android.settings.suggested.category.EMAIL" />
    <step category="com.android.settings.suggested.category.PARTNER_ACCOUNT"
        multiple="true" />
    <step category="com.android.settings.suggested.category.HOTWORD" />
    <step category="com.android.settings.suggested.category.DEFAULT"
        multiple="true" />
    <step category="com.android.settings.suggested.category.SETTINGS_ONLY"
        multiple="true" />
</optional-steps>

這裡會通過SuggestionParser.java中new出來的SuggestionOrderInflater來解析這個檔案,Suggestion相關的很多解析操作都是由SuggestionParser.java來處理的。

在SuggestionParser有以下的配置:
image

這個類中定義的常量會在Settings的AndroidManifest.xml使用。
image

如上圖中定義的com.android.settings.require_feature的meta-data節點表示該Suggestion的顯示需要特定的feature支援,對於FingerprintEnrollSuggestionActivity這個Suggestion的顯示則需要指紋的支援。

另外對於META_DATA_DISMISS_CONTROL則控制著當前Suggestion的顯示時機。正如上面截圖的註釋描述。

Allows suggestions to appear after a certain number of days, and to re-appear if dismissed.
For instance:
 0,10
 Will appear immediately, but if the user removes it, it will come back after 10 days.

Another example:
10,30
 Will only show up after 10 days, and then again after 30.
這個屬性允許Suggestion在特定的天數後顯示,並且在被拒絕後重新顯示。

0,10表示該Suggestion會立即顯示,但是如果使用者刪除後,會在10天后再次顯示。
10,30則表示在10天后顯示,然後在30天之後再次顯示。

以上就是對於android7.0Settings的一些新功能的分析,其實這部分還有很多東西沒有詳細地去分析,這部分只是做了簡單的介紹。

另外,再去看原始碼的時候,發現Google的設計真的是厲害,而且自己很多時候都是從原始碼的功能去理解,其實從架構,效能方面考慮,原始碼都是非常優秀的,有很多值得學習的地方。

歡迎關注我的公眾號 ,不定期會有優質技術文章推送 。

微信掃一掃下方二維碼即可關注
在這裡插入圖片描述