1.Settings简介
Settings,包括手机各项属性的基本调整和功能的开关,是用户根据个人喜好对手机进行定制的最方便的入口,也是用户在日常生活中使用频率最高的模块之一。因此,它的稳定性、修改定制,对于开发者来说尤为重要。
原生的Android4.0以后的系统中,将设置分为四个部分:
WIRELESS&NETWORKS:SIM卡管理,流量使用情况,飞行模式,VPN,网络共享等。 DEVICE:情景模式,显示,存储,电池,应用程序。 PERSONAL:账户与同步,位置服务,安全,语言和输入法,备份和重置。 SYSTEM:日期和时间,定时开关及,辅助功能,开发人员选项,关于手机。
2.Settings代码结构
Settings其实是以应用Settings.apk的形式存在于手机系统中的。在Google源码中的路径为:
/packages/apps/Settings/src
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">
<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 PreferenceActivity implements 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" >
<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> /①
从上面布局图,很容易看出,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-pane View 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 breadcrumbs if (onIsMultiPane()) { getActionBar().setDisplayHomeAsUpEnabled(false); getActionBar().setHomeButtonEnabled(false); }
5.3.7 Settings.java中的onNewIntent函数
如果不是从历史栈中启动,将重置到一级菜单。如果是分页模式,将调用switchToHeaderLocal()方法,其最终调用的是PA的switchToHeader()方法,前面已经有介绍。
@Override public void onNewIntent(Intent intent) { super.onNewIntent(intent); // If it is not launched from history, then reset to top-level if ((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的形式进行加载。不难发现,这个方法对分页模式不会有任何影响。
@Override public 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 method if (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() { @Override public 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. */ @Override public void onBuildHeaders(List
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">
<header android:id="@+id/wireless_section" android:title="@string/header_category_wireless_networks" />
<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" />
<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" />
<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> implements CompoundButton.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 FR ImageButton button_; View divider_; }
而获取视图的方法为getView(),主要思路很简单,就是判断不同类型的HEADER_TYPE,然后根据不同的HEADER_TYPE做不同的处理,将所需要的title、icon等等信息装入事先定义好的HeaderViewHolder对象。然后用setTag(holder)方法传递给view对象,最后将view返回。(代码略)
最后就是单击等操作的监听器设置。这里只是想强调一点,附加功能都可以通过接口来实现,例如在上个例子里,就实现了CompoundButton.OnCheckedChangeListener接口,而这个是我们在做定制时自己添加的。
@Override public 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 here com.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 }; @Override protected 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
android:uiOptions="splitActionBarWhenNarrow"
而在Settings.java中的控制在onBuildStartFragmentIntent()方法中,代码如下。如果要修改相关功能,只需在其中做增删即可。
@Override public 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 {
@Override public boolean onNavigateUp() { finish(); return true; } @Override protected 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; }
<category android:name="com.android.settings.SHORTCUT" />
如果我们想将Security设置项添加到shortcut列表,我们只需要在androidmanifest.xml中声明Settings$SecuritySettingsActivity部分加上
<category android:name="com.android.settings.SHORTCUT" />
即可。
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的设计结构也由一定关系:
- eyelike@2014-06-11
发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/220621.html原文链接:https://javaforall.net
