android N中Settings新功能全面解析

来源:互联网 发布:社会关系网络理论 编辑:程序博客网 时间:2024/06/09 23:01

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";
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

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>
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

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";
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

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();    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

recountItems方法会在构建布局的时候多次调用,这个方法里面会在这里加入多个layout布局文件。

如上面的代码的注释标明部分

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

这里使用countItem方法将各个布局加入到List中去,分别是下面三个集合

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

在将这些布局文件加入到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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

接着来看左侧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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112

接着来看看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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

默认情况下这些Condition是关闭的,即enabled的。在这个Receiver中,会去接收这个广播,当Condition的状态改变的时候会去更新状态。

//AirplaneModeCondition.java@Overridepublic 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();       }   }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

Settings中的Suggestion

suggestion能够在设置的主页面显示一些建议项,相当于为一些常用的功能界面添加入口,用户通过点击这些建议项可以跳到相应的页面进行操作,并且用户可以手动移除这些建议项。

如下面的截图,Suggestion页面分为两个 
suggestion_header.xml和suggestion_tile.xml两个布局组成。 
image

关于Suggestion的配置信息: 
Suggestion默认的数量为2个,如上图所示,这个常量的设置是在DashboardAdapter.java里面。

 private static final int DEFAULT_SUGGESTION_COUNT = 2;
  • 1
  • 1

另外这些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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

这里会通过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.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
这个属性允许Suggestion在特定的天数后显示,并且在被拒绝后重新显示。0,10表示该Suggestion会立即显示,但是如果用户删除后,会在10天后再次显示。10,30则表示在10天后显示,然后在30天之后再次显示。
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

以上就是对于android7.0Settings的一些新功能的分析,其实这部分还有很多东西没有详细地去分析,这部分只是做了简单的介绍。

另外,再去看源码的时候,发现Google的设计真的是厉害,而且自己很多时候都是从源码的功能去理解,其实从架构,性能方面考虑,源码都是非常优秀的,有很多值得学习的地方。