Android5.1源码分析系列Settings源码分析
Posted EndLessTwoWay
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android5.1源码分析系列Settings源码分析相关的知识,希望对你有一定的参考价值。
最近在看android源码,现在想做一个系列,专门对源码进行简单直接的分析。本人道行尚浅,希望大家能够进行批评,本人感激不尽。
Android5.1 Settints源码分析
1 概要
本文分析的文件目录:
/packages/apps/Settings/AndroidManifest.xml
/packages/apps/settings/src/com/android/settings/Settings.java
/packages/apps/settings/src/com/android/settings/SettingsActivity.java
/packages/apps/settings/res/layout/Settings_main_prefs.xml
/packages/apps/settings/src/com/android/settings/dashboard/DashboardSummary.java
/packages/apps/settings/res/layout/Dashboard.xml
/packages/apps/settings/res/xml/Dashboard_categories.xml
/packages/apps/settings/src/com/android/settings/dashboard/DashboardTileView.java
本文将从这几个文件进行简要的分析,对Settings的大致过程进行简要的分析。
2 分析过程
2.1 AndroidManifest.xml
在分析源码应用的过程中首先看它的清单文件。AndroidManifest.xml 是每个android程序中必须的文件。它位于整个项目的根目录,描述了package中暴露的组件(activities, services, 等等),他们各自的实现类,各种能被处理的数据和启动位置。 除了能声明程序中的Activities, ContentProviders, Services, 和Intent Receivers,还能指定permissions和instrumentation(安全控制和测试)。在Settings的清单文件中可以看到:
<activity-alias android:name="Settings"
android:taskAffinity="com.android.settings"
android:label="@string/settings_label_launcher"
android:launchMode="singleTask"
android:targetActivity="Settings">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<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-alias>
可以看出其启动类是Settings。ok,现在来看Settings.java。
2.2 Settings.java
public class Settings extends SettingsActivity {
/*
* Settings subclasses for launching independently.
*/
public static class BluetoothSettingsActivity extends SettingsActivity { /* empty */ }
public static class WirelessSettingsActivity extends SettingsActivity { /* empty */ }
public static class SimSettingsActivity extends SettingsActivity { /* empty */ }
public static class TetherSettingsActivity extends SettingsActivity { /* empty */ }
public static class VpnSettingsActivity extends SettingsActivity { /* empty */ }
public static class DateTimeSettingsActivity extends SettingsActivity { /* empty */ }
public static class StorageSettingsActivity extends SettingsActivity { /* empty */ }
public static class WifiSettingsActivity extends SettingsActivity { /* empty */ }
public static class WifiP2pSettingsActivity extends SettingsActivity { /* empty */ }
public static class InputMethodAndLanguageSettingsActivity extends SettingsActivity { /* empty */ }
public static class KeyboardLayoutPickerActivity extends SettingsActivity { /* empty */ }
public static class InputMethodAndSubtypeEnablerActivity extends SettingsActivity { /* empty */ }
public static class VoiceInputSettingsActivity extends SettingsActivity { /* empty */ }
public static class SpellCheckersSettingsActivity extends SettingsActivity { /* empty */ }
...
}
打开Settings这个类我们并没有找到一些方法,只有一些静态内部类。这些类能够直接被调用而启动。用来直接进行设置相关功能的启动类。ok,既然其直接继承SettingsActivity 类,那我们继续跳转到SettingsActivity .java。
2.3 SettingsActivity .java
public class SettingsActivity extends Activity
implements PreferenceManager.OnPreferenceTreeClickListener,
PreferenceFragment.OnPreferenceStartFragmentCallback,
ButtonBarHandler, FragmentManager.OnBackStackChangedListener,
SearchView.OnQueryTextListener, SearchView.OnCloseListener,
MenuItem.OnActionExpandListener
上述代码可以看到SettingsActivity继承了Activity类,之后我们进入onCreate中,看看其中到底做了什么。onCreate中的方法比较多,下面捡重点的进行分析。
// Should happen before any call to getIntent()
getMetaData();
首先看getMetaData()这个方法;
private void getMetaData() {
try {
ActivityInfo ai = getPackageManager().getActivityInfo(getComponentName(),
PackageManager.GET_META_DATA);
if (ai == null || ai.metaData == null) return;
mFragmentClass = ai.metaData.getString(META_DATA_KEY_FRAGMENT_CLASS);
} catch (NameNotFoundException nnfe) {
// No recovery
Log.d(LOG_TAG, "Cannot get Metadata for: " + getComponentName().toString());
}
}
由上述代码可以看出getMetaData()这个方法是为了获取mFragmentClass ,获取到就会启动相应的Activity。如果是直接启动Settings模块,则直接return,不会再去获取mFragmentClass 。ok,继续往下看:
final ComponentName cn = intent.getComponent();
final String className = cn.getClassName();
mIsShowingDashboard = className.equals(Settings.class.getName());
这段代码可以看出,设置了一个标志,当前是不是我们的主界面,因为Settings继承Activity所以其值为true.
接着:
// This is a "Sub Settings" when:
// - this is a real SubSettings
// - or :settings:show_fragment_as_subsetting is passed to the Intent
final boolean isSubSettings = className.equals(SubSettings.class.getName()) ||
intent.getBooleanExtra(EXTRA_SHOW_FRAGMENT_AS_SUBSETTING, false);
很明显这是一个二级设置,并且明显的判断出其值为false,以下有一个判断:
// If this is a sub settings, then apply the SubSettings Theme for the ActionBar content insets
if (isSubSettings) {
// Check also that we are not a Theme Dialog as we don‘t want to override them
final int themeResId = getThemeResId();
if (themeResId != R.style.Theme_DialogWhenLarge &&
themeResId != R.style.Theme_SubSettingsDialogWhenLarge) {
setTheme(R.style.Theme_SubSettings);
}
}
既然是我们设置的主界面,所以这一步省略,暂且不讲。
好的,我们接着看:
setContentView(mIsShowingDashboard ?
R.layout.settings_main_dashboard : R.layout.settings_main_prefs);
这一步很容易也很关键,加载一个xml,因为判断的mIsShowingDashboard为true,所以取settings_main_dashboard 这个xml,ok,我们就来看这个:
settings_main_dashboard.xml:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_content"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:background="@color/dashboard_background_color"
/>
一看就明白,这个就是一个容器,用来为fragment布局,ok,这没啥看的,直接往下走:
if (savedState != null) {
// We are restarting from a previous saved state; used that to initialize, instead
// of starting fresh.
mSearchMenuItemExpanded = savedState.getBoolean(SAVE_KEY_SEARCH_MENU_EXPANDED);
mSearchQuery = savedState.getString(SAVE_KEY_SEARCH_QUERY);
setTitleFromIntent(intent);
ArrayList<DashboardCategory> categories =
savedState.getParcelableArrayList(SAVE_KEY_CATEGORIES);
if (categories != null) {
mCategories.clear();
mCategories.addAll(categories);
setTitleFromBackStack();
}
mDisplayHomeAsUpEnabled = savedState.getBoolean(SAVE_KEY_SHOW_HOME_AS_UP);
mDisplaySearch = savedState.getBoolean(SAVE_KEY_SHOW_SEARCH);
mHomeActivitiesCount = savedState.getInt(SAVE_KEY_HOME_ACTIVITIES_COUNT,
1 /* one home activity by default */);
} else {
if (!mIsShowingDashboard) {
// Search is shown we are launched thru a Settings "shortcut". UP will be shown
// only if it is a sub settings
if (mIsShortcut) {
mDisplayHomeAsUpEnabled = isSubSettings;
mDisplaySearch = false;
} else if (isSubSettings) {
mDisplayHomeAsUpEnabled = true;
mDisplaySearch = true;
} else {
mDisplayHomeAsUpEnabled = false;
mDisplaySearch = false;
}
setTitleFromIntent(intent);
Bundle initialArguments = intent.getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
switchToFragment(initialFragmentName, initialArguments, true, false,
mInitialTitleResId, mInitialTitle, false);
} else {
// No UP affordance if we are displaying the main Dashboard
mDisplayHomeAsUpEnabled = false;
// Show Search affordance
mDisplaySearch = true;
mInitialTitleResId = R.string.dashboard_title;
switchToFragment(DashboardSummary.class.getName(), null, false, false,
mInitialTitleResId, mInitialTitle, false);
}
}
这段代码比较长,但是因为我们是第一次进行创建所以savedState ==null,之后mIsShowingDashboard又为true,所以代码直接看:
else {
// No UP affordance if we are displaying the main Dashboard
mDisplayHomeAsUpEnabled = false;
// Show Search affordance
mDisplaySearch = true;
mInitialTitleResId = R.string.dashboard_title;
switchToFragment(DashboardSummary.class.getName(), null, false, false,
mInitialTitleResId, mInitialTitle, false);
}
在这里我们注意switchToFragment()这个方法,很明显的看出这个方法要切换到DashboardSummary这个Fragment,ok,那么我们直接追随到DashboardSummary这个Fragment。
2.4 DashboardSummary.java
直接打开这个fragment,并且定位到onCreateView方法:
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mLayoutInflater = inflater;
mExt = UtilsExt.getMiscPlugin(this.getActivity());
final View rootView = inflater.inflate(R.layout.dashboard, container, false);
mDashboard = (ViewGroup) rootView.findViewById(R.id.dashboard_container);
return rootView;
}
这个fragment填充了dashboard的布局,ok,继续点进去看这个布局:
dashboard.xml:
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/dashboard"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbarStyle="insideOverlay"
android:clipToPadding="false">
<LinearLayout
android:id="@+id/dashboard_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
android:paddingStart="@dimen/dashboard_padding_start"
android:paddingEnd="@dimen/dashboard_padding_end"
android:paddingTop="@dimen/dashboard_padding_top"
android:paddingBottom="@dimen/dashboard_padding_bottom"
android:orientation="vertical"
/>
</ScrollView>
这又是一个空布局,作为一个容器使用,可以看出Settings的选项视图应该就是显示在dashboard_container中的。
ok,继续往下走接着它会跑到onResume()方法:
public void onResume() {
super.onResume();
sendRebuildUI();
final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
filter.addDataScheme("package");
getActivity().registerReceiver(mHomePackageReceiver, filter);
}
从中可以看到过滤到了一些添加,移除应用之类的,但是我们要注意sendRebuildUI()这个方法:
private void sendRebuildUI() {
if (!mHandler.hasMessages(MSG_REBUILD_UI)) {
mHandler.sendEmptyMessage(MSG_REBUILD_UI);
}
其发送了一个空消息,主要看handle的执行接收执行和处理:
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_REBUILD_UI: {
final Context context = getActivity();
rebuildUI(context);
} break;
}
当接收到MSG_REBUILD_UI时,执行rebuildUI()这个里面的东西比较多,主要的作用就是重塑UI。这个方法稍后介绍,ok,看关键一句:
rebuildUI():
List<DashboardCategory> categories =
((SettingsActivity) context).getDashboardCategories(true);
这句为一个列表保存数据,ok,直接进入getDashboardCategories();
这时惊喜出现了,你会发现又返回到了SettingsActivity.java;ok,接着直接看:
2.5 SettingsActivity.java
看这个getDashboardCategories:
public List<DashboardCategory> getDashboardCategories(boolean forceRefresh) {
if (forceRefresh || mCategories.size() == 0) {
buildDashboardCategories(mCategories);
}
return mCategories;
}
ok,直接调用的是buildDashboardCategories()方法;直接进入:
buildDashboardCategories():
private void buildDashboardCategories(List<DashboardCategory> categories) {
categories.clear();
loadCategoriesFromResource(R.xml.dashboard_categories, categories);
updateTilesList(categories);
}
好的,现在我们发现了这个非常重要的加载函数loadCategoriesFromResource(),可以看出其加载了dashboard_categories.xml布局。ok,直接点进去看:
dashboard_categories.xml:
<dashboard-categories
xmlns:android="http://schemas.android.com/apk/res/android">
<!-- WIRELESS and NETWORKS -->
<dashboard-category
android:id="@+id/wireless_section"
android:title="@string/header_category_wireless_networks" >
<!-- Wifi -->
<dashboard-tile
android:id="@+id/wifi_settings"
android:title="@string/wifi_settings_title"
android:fragment="com.android.settings.wifi.WifiSettings"
android:icon="@drawable/ic_settings_wireless"
/>
<!--HetComm-->
<dashboard-tile
android:id="@+id/hetcomm_settings"
android:icon="@drawable/ic_settings_hetcomm"
android:title="@string/hetcom_setting_title">
<intent android:action="com.android.settings.HETCOMM_SETTINGS" />
</dashboard-tile>
<!-- Bluetooth -->
<dashboard-tile
android:id="@+id/bluetooth_settings"
android:title="@string/bluetooth_settings_title"
android:fragment="com.android.settings.bluetooth.BluetoothSettings"
android:icon="@drawable/ic_settings_bluetooth2"
/>
<!--^_^ test_zxr ^_^ -->
<dashboard-tile
android:id="@+id/test_zxr"
android:title="@string/test_zxr"
android:fragment="com.android.settings.DeviceInfoSettings"
android:icon="@drawable/ic_settings_about"
/>
</dashboard-category>
...
这个就是我们整体的设置布局,加载完这个xml,获取到里面的DashboardCategory。然后再把这些item加到容器中去:
ok,现在就来看rebuildUI():
for (int n = 0; n < count; n++) {
DashboardCategory category = categories.get(n);
View categoryView = mLayoutInflater.inflate(R.layout.dashboard_category, mDashboard,
false);
TextView categoryLabel = (TextView) categoryView.findViewById(R.id.category_title);
categoryLabel.setText(category.getTitle(res));
ViewGroup categoryContent =
(ViewGroup) categoryView.findViewById(R.id.category_content);
final int tilesCount = category.getTilesCount();
for (int i = 0; i < tilesCount; i++) {
DashboardTile tile = category.getTile(i);
DashboardTileView tileView = new DashboardTileView(context);
updateTileView(context, res, tile, tileView.getImageView(),
tileView.getTitleTextView(), tileView.getStatusTextView());
tileView.setTile(tile);
if(tile != null && tile.extras != null && tile.extras.containsKey(CUSTOMIZE_ITEM_INDEX)){
int index = tile.extras.getInt(CUSTOMIZE_ITEM_INDEX, -1);
categoryContent.addView(tileView, index);
} else {
categoryContent.addView(tileView);
}
}
// Add the category
mDashboard.addView(categoryView);
这段代码逻辑很简单,遍历categories这个列表来获取DashboardCategory对象,将所有DashboardCategory对象和DashboardCategory对象中的DashboardTile对象转化为视图对象并添加到主视图对象mDashboard中。ok。加载完view,settings的布局就ok了,接下来就是其实现了。
2.6 DashboardTileView.java
现在来看在View中的实现:打开DashboardTileView.java,可以看到其构造:
public DashboardTileView(Context context, AttributeSet attrs) {
super(context, attrs);
final View view = LayoutInflater.from(context).inflate(R.layout.dashboard_tile, this);
mImageView = (ImageView) view.findViewById(R.id.icon);
mTitleTextView = (TextView) view.findViewById(R.id.title);
mStatusTextView = (TextView) view.findViewById(R.id.status);
mDivider = view.findViewById(R.id.tile_divider);
setOnClickListener(this);
setBackgroundResource(R.drawable.dashboard_tile_background);
setFocusable(true);
}
可以看到其定义了一些控件用来找到我们的图标,标题等等的定义,之后定义一些函数用来返回控件,好了,现在直接看onClick()方法:
public void onClick(View v) {
if (mTile.fragment != null) {
Utils.startWithFragment(getContext(), mTile.fragment, mTile.fragmentArguments, null, 0,
mTile.titleRes, mTile.getTitle(getResources()));
} else if (mTile.intent != null) {
getContext().startActivity(mTile.intent);
}
}
可以看到其点击事件中启用了一个方法,从表面看来应该是启动一个Fragment,ok,那么我么继续跟进这个方法:
/**
* Start a new instance of the activity, showing only the given fragment.
* When launched in this mode, the given preference fragment will be instantiated and fill the
* entire activity.
*
* @param context The context.
* @param fragmentName The name of the fragment to display.
* @param args Optional arguments to supply to the fragment.
* @param resultTo Option fragment that should receive the result of the activity launch.
* @param resultRequestCode If resultTo is non-null, this is the request code in which
* to report the result.
* @param titleResId resource id for the String to display for the title of this set
* of preferences.
* @param title String to display for the title of this set of preferences.
*/
public static void startWithFragment(Context context, String fragmentName, Bundle args,
Fragment resultTo, int resultRequestCode, int titleResId,
CharSequence title) {
startWithFragment(context, fragmentName, args, resultTo, resultRequestCode,
null /* titleResPackageName */, titleResId, title, false /* not a shortcut */);
}
在这里我特意贴了注释,从源码的注释可以看出其实直接启动一个Activity,好的,我们现在继续跟进:
public static void startWithFragment(Context context, String fragmentName, Bundle args,
Fragment resultTo, int resultRequestCode, String titleResPackageName, int titleResId,
CharSequence title, boolean isShortcut) {
Intent intent = onBuildStartFragmentIntent(context, fragmentName, args, titleResPackageName,
titleResId, title, isShortcut);
if (resultTo == null) {
context.startActivity(intent);
} else {
resultTo.startActivityForResult(intent, resultRequestCode);
}
}
很明显可以看出intent链接到onBuildStartFragmentIntent,现在跳转到onBuildStartFragmentIntent:
public static Intent onBuildStartFragmentIntent(Context context, String fragmentName,
Bundle args, String titleResPackageName, int titleResId, CharSequence title,
boolean isShortcut) {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.setClass(context, SubSettings.class);
intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT, fragmentName);
intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, args);
intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE_RES_PACKAGE_NAME,
titleResPackageName);
intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE_RESID, titleResId);
intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE, title);
intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_AS_SHORTCUT, isShortcut);
return intent;
}
看到它的启动信息,直接链接到SubSettings这个类,并且附带这个fragment的fragmentName,args,titleResPackageName,等信息直接跳转。ok,既然直接启动SubSettings,那么我们就看这个类:
public class SubSettings extends SettingsActivity
可以看到这个类继承了SettingsActivity,所以我们又得回到SettingsActivity的onCreate()方法,^_^。
2.7 SettingsActivity.java
OK,既然有跳到了SettingsActivity,那么我们就继续跟进^_^:
又到了这个标志位,是不是觉得非常熟悉呢:
mIsShowingDashboard = className.equals(Settings.class.getName());
哈哈,对了前面因为我们是第一次创建所以这个值返回的是true,而现在呢?我们的className已经改变了,所以返回false,ok,接着看:
setContentView(mIsShowingDashboard ? R.layout.settings_main_dashboard : R.layout.settings_main_prefs);
现在加载的却是settings_main_prefs这个界面。
final String initialFragmentName = intent.getStringExtra(EXTRA_SHOW_FRAGMENT);
获取传递过来的FRAGMENTname,接着:
switchToFragment(initialFragmentName, initialArguments, true, false,mInitialTitleResId, mInitialTitle, false);
又开始了我们的Fragment切换^_^。至此应该一轮结束。
3.小结
本文是我自己分析android源码的第一次完整的对一个模块的分析,也参考了网上的一些文章,还有自己的一些看法,在分析的过程中发现android源码分析却是非常不容易,跳过来跳过去,若是不能及时的跳出来,很容易陷进去,在分析的过程中一定要请教交流,不然很多时候却是很费解。这是我的源码分析的第一步,后续会持续跟新其他完整的模块,非常希望感兴趣的同学能够多多交流批评指正。
以上是关于Android5.1源码分析系列Settings源码分析的主要内容,如果未能解决你的问题,请参考以下文章