1.Settings简介

Settings,包括手机各项属性的基本调整和功能的开关,是用户根据个人喜好对手机进行定制的最方便的入口,也是用户在日常生活中使用频率最高的模块之一。因此,它的稳定性、修改定制,对于开发者来说尤为重要。

在目前的移动设备中,Settings界面除过主题定制的颜色图标等差别外,存在两种形式:单页形式和分页形式。单页形式为主要形式,而在平板等大屏设备中,则会采用分页形式。


图1 单页(左)和分页(右)

原生的Android4.0以后的系统中,将设置分为四个部分:

WIRELESS&NETWORKS:SIM卡管理,流量使用情况,飞行模式,VPN,网络共享等。
DEVICE:情景模式,显示,存储,电池,应用程序。
PERSONAL:账户与同步,位置服务,安全,语言和输入法,备份和重置。
SYSTEM:日期和时间,定时开关及,辅助功能,开发人员选项,关于手机。

2.Settings代码结构

Settings其实是以应用Settings.apk的形式存在于手机系统中的。在Google源码中的路径为:

/packages/apps/Settings/src

具体代码结构图如下:

图2 Settings代码结构图

Settings第一级菜单的显示主要由包com.android.settings下面的Settings.java来负责控制。在该包下面,还包含了其他一些功能设置项的控制类,比如DisplaySettings.java等。其他包从包名基本可以看出,具体负责对应功能模块的控制。各个功能模块封装相对独立,这样,我们只需要进入具体模块,一般就可以完成对其的修改。

3.Settings配置文件

既然是APK,我们进入AndroidManifest.xml文件中可以看到它的配置信息。在该文件中,有相当多的权限使用声明,这正是因为Settings包含众多的模块,不同的模块可能需要不同的权限所致。

<application android:label="@string/settings_label"android:icon="@mipmap/ic_launcher_settings"android:taskAffinity=""android:theme="@style/Theme.Settings"android:hardwareAccelerated="true"android:requiredForAllUsers="true"android:supportsRtl="true"><!-- Settings --><activity android:name="Settings"android:label="@string/settings_label_launcher"android:taskAffinity="com.android.settings"android:configChanges="keyboardHidden|screenSize|mcc|mnc"android:launchMode="singleTask"><intent-filter><action android:name="android.intent.action.MAIN" /><action android:name="android.settings.SETTINGS" /><category android:name="android.intent.category.DEFAULT" /><category android:name="android.intent.category.LAUNCHER" /><category android:name="android.intent.category.APP_SETTINGS" /></intent-filter></activity><activity android:name=".SubSettings"android:taskAffinity="com.android.settings"android:configChanges="orientation|keyboardHidden|screenSize|mcc|mnc"android:parentActivityName="Settings"></activity>
……

第一个标签中,”android.intent.action.MAIN”的action配合”android.intent.category.DEFUALT”的category,决定了整个Settings.APK默认从Settings这个Activity进入。Settings在Launcher进入时,启动的是Settings.java,由”android.intent.category.LAUNCHER”决定。

而整个APK在Launcher中的图标,目标进程,主题,硬件加速,是否面向所有用户,是否支持阿拉伯语等属性在标签下进行定义。

在上面的代码最后,还有一个SubSettings的activity,这也是比较重要的一个类,在小分辨率(未分页)的时候,Settings绝大部分二级菜单都是在SubSettings这个activity中负责控制的。这个后面再讲。

4.Settings实现原理

Settings第一级菜单,是一个ListView,每一个item都是由一个Header构成,整个列表由HeaderAdapter来进行适配。在适配的时候,会取出Header的icon以及title,summary等并放入HeaderViewHolder中,这些就是我们在图一左中看到的外在信息。

然后是对各item的监听,当点击一个item的时候,跳转到具体的模块对应的Fragment中去。

分页模式和单页模式在基本实现上是一致的,区别在于分页模式Header和对应的Fragment将同时显示,因此,在对应模块的Fragment的显示的时候有区别,这个后面再讲。

以上,是Settings实现的基本流程,出现的几个词汇分别是Header、Fragment、HeaderAdapter、HeaderViewHolder,后面代码遇到的时候会讲。这里知道大概流程以及需要这些组件就可以了。

5.Settings代码分析

5.1 父类PreferenceActivity.java

我们首先进入Settings.java,它的注释说得很清楚,这个类是用来处理Settings单页和双页的UI布局的顶级Activity。

/*** Top-level settings activity to handle single pane and double pane UI layout.*/
public class Settings extends PreferenceActivityimplements ButtonBarHandler, OnAccountsUpdateListener {

它继承于PreferenceActivity,并实现了ButtonBarHandler和onAccountsUpdateListener接口。PreferencActivity以下简称PA,需要重点分析,因为在当前Settings.java中的部分方法就是重写PA的,有很多重要的代码,单单在Settings.java中是无法理解的,必须进入PA中,才能发现根本原理。而两个接口,只是为了增加按钮栏的处理和账户更新处理的功能,我们不去深入讲。

5.2 布局文件preference_content_list.xml

在PA的onCreate()方法中,通过setContentView()设置了preference_content_list的布局,该布局文件定义了Settings的主要界面表现形式。代码如下。

<LinearLayout ①xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_height="match_parent"android:layout_width="match_parent"><LinearLayout ②android:orientation="horizontal"android:layout_width="match_parent"android:layout_height="0px"android:layout_weight="1"><LinearLayout ③style="?attr/preferenceHeaderPanelStyle"android:id="@+id/headers"android:orientation="vertical"android:layout_width="0px"android:layout_height="match_parent"android:layout_weight="@integer/preferences_left_pane_weight"><ListView android:id="@android:id/list"style="?attr/preferenceListStyle"android:layout_width="match_parent"android:layout_height="0px"android:layout_weight="1"android:clipToPadding="false"android:drawSelectorOnTop="false"android:cacheColorHint="@android:color/transparent"android:listPreferredItemHeight="48dp"android:scrollbarAlwaysDrawVerticalTrack="true" /><FrameLayout android:id="@+id/list_footer" ④android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_weight="0" /> /④</LinearLayout> /③<LinearLayout ⑤android:id="@+id/prefs_frame"style="?attr/preferencePanelStyle"android:layout_width="0px"android:layout_height="match_parent"android:layout_weight="@integer/preferences_right_pane_weight"android:orientation="vertical"android:visibility="gone" ><!-- Breadcrumb inserted here, in certain screen sizes. In others, it will be anempty layout or just padding, and PreferenceActivity will put the breadcrumbs inthe action bar. --><include layout="@layout/breadcrumbs_in_fragment" /><android.preference.PreferenceFrameLayout android:id="@+id/prefs"android:layout_width="match_parent"android:layout_height="0dip"android:layout_weight="1"/></LinearLayout> /⑤</LinearLayout> /②<RelativeLayout android:id="@+id/button_bar" ⑥android:layout_height="wrap_content"android:layout_width="match_parent"android:layout_weight="0"android:visibility="gone"><Button android:id="@+id/back_button"android:layout_width="150dip"android:layout_height="wrap_content"android:layout_margin="5dip"android:layout_alignParentStart="true"android:text="@string/back_button_label"/><LinearLayout ⑦android:orientation="horizontal"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignParentEnd="true"><Button android:id="@+id/skip_button"android:layout_width="150dip"android:layout_height="wrap_content"android:layout_margin="5dip"android:text="@string/skip_button_label"android:visibility="gone"/><Button android:id="@+id/next_button"android:layout_width="150dip"android:layout_height="wrap_content"android:layout_margin="5dip"android:text="@string/next_button_label"/></LinearLayout> /⑦</RelativeLayout> /⑥
</LinearLayout> /①

布局图如下:

图3 PreferenceActivity布局图

从上面布局图,很容易看出,id为headers的LinearLayout即放置HeaderList的地方,右侧则为放置Fragment的地方。单页的时候,只显示左侧LinearLayout③;分页后,右侧的LinearLayout⑤由默认的不显示变为显示,就成为了图1(右)分页后的效果。至于最下方的RelativeLayout⑥,为返回、跳过、前进按钮,默认是不显示的。

5.3分页相关代码块

5.3.1 判断是否是分页模式的方法onIsMultiPane()

在PA中,有个方法onIsMultiPane()来判断是否需要进行分页显示。代码如下,而它其实是通过读取系统属性preferences_prefer_dual_pane的值来判定的。该布尔值位于/frameworks/base/core/res/res/values/bools.xml中。

/*** Called to determine if the activity should run in multi-pane mode.* The default implementation returns true if the screen is large* enough.*/public boolean onIsMultiPane() {boolean preferMultiPane = getResources().getBoolean(com.android.internal.R.bool.preferences_prefer_dual_pane);return preferMultiPane;}

5.3.2 是否是单页模式的标志mSinglePane

在PA中,有一个布尔值mSinglePane专门用来标识是否是单页还是分页显示。

private boolean mSinglePane;

它在onCreate()方法中获得具体值,如果HeaderList被隐藏了(意味着此时只会显示具体模块的内容)或者非多页模式,那么mSinglePane即为true,表示单页模式。在PA中,涉及到切换到双页模式的几处关键代码,都和这个值有关。下面接着看其他地方。

boolean hidingHeaders = onIsHidingHeaders();mSinglePane = hidingHeaders || !onIsMultiPane();

5.3.3 控制Fragment的显示

下面的代码仍然在onCreate()方法中,重点看else分支,这个分支即表示切换到分页模式,如果是分页模式且initialFragment为空,也就是暂时没有要显示的Fragment,则通过onGetInitialHeader()方法获取一个初始Header,然后通过switchHeader(h)方法将Header(此时为分页模式,在显示该Header的时候会同样会将整个HeaderList显示出来)和对应的Fragment显示出来。如果initialFragment本来就不为空,则通过switchHeader(initialFragment,initialArgument)方法将此Fragment显示出来。

 if (initialFragment != null && mSinglePane) {Log.d(TAG, "    Show a fragment from EXTRA_SHOW_FRAGMENT.");// If we are just showing a fragment, we want to run in// new fragment mode, but don't need to compute and show// the headers.switchToHeader(initialFragment, initialArguments);if (initialTitle != 0) {CharSequence initialTitleStr = getText(initialTitle);CharSequence initialShortTitleStr = initialShortTitle != 0? getText(initialShortTitle) : null;showBreadCrumbs(initialTitleStr, initialShortTitleStr);}} else {// We need to try to build the headers.onBuildHeaders(mHeaders);// If there are headers, then at this point we need to show// them and, depending on the screen, we may also show in-line// the currently selected preference fragment.if (mHeaders.size() > 0) {Log.d(TAG, "    Build headers successfully.");if (!mSinglePane) {if (initialFragment == null) {Header h = onGetInitialHeader();switchToHeader(h);} else {switchToHeader(initialFragment, initialArguments);}}}

在上面代码中,出现几个重要方法:switchToHeader(initialFragment, initialArguments)、showBreadCrumbs(initialTitleStr, initialShortTitleStr)、switchToHeader(h)。可以说,这几个方法决定了分页显示的最终结果。下面将代码贴出来。

/*** When in two-pane mode, switch the fragment pane to show the given* preference fragment.** @param fragmentName The name of the fragment to display.* @param args Optional arguments to supply to the fragment.*/public void switchToHeader(String fragmentName, Bundle args) {setSelectedHeader(null);switchToHeaderInner(fragmentName, args, 0);}/*** When in two-pane mode, switch to the fragment pane to show the given* preference fragment.** @param header The new header to display.*/public void switchToHeader(Header header) {if (mCurHeader == header) {// This is the header we are currently displaying.  Just make sure// to pop the stack up to its root state.getFragmentManager().popBackStack(BACK_STACK_PREFS,FragmentManager.POP_BACK_STACK_INCLUSIVE);} else {if (header.fragment == null) {throw new IllegalStateException("can't switch to header that has no fragment");}int direction = mHeaders.indexOf(header) - mHeaders.indexOf(mCurHeader);switchToHeaderInner(header.fragment, header.fragmentArguments, direction);setSelectedHeader(header);}}

可以看到,这两个为switchToHeader()的参数重载方法。它们最终,都调用了方法switchToHeaderInner(),这个方法中对即将要显示的Fragment进行了初始化,并通过FragmentTransaction的方式启动。

 private void switchToHeaderInner(String fragmentName, Bundle args, int direction) {getFragmentManager().popBackStack(BACK_STACK_PREFS,FragmentManager.POP_BACK_STACK_INCLUSIVE);if (!isValidFragment(fragmentName)) {throw new IllegalArgumentException("Invalid fragment for this activity: "+ fragmentName);}Fragment f = Fragment.instantiate(this, fragmentName, args);FragmentTransaction transaction = getFragmentManager().beginTransaction();transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);transaction.replace(com.android.internal.R.id.prefs, f);transaction.commitAllowingStateLoss();}

而showBreadCrumbs()为两个参数重载方法。

void showBreadCrumbs(Header header) {if (header != null) {CharSequence title = header.getBreadCrumbTitle(getResources());if (title == null) title = header.getTitle(getResources());if (title == null) title = getTitle();showBreadCrumbs(title, header.getBreadCrumbShortTitle(getResources()));} else {showBreadCrumbs(getTitle(), null);}}

上面这个单参数方法,最终其实也是调用了它的另外一个重载方法。它的功能。。。

/*** Change the base title of the bread crumbs for the current preferences.* This will normally be called for you.  See* {@link android.app.FragmentBreadCrumbs} for more information.*/public void showBreadCrumbs(CharSequence title, CharSequence shortTitle) {if (mFragmentBreadCrumbs == null) {View crumbs = findViewById(android.R.id.title);// For screens with a different kind of title, don't create breadcrumbs.try {mFragmentBreadCrumbs = (FragmentBreadCrumbs)crumbs;} catch (ClassCastException e) {setTitle(title);return;}if (mFragmentBreadCrumbs == null) {if (title != null) {setTitle(title);}return;}if (mSinglePane) {mFragmentBreadCrumbs.setVisibility(View.GONE);// Hide the breadcrumb section completely for single-paneView bcSection = findViewById(com.android.internal.R.id.breadcrumb_section);if (bcSection != null) bcSection.setVisibility(View.GONE);setTitle(title);}mFragmentBreadCrumbs.setMaxVisible(2);mFragmentBreadCrumbs.setActivity(this);}if (mFragmentBreadCrumbs.getVisibility() != View.VISIBLE) {setTitle(title);} else {mFragmentBreadCrumbs.setTitle(title, shortTitle);mFragmentBreadCrumbs.setParentTitle(null, null, null);}}

5.3.4 控制Fragment在其他配置下的显示

重新回到PA的onCreate()方法中,继续向下看。

  // The default configuration is to only show the list view.  Adjust// visibility for other configurations.if (initialFragment != null && mSinglePane) {Log.d(TAG, "    Single pane, showing just a prefs fragment.");// Single pane, showing just a prefs fragment.findViewById(com.android.internal.R.id.headers).setVisibility(View.GONE);mPrefsContainer.setVisibility(View.VISIBLE);if (initialTitle != 0) {CharSequence initialTitleStr = getText(initialTitle);CharSequence initialShortTitleStr = initialShortTitle != 0? getText(initialShortTitle) : null;showBreadCrumbs(initialTitleStr, initialShortTitleStr);}} else if (mHeaders.size() > 0) {Log.d(TAG, "    Set list adapter created from headers.");setListAdapter(new HeaderAdapter(this, mHeaders));if (!mSinglePane) {// Multi-pane.getListView().setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);if (mCurHeader != null) {setSelectedHeader(mCurHeader);}mPrefsContainer.setVisibility(View.VISIBLE);}} else {Log.d(TAG, "    In the old \"just show a screen of preferences\" mode.");// If there are no headers, we are in the old "just show a screen// of preferences" mode.setContentView(com.android.internal.R.layout.preference_list_content_single);mListFooter = (FrameLayout) findViewById(com.android.internal.R.id.list_footer);mPrefsContainer = (ViewGroup) findViewById(com.android.internal.R.id.prefs);mPreferenceManager = new PreferenceManager(this, FIRST_REQUEST_CODE);mPreferenceManager.setOnPreferenceTreeClickListener(this);}

5.3.5 Settings.java中设置Settings的label

在Settings.java中,也有部分与分页有关的代码。这部分代码,主要是PreferenceActivity无法直接满足Settings具体的要求而进行修改定制时所用。下面这段代码在onCreate()方法中,作用是在分页模式下,将界面的标题设置为Settings的label。这样从Launcher一进入Settings第一级菜单,就会看到左上角的应用标题为Settings。没有这段代码,前面提到的在PA的onCreate()方法中的onGetInitialHeader()方法将会生效,那么第一次进入后将使用HeaderList的第一个Header(WifiSettings)的标题作为标题。

 if (!onIsHidingHeaders() && onIsMultiPane()) {highlightHeader(mTopLevelHeaderId);// Force the title so that it doesn't get overridden by a direct launch of// a specific settings screen.setTitle(R.string.settings_label);}

5.3.6 Settings中禁用顶端Home返回键

仍然在Settings的onCreate()方法中,下面的代码用于在分页的时候,禁用界面顶端的Home返回键。从这些代码看出,如果要对Settings的一级菜单进行定制,在onCreate()方法中增加相应的控制代码就可以。

  // Override up navigation for multi-pane, since we handle it in the fragment breadcrumbsif (onIsMultiPane()) {getActionBar().setDisplayHomeAsUpEnabled(false);getActionBar().setHomeButtonEnabled(false);}

5.3.7 Settings.java中的onNewIntent函数

如果不是从历史栈中启动,将重置到一级菜单。如果是分页模式,将调用switchToHeaderLocal()方法,其最终调用的是PA的switchToHeader()方法,前面已经有介绍。

   @Overridepublic void onNewIntent(Intent intent) {super.onNewIntent(intent);// If it is not launched from history, then reset to top-levelif ((intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == 0) {if (mFirstHeader != null && !onIsHidingHeaders() && onIsMultiPane()) {switchToHeaderLocal(mFirstHeader);}getListView().setSelectionFromTop(0, 0);}}

5.3.8 Settings.java中的getIntent函数

下面getIntent()方法,作用是对传递过来的Intent作一下判断和处理,增加Extra信息。主要需要理解这个方法:getStartFragmentClass()。它将得到的superIntent中的组件名与Settings类的进行比对,如果相同则返回null;如果不同,则返回类名,使其能够以Fragment的形式进行加载。不难发现,这个方法对分页模式不会有任何影响。

 @Overridepublic Intent getIntent() {Intent superIntent = super.getIntent();String startingFragment = getStartingFragmentClass(superIntent);// This is called from super.onCreate, isMultiPane() is not yet reliable// Do not use onIsHidingHeaders either, which relies itself on this methodif (startingFragment != null && !onIsMultiPane()) {Intent modIntent = new Intent(superIntent);modIntent.putExtra(EXTRA_SHOW_FRAGMENT, startingFragment);Bundle args = superIntent.getExtras();if (args != null) {args = new Bundle(args);} else {args = new Bundle();}args.putParcelable("intent", superIntent);modIntent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, superIntent.getExtras());return modIntent;}return superIntent;}

5.4 Settings.java核心代码

5.4.1 Settings.java的onCreate函数

onCreate()方法在刚才已经有讲过,主要对PA进行进一步的定制,不再多说。

5.4.2 Settings.java的onResume函数

onResume()方法中,进行了多个BroadcastRecevier的注册。其中一个比较重要的地方,就是对【开发者选项】的监听器。在用户版本,默认【开发者选项】是被隐藏的。只有在第一级菜单先进入【关于手机】,然后连续按7次【Build Number】后,才能将其激活,从而在第一级菜单中显示出来。下面的代码就是这个监听器的创建和注册。

   mDevelopmentPreferencesListener = new SharedPreferences.OnSharedPreferenceChangeListener() {@Overridepublic void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {invalidateHeaders();}};mDevelopmentPreferences.registerOnSharedPreferenceChangeListener(mDevelopmentPreferencesListener);

5.4.3 Settings.java的onPause函数

在onPause()方法中,对在onResume()方法中注册的监听器进行unRegisterRecevier()操作。(代码略)

5.4.4 Settings.java的onBuilderHeaders函数

Settings一级菜单中几乎所有(账户相关的由代码中的具体方法控制增删,后面有讲)的Header均是通过onBuildHeaders()方法进行加载的。

  /*** Populate the activity with the top-level headers.*/@Overridepublic void onBuildHeaders(List<Header> headers) {if (!onIsHidingHeaders()) {PDebug.Start("loadHeadersFromResource");loadHeadersFromResource(R.xml.settings_headers, headers);PDebug.End("loadHeadersFromResource");updateHeaderList(headers);}}

从上面代码中,可以看到一个非常重要的XML文件:settings_headers.xml。所有要显示的Header均在这个文件中以 < header>的形式进行定义。

每一个< header>中,定义了id、icon、fragment、title属性,各自的作用分别为:id用来标识是哪个header;icon即Settings一级菜单上显示的每一个item前的图像;fragment用来指定具体启动的类,用完整的包名表示;title即一级菜单中每一个item的名称,例如Wifi、Bluetooth等。

所以,我们要增加一个功能的话,只需要在这个文件中增加一个< header>,然后实现对应的功能类即可。

<preference-headers
        xmlns:android="http://schemas.android.com/apk/res/android"><!-- WIRELESS and NETWORKS --><header android:id="@+id/wireless_section"android:title="@string/header_category_wireless_networks" /><!-- Sim management --><header
        android:id="@+id/sim_settings"android:icon="@drawable/ic_settings_dualsim"android:fragment="com.mediatek.gemini.SimManagement"android:title="@string/gemini_sim_management_title" /><!-- Wifi --><header
        android:id="@+id/wifi_settings"android:fragment="com.android.settings.wifi.WifiSettings"android:title="@string/wifi_settings_title"android:icon="@drawable/ic_settings_wireless" /><!-- Bluetooth --><header
        android:id="@+id/bluetooth_settings"android:fragment="com.android.settings.bluetooth.BluetoothSettings"android:title="@string/bluetooth_settings_title"android:icon="@drawable/ic_settings_bluetooth2" />……(省略中间部分)……<header
        android:id="@+id/about_settings"android:fragment="com.android.settings.DeviceInfoSettings"android:icon="@drawable/ic_settings_about"android:title="@string/about_settings" /></preference-headers>

5.4.5 Settings.java的updateHeaderList函数

接着上面,在通过loadHeadersFromResource()方法加载settings_headers.xml后,紧跟着调用方法updateHeaderList(headers)对headers做具体的处理。这块的代码比较长,具体所做的事就是对各个具体功能根据需要进行控制,代码逻辑非常清晰,就不再贴出来了。

5.4.6 Settings.java的HeaderAdapter内部类

HeaderAdapter为整个Settings的一级菜单ListView的适配器,它声明为了Settings.java的静态内部类,继承自ArrayAdapter。与所有Adapter一样,它的主要内容是将我们之前得到的headersList如何显示在ListView中去。我讲一下需要特别理解的主要思路:如下面代码,预先定义了5种Header的类型,以满足对不同外观的Header的分别管控处理,比如Wifi和蓝牙这样的带有开关的,即HEADER_TYPE_SWITCH;DisplaySettings这样正常的,即HEADER_TYPE_NORMAL;快速启动这样带有CheckBox的,即HEADER_TYPE_CHECK;还有fragment和intent都为空的只是为了做区分的Header,即HEADER_TYPE_CHECK。

不同的Header可以自定义不同的XML布局,这样,就使得一级菜单每一个item根据功能的不同表现出不同的外观。

   private static class HeaderAdapter extends ArrayAdapter<Header> implementsCompoundButton.OnCheckedChangeListener {static final int HEADER_TYPE_CATEGORY = 0;static final int HEADER_TYPE_NORMAL = 1;static final int HEADER_TYPE_SWITCH = 2;static final int HEADER_TYPE_BUTTON = 3;static final int HEADER_TYPE_CHECK = 4;private static final int HEADER_TYPE_COUNT = HEADER_TYPE_CHECK + 1;

在HeaderAdapter.java中,又定义了一个内部静态类HeaderViewHolder,它相当每一个Header在ListView中要表现的所有元素的数据类型集合。

      private static class HeaderViewHolder {ImageView icon;TextView title;TextView summary;Switch switch_;CheckBox check;//add by yangzhong.gong for FR581773ImageButton button_;View divider_;}

而获取视图的方法为getView(),主要思路很简单,就是判断不同类型的HEADER_TYPE,然后根据不同的HEADER_TYPE做不同的处理,将所需要的title、icon等等信息装入事先定义好的HeaderViewHolder对象。然后用setTag(holder)方法传递给view对象,最后将view返回。(代码略)

最后就是单击等操作的监听器设置。这里只是想强调一点,附加功能都可以通过接口来实现,例如在上个例子里,就实现了CompoundButton.OnCheckedChangeListener接口,而这个是我们在做定制时自己添加的。

@Overridepublic void onHeaderClick(Header header, int position) {boolean revert = false;if (header.id == R.id.account_add) {revert = true;}super.onHeaderClick(header, position);if (revert && mLastHeader != null) {highlightHeader((int) mLastHeader.id);} else {mLastHeader = header;}if (header.id == R.id.regulatory_safety) {Intent intent = new Intent();ComponentName comp = new ComponentName("com.eyelike.Elabel","com.eyelike.Elabel.SettingsRegulatoryActivity");intent.setComponent(comp);startActivity(intent);}}

5.5 定制相关的代码块

5.5.1 定制fragment

在Settings.java中定义了一个字符串数组ENTY_FRAGMENTS,这个数组的声明与方法isValidFragment()关系甚大。而isValidFragment()方法是PA用来判断Fragment是否可用的,在Settings.java中做了复写。

之前在讲Settings主要实现原理的时候有讲,每一个具体功能都由Fragment来实现。如果我们想要在第一级菜单中增加一个功能,只需要在

private static final String[] ENTRY_FRAGMENTS = {WirelessSettings.class.getName(),WifiSettings.class.getName(),AdvancedWifiSettings.class.getName(),BluetoothSettings.class.getName(),TetherSettings.class.getName(),WifiP2pSettings.class.getName(),VpnSettings.class.getName(),DateTimeSettings.class.getName(),LocalePicker.class.getName(),InputMethodAndLanguageSettings.class.getName(),SpellCheckersSettings.class.getName(),UserDictionaryList.class.getName(),UserDictionarySettings.class.getName(),SoundSettings.class.getName(),DisplaySettings.class.getName(),DeviceInfoSettings.class.getName(),ManageApplications.class.getName(),ProcessStatsUi.class.getName(),NotificationStation.class.getName(),LocationSettings.class.getName(),SecuritySettings.class.getName(),PrivacySettings.class.getName(),DeviceAdminSettings.class.getName(),AccessibilitySettings.class.getName(),ToggleCaptioningPreferenceFragment.class.getName(),TextToSpeechSettings.class.getName(),Memory.class.getName(),DevelopmentSettings.class.getName(),UsbSettings.class.getName(),AndroidBeam.class.getName(),WifiDisplaySettings.class.getName(),PowerUsageSummary.class.getName(),AccountSyncSettings.class.getName(),CryptKeeperSettings.class.getName(),DataUsageSummary.class.getName(),DreamSettings.class.getName(),UserSettings.class.getName(),NotificationAccessSettings.class.getName(),ManageAccountsSettings.class.getName(),PrintSettingsFragment.class.getName(),PrintJobSettingsFragment.class.getName(),TrustedCredentialsSettings.class.getName(),PaymentSettings.class.getName(),KeyboardLayoutPickerFragment.class.getName(),//M@:SimManagement.class.getName(),SimInfoEditor.class.getName(),//Class name same as Activity name so use full name herecom.mediatek.gemini.SimDataRoamingSettings.class.getName(),AudioProfileSettings.class.getName(),Editprofile.class.getName(),HDMISettings.class.getName(),SelectSimCardFragment.class.getName(),UsbSharingChoose.class.getName(),UsbSharingInfo.class.getName(),TetherWifiSettings.class.getName(),DrmSettings.class.getName(),NfcSettings.class.getName(),WifiGprsSelector.class.getName(),BeamShareHistory.class.getName(),CardEmulationSettings.class.getName(),MtkAndroidBeam.class.getName(),HotKnotSettings.class.getName(),MasterClear.class.getName()//add by eyelike};@Overrideprotected boolean isValidFragment(String fragmentName) {// Almost all fragments are wrapped in this,// except for a few that have their own activities.for (int i = 0; i < ENTRY_FRAGMENTS.length; i++) {if (ENTRY_FRAGMENTS[i].equals(fragmentName)) return true;}return false;}

5.5.2 定制ActionBar

在Settings的第二级菜单,也就是各个具体功能界面,大量应用了ActionBar(界面最上方的条状栏,右侧往往有开关等功能按钮)。而在Wifi、蓝牙等设置界面,有时候会看到界面下方也有按钮,如下图。

图4 ActionBar分离示例图
这是由ActionBar的属性来控制的,对应于XML文件中的属性为:

android:uiOptions="splitActionBarWhenNarrow"

而在Settings.java中的控制在onBuildStartFragmentIntent()方法中,代码如下。如果要修改相关功能,只需在其中做增删即可。

 @Overridepublic Intent onBuildStartFragmentIntent(String fragmentName, Bundle args,int titleRes, int shortTitleRes) {Intent intent = super.onBuildStartFragmentIntent(fragmentName, args,titleRes, shortTitleRes);// Some fragments want split ActionBar; these should stay in sync with// uiOptions for fragments also defined as activities in manifest.if (WifiSettings.class.getName().equals(fragmentName) ||WifiP2pSettings.class.getName().equals(fragmentName) ||BluetoothSettings.class.getName().equals(fragmentName) ||DreamSettings.class.getName().equals(fragmentName) ||LocationSettings.class.getName().equals(fragmentName) ||BeamShareHistory.class.getName().equals(fragmentName) ||MtkAndroidBeam.class.getName().equals(fragmentName) ||AudioProfileSettings.class.getName().equals(fragmentName) ||                ToggleAccessibilityServicePreferenceFragment.class.getName().equals(fragmentName) ||PrintSettingsFragment.class.getName().equals(fragmentName) ||PrintServiceSettingsFragment.class.getName().equals(fragmentName) ||HotKnotSettings.class.getName().equals(fragmentName)) {intent.putExtra(EXTRA_UI_OPTIONS, ActivityInfo.UIOPTION_SPLIT_ACTION_BAR_WHEN_NARROW);}intent.setClass(this, SubSettings.class);return intent;}

5.6 Settings其他重要问题释疑

以上通过代码段对主要实现进行了介绍,但是,如果跳出一小块一小块代码,从整体上来看,还是会有一些一时难以琢磨理解的疑问。下面,就将我曾经遇到的一些主要疑问列出来,并做一些解答。

5.6.1 为什么使用Hierarchyviewer 工具查看时Settings中的很多界面显示的都是SubSettings?

要解决这个问题我们先要清楚为什么会写一个SubSettings.java继承自Settings.java?SubSettings.java的内容非常简单,代码如下。

/*** Stub class for showing sub-settings; we can't use the main Settings class* since for our app it is a special singleTask class.*/
public class SubSettings extends Settings {@Overridepublic boolean onNavigateUp() {finish();return true;}@Overrideprotected boolean isValidFragment(String fragmentName) {Log.d("SubSettings", "Launching fragment " + fragmentName);return true;}
}

SubSettings.java中的注释很清楚的告诉了我们原因:Stub class for showing sub-settings; we can’t use the main Settings class since for our app it is a special singleTask class。

原来是因为Settings.java在声明时指定了android:launchMode=”singleTask”。

要显示Fragment的内容,我们就必须为其指定一个Activity。而Settings中的很多设置界面是由PreferenceFragment来完成的,当然也需要我们指定Activity。PA中得onBuildStartFragmentIntent函数会为我们构造一个显示Fragment的Intent对象(该函数的注释写的非常明白)。Settings.java重写了这个函数(见4.2,重写时它调用了super的该方法),在为intent对象setClass时都使用SubSettings.java(注:在settings_headers.xml指定了intent的header是不会触发onBuildStartFragmentIntent的)。

结果就是,Settings中大部分fragment都是使用的SubSettings这个Activity来显示。由于Hierarchyviewer只是显示当前界面使用的Activity(不能显示这个界面是由哪个Fragment构造的),所以我们使用Hierarchyviewer 对Settings进行观察时很多设置界面显示的是SubSettings。

5.6.2 Hierarchyviewer 中显示SubSetting时如何确定我进入的是哪个fragment?

在res/xml/settings_headers.xml中声明了各个header被点击后使用的fragment。我们可以根据这个文件确定我们进入的fragment。

例如,当我们点击Display时Hierarchyviewer 中显示SubSetting。我们通过查找settings_headers就可知道使用的是哪个fragment(见5.1)。header中使用 android:fragment指明使用的fragment。由此可知,Display使用的是com.android.settings.DisplaySettings这个fragment。

5.6.3 点击设置界面的某一个header时,设置界面是如何切换的?

点击设置界面的header时,会触发Settings中onHeaderClick函数,主要的处理都在其父类PreferenceActivity的onHeaderClick中实现的。如果这个header指定了fragment,在mSinglePane(见5.3)为true时,会调用startWithFragment方法,在startWithFragment方法中将调用onBuildStartFragmentIntent方法来构造intent对象(重要),最后使用该intent对象启动一个activity来显示fragment。

以点击Settings中的Display为例(Bluetooth同理,只不过启动的Activity变为BluetoothSettingsActivity(继承自Settings,但是没有实现重写任何方法,所以与SubSettings是一样的处理),fragment变为 com.android.settings.bluetooth.BluetoothSettings)。fragment是com.android.settings.DisplaySettings,activity是com.android.settings.SubSettings(fragment是由onHeaderClick函数传入的,activity是由onBuildStartFragmentIntent()指定的)。

执行startActivity后将启动SubSettings.java。即我们将会再一次执行SubSettings和PreferenceActivity的onCreate方法(因为Settings.java的onCreate方法调用了super.onCreate()),但是这次并不会进入Settings的主界面,因为我们的使用的intent对象是有很大不同的。这一次onCreate函数(PreferenceActivity)中的initialFragment 将被初始化为com.android.settings.DisplaySettings,然后我们将进入switchToHeader(),最后switchToHeaderInner会取得FragmentTransaction对象(见5.6),然后执行了transaction.replace(com.android.internal.R.id.prefs, f)。就这样把我们的fragment显示出来了。在onCreate中会对其他view的visibility进行设置,以保证只显示prefs。如,将com.android.internal.R.id.headers的visibility设置为VIEW.GONE。

5.6.4 Settings.java中getMetaData与getStartingFragmentClass这两个函数是否有点矛盾?

这两个函数可以说是相辅相成的。getMetaData会从AndroidManifest.xml中读取Activity的节点的数据;getStartingFragmentClass则从启动Activity的intent中读取数据。这两个函数会对读取到的数据进行整合,getStartingFragmentClass依赖于getMetaData读取到的数据,但是它也可能对数据作出修改(为了兼容性,如对原有manage apps类进行特殊处理)。

5.6.5 Settings的shortcut是如何创建的?从shortcut进入Settings的流程是什么?

创建Settings的shortcut时Luancher将会启动CreateShortcut,创建shortcut所需的intent对象将会由CreateShortcut和其父类LuancherActivity共同构建(详见 CreateShortcut的onListItemClick),这时创建的Intent对象使用的就不是SubSettings了(LuancherActivity中intentForPosition函数执行setClassName()时使用的参数并不是SubSettings)。

 public Intent intentForPosition(int position) {if (mActivitiesList == null) {return null;}Intent intent = new Intent(mIntent);ListItem item = mActivitiesList.get(position);intent.setClassName(item.packageName, item.className);if (item.extras != null) {intent.putExtras(item.extras);}return intent;}

CreateShortcut中列出了可以创建shortcut的设置项,这些设置项怎样检索出来的?
原来,在创建LuancherActivity的ActivityAdapter对象时,其构造函数中执行了makeListItems函数,该函数将使用PackageManager的queryIntentActivities来根据intent对象查询符合条件的activity。使用的intent是从getTargetIntent函数返回的。不难发现,要想在CreateShortcut中显示,Activity在必须要有

<category android:name="com.android.settings.SHORTCUT" />

如果我们想将Security设置项添加到shortcut列表,我们只需要在androidmanifest.xml中声明Settings$SecuritySettingsActivity部分加上

<category android:name="com.android.settings.SHORTCUT" />

即可。

回到正题,点击shortcut进入Settings时,传入的Intent对象中包含了目标fragment和目标activity以及其他信息。PreferenceActivity得到了足够多的信息,因此在onCreate中将依次执行switchToHeader()->setSelectedHeader(null)->switchToHeaderInner()->transaction.replace(com.android.internal.R.id.prefs, f);
这样就完成了fragment的显示(使用的activity是从intent解析出来的。在switchToHeaderInner中执行Fragment.instantiate时使用的Context是this!!)。不像执行onHeaderClick那样会执行函数onBuildStartFragmentIntent(Settings中重写了该函数)来重新指定我们使用的Activity。

5.6.6 为什么我从Settings的shortcut进入时,Hierarchyviewer显示的就不是SubSettings(如Data usage)?

Hierarchyviewer中显示SubSettings是因为我们在onBuildStartFragmentIntent方法中做了特殊处理(详见问题二)。从shortcut进入Settings时不显示SubSettings是因为没有走这个函数,因此就不会显示为SubSettings了(详见问题六)。

5.6.7 Settings.java中很多继承自它的内部类都是空实现,为什么要写这些类?

空实现,使得他们虽然被声明,但仍然都将使用Settings.java中的函数(注意private的属性和方法的访问权限问题)。因此,这样的构造必定是为了其他的便利。注释讲了一点原因:声明的这些类都将作为Settings的子类,为的是在启动的时候保持独立性。这样能够提高各个设置项、整个Settings的灵活性,方便开发者进行扩展。

/** Settings subclasses for launching independently.*/

除此之外,和整个Settings的设计结构也由一定关系:

①这样声明非常清晰明朗,易于维护;
②可以让我们为单独的设置项添加 shortcut(如data usage),因为创建shortcut使用queryIntentActivities查询使用的activity;
③允许其它程序访问单独的设置项;
④结构设计需要,启动Activity会读取meta-data信息;
⑤使得某些设置项可以不使用SubSettings的属性。如,在Settings中点击Bluetooth时使用BluetoothSettingsActivity,启动Bluetooth时将使用BluetoothSettingsActivity的属性,如 android:clearTaskOnLaunch=”true”。
等等。

  • eyelike@2014-06-11

Android Settings总结相关推荐

  1. Android Settings开发之修改

    版本:1.0 日期:2014.3.20  2014.3.25   版权:© 2014 kince 转载注明出处                下面是Seeings应用的截图:   可以看出这是很典型的 ...

  2. Android开发:关于Android Settings中的八个问题

    本问将回答以下八个问题,如有错误,敬请批评指正,不胜感激!(注:本文中的Settings解析基于android4.0+) 问题一.Settings的主界面是怎么实现的? 问题二.为什么使用hierar ...

  3. android settings源代码分析(3)

    本章主要分析google settings里面存储模块的代码. 存储模块所在的fragment为: [html] view plaincopy <!-- Storage --> <h ...

  4. android settings源代码分析(2)

    通过前一篇文章  Android settings源代码分析(1)  分析,大概知道了Settings主页面是如何显示,今天主要分析"应用"这一块google是如何实现的. 应用对 ...

  5. android settings源代码分析(1)

    1.Android settings源码的source code路径为: kikat_4.4_CTS\packages\apps\Settings 2.settings主界面UI布局 Settings ...

  6. android settings 源码,菜鸟学Android源码-Settings(2)

    最近一直忙着项目上的活计,没啥心思关注博客,前两天屌丝逆袭, 看了一场你的名字,深深感叹叔叔辈的无奈啊, 青葱憧憬爱情的年华,垂老怀旧相依的岁月, 而我老未及老,幼以过幼, 哎,白瞎了一张电影票. 上 ...

  7. Android Settings 应用二 获取应用消耗的流量

    了解这一块也是因为有需求要获取指定应用所消耗的流量.在Android中,流量消耗主要分为手机卡和WIFI,在Settings中,也有统计流量的使用情况.经了解,Settings中是通过Loader去加 ...

  8. Android Settings 中wifi详情页 隐私默认选中设备mac

    1.概述 在10.0的系统产品开发中,在对ota升级中,由于要固定使用本机mac地址,所以不能让选择默认mac地址,这时就要修改默认设置,保证升级能够成功 2.Settings 中wifi详情页 隐私 ...

  9. Android Settings模块

    Settings SettingsIntelligence XML:Preference.PreferenceScreen.PreferenceCategory Fragment:DashboardF ...

  10. qt 关联android,从QT app(com.android.settings)打开android设置

    QAndroidJniObject可以从Qt C代码创建JNI对象. 例如:获取活动: QAndroidJniObject activity = QAndroidJniObject::callStat ...

最新文章

  1. Matplitlib绘图入门1,这一篇就够了
  2. 力扣(LeetCode)刷题,简单题(第2期)
  3. TensorFlow AI 新品更易用!联手NVIDIA,支持Swift和JavaScript
  4. java plug in错误_Eclipse启动失败 - 在安装BlackBerry Java Plug-in for Eclipse v1.3之后
  5. SSM三大框架整合Springfox(Swagger2)详细解决方案
  6. cmd测试cuda安装_安装:anaconda+cuda+pytorch+pycharm
  7. 7-177 输出全排列 (20 分)
  8. 《计算机网络基础》第一套作业,东财在线21春《计算机网络基础》第一套作业【标准答案】...
  9. 不是shell具有的功能和特点的是_环境监控主机具有哪些功能特点
  10. 【转】拉勾网 - 《2016互联网职场生态白皮书》
  11. web前端常用开发工具有哪些?
  12. win10右键一直转圈_win10投屏不能使用的解决办法
  13. 【STM32】关于DMA控制器的介绍和使用
  14. 三星note3smn9005怎么放SIM卡
  15. 图像处理——中值滤波
  16. python中str.format用法_详解Python中的str.format方法
  17. 操作系统基础:进程逻辑思维导图,超简单理解进程管理
  18. 计算机机房 网络安全,机房网络安全建设标准参考
  19. HTTPS —— HTTPS的加密方式
  20. 记一次生产大对象导致的OOM让架构师连夜排查解决

热门文章

  1. 【ECNU OJ 3373】 骑士游戏 最短路径+动态规划
  2. Kafka集群搭建及SASL/SCRAM,ACL权限控制
  3. 看了某些蛊惑人心的招聘广告,实在忍不住想要提醒那些跃跃欲奉献的后生们...
  4. 英语总结系列(二十三):Baby上海一月游
  5. android 键盘隐藏监听,Android监听键盘显示和隐藏
  6. 白天嗜睡很可能是表观遗传变化的结果
  7. 七彩cms云转码_七彩CMS 2019云转码完全开源版本 程序源码带安装教程
  8. 攻防世界-reverse-easyRE1
  9. 时间复杂度以及空间复杂度(大O的渐进表示法)
  10. C++学习continue用法