android 7.0中Settings新功能全面解析
Settings N預覽
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的整體結構,參見下圖。
在Settings N中,則將dashboard這部分的邏輯抽取了出來,放在/frameworks/base/packages/SettingsLib/目錄下。N中不再使用dashboard_categories.xml這個檔案來描述Settings各選項的架構,而且將Dashboard的初始化放在SettingsLib中來處理,首先看下面的圖片:
Settings AndroidManifest.xml
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";
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是如何處理的
上面的getCategories方法主要分為兩個部分來看,首先通過PackageManager獲得各個Category的資訊儲存到ArrayList中,接著對ArrayList中的資料按照優先順序進行排序,這樣主介面拿到這些資料就可以顯示了。
Dashboard category的整體佈局
看上面這幅圖,從上而下分別是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的實現
N中的Settings使用DrawerLayout為Settings介面加入了側滑選單的功能。我們對比下M平臺和N平臺的Settings Activity的結構就大概明白了。
android N在在SettingsActivity上面構建了一個SettingsDrawerActivity,側滑的功能則是在SettingsDrawerActivity中實現的,SettingsActivity位於SettingsLib下面。
接下來我們看看SettingsDrawerActivity這個類:
在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來實現。
如上截圖,在SettingsDrawerAdapter的updateCategories方法中,新增最上面的home的圖片和檔案後,然後遍歷裝有DashboardCategory的集合,取出裡面的DashboardCategory和其中的Tile存放到對應的集合中去,用於顯示到頁面上去。
Settings中的Condition
7.0中的Settings加入的Condition可以顯示設定有些item的狀態,並且提供快捷開關,在單擊後,可以跳轉到相應的Settings 頁面。
在上文介紹DashboardCategory的整體佈局的時候,介紹了Condition部分載入的檔案是condition_card.xml檔案
如上圖和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的繼承層次:
我們拿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兩個佈局組成。
關於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有以下的配置:
這個類中定義的常量會在Settings的AndroidManifest.xml使用。
如上圖中定義的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的設計真的是厲害,而且自己很多時候都是從原始碼的功能去理解,其實從架構,效能方面考慮,原始碼都是非常優秀的,有很多值得學習的地方。
歡迎關注我的公眾號 ,不定期會有優質技術文章推送 。
微信掃一掃下方二維碼即可關注