赞
踩
Settings,包括手机各项属性的基本调整和功能的开关,是用户根据个人喜好对手机进行定制的最方便的入口,也是用户在日常生活中使用频率最高的模块之一。因此,它的稳定性、修改定制,对于开发者来说尤为重要。
在目前的移动设备中,Settings界面除过主题定制的颜色图标等差别外,存在两种形式:单页形式和分页形式。单页形式为主要形式,而在平板等大屏设备中,则会采用分页形式。
图1 单页(左)和分页(右)
原生的Android4.0以后的系统中,将设置分为四个部分:
WIRELESS&NETWORKS:SIM卡管理,流量使用情况,飞行模式,VPN,网络共享等。
DEVICE:情景模式,显示,存储,电池,应用程序。
PERSONAL:账户与同步,位置服务,安全,语言和输入法,备份和重置。
SYSTEM:日期和时间,定时开关及,辅助功能,开发人员选项,关于手机。
Settings其实是以应用Settings.apk的形式存在于手机系统中的。在Google源码中的路径为:
/packages/apps/Settings/src
具体代码结构图如下:
图2 Settings代码结构图
Settings第一级菜单的显示主要由包com.android.settings下面的Settings.java来负责控制。在该包下面,还包含了其他一些功能设置项的控制类,比如DisplaySettings.java等。其他包从包名基本可以看出,具体负责对应功能模块的控制。各个功能模块封装相对独立,这样,我们只需要进入具体模块,一般就可以完成对其的修改。
既然是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中负责控制的。这个后面再讲。
Settings第一级菜单,是一个ListView,每一个item都是由一个Header构成,整个列表由HeaderAdapter来进行适配。在适配的时候,会取出Header的icon以及title,summary等并放入HeaderViewHolder中,这些就是我们在图一左中看到的外在信息。
然后是对各item的监听,当点击一个item的时候,跳转到具体的模块对应的Fragment中去。
分页模式和单页模式在基本实现上是一致的,区别在于分页模式Header和对应的Fragment将同时显示,因此,在对应模块的Fragment的显示的时候有区别,这个后面再讲。
以上,是Settings实现的基本流程,出现的几个词汇分别是Header、Fragment、HeaderAdapter、HeaderViewHolder,后面代码遇到的时候会讲。这里知道大概流程以及需要这些组件就可以了。
我们首先进入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中,才能发现根本原理。而两个接口,只是为了增加按钮栏的处理和账户更新处理的功能,我们不去深入讲。
在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 an
empty layout or just padding, and PreferenceActivity will put the breadcrumbs in
the 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⑥,为返回、跳过、前进按钮,默认是不显示的。
在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;
}
在PA中,有一个布尔值mSinglePane专门用来标识是否是单页还是分页显示。
private boolean mSinglePane;
它在onCreate()方法中获得具体值,如果HeaderList被隐藏了(意味着此时只会显示具体模块的内容)或者非多页模式,那么mSinglePane即为true,表示单页模式。在PA中,涉及到切换到双页模式的几处关键代码,都和这个值有关。下面接着看其他地方。
boolean hidingHeaders = onIsHidingHeaders();
mSinglePane = hidingHeaders || !onIsMultiPane();
下面的代码仍然在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);
}
}
重新回到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);
}
在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);
}
仍然在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);
}
如果不是从历史栈中启动,将重置到一级菜单。如果是分页模式,将调用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);
}
}
下面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;
}
onCreate()方法在刚才已经有讲过,主要对PA进行进一步的定制,不再多说。
onResume()方法中,进行了多个BroadcastRecevier的注册。其中一个比较重要的地方,就是对【开发者选项】的监听器。在用户版本,默认【开发者选项】是被隐藏的。只有在第一级菜单先进入【关于手机】,然后连续按7次【Build Number】后,才能将其激活,从而在第一级菜单中显示出来。下面的代码就是这个监听器的创建和注册。
mDevelopmentPreferencesListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
invalidateHeaders();
}
};
mDevelopmentPreferences.registerOnSharedPreferenceChangeListener(
mDevelopmentPreferencesListener);
在onPause()方法中,对在onResume()方法中注册的监听器进行unRegisterRecevier()操作。(代码略)
Settings一级菜单中几乎所有(账户相关的由代码中的具体方法控制增删,后面有讲)的Header均是通过onBuildHeaders()方法进行加载的。
/**
* Populate the activity with the top-level headers.
*/
@Override
public 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>
接着上面,在通过loadHeadersFromResource()方法加载settings_headers.xml后,紧跟着调用方法updateHeaderList(headers)对headers做具体的处理。这块的代码比较长,具体所做的事就是对各个具体功能根据需要进行控制,代码逻辑非常清晰,就不再贴出来了。
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 FR581773
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);
}
}
在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;
}
在Settings的第二级菜单,也就是各个具体功能界面,大量应用了ActionBar(界面最上方的条状栏,右侧往往有开关等功能按钮)。而在Wifi、蓝牙等设置界面,有时候会看到界面下方也有按钮,如下图。
图4 ActionBar分离示例图
这是由ActionBar的属性来控制的,对应于XML文件中的属性为:
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;
}
以上通过代码段对主要实现进行了介绍,但是,如果跳出一小块一小块代码,从整体上来看,还是会有一些一时难以琢磨理解的疑问。下面,就将我曾经遇到的一些主要疑问列出来,并做一些解答。
要解决这个问题我们先要清楚为什么会写一个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。
在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。
点击设置界面的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。
这两个函数可以说是相辅相成的。getMetaData会从AndroidManifest.xml中读取Activity的节点的数据;getStartingFragmentClass则从启动Activity的intent中读取数据。这两个函数会对读取到的数据进行整合,getStartingFragmentClass依赖于getMetaData读取到的数据,但是它也可能对数据作出修改(为了兼容性,如对原有manage apps类进行特殊处理)。
创建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。
Hierarchyviewer中显示SubSettings是因为我们在onBuildStartFragmentIntent方法中做了特殊处理(详见问题二)。从shortcut进入Settings时不显示SubSettings是因为没有走这个函数,因此就不会显示为SubSettings了(详见问题六)。
空实现,使得他们虽然被声明,但仍然都将使用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”。
等等。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。