这几天在学习 Android Settings 设置菜单的相关内容,原本打算一篇搞定,谁知入坑后发现 Android 6.0、7.0、8.0 设置菜单的加载方式各有千秋,妈耶,好坑,所以接下来我会从 6.0、7.0、8.0 去分别分析设置界面是怎么加载出来的。预计会分为三篇,本篇是第一篇 6.0 分析,下面请各位跟我的教程一步步走,喂喂喂!!说你呢,小本本拿好,我要开讲啦!!
偷偷告诉你 Android 源码和本篇文章结合起来看效果更好哦。
Android 源码查看地址:https://www.androidos.net.cn/sourcecode 。
另外本系列文章所涉及的所有 Android 源码在此网站上都可以找到,最后来看看 6.0 系统的设置菜单到底长什么样,瞅图:
首先我们来看 Settings 模块中的 AndroidManifest.xml 文件,这里会展示四大组件,默认启动入口Activity等信息,具体路径如下:/packages/apps/Settings/AndroidManifest.xml。
···
<activity android:name="Settings"
android:taskAffinity="com.android.settings"
android:label="@string/settings_label_launcher"
android:launchMode="singleTask">
<intent-filter android:priority="1">
<action android:name="android.settings.SETTINGS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data android:name="com.android.settings.PRIMARY_PROFILE_CONTROLLED"
android:value="true" />
</activity>
//activity-alias可用来设置某个Activity的快捷入口,可以放在桌面上或者通过该别名被其他组件快速调起。
//android:targetActivity为目标Activity.
<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" />
</intent-filter>
</activity-alias>
···
可以看出入口Activity为Settings。
具体路径:/packages/apps/Settings/src/com/android/settings/Settings.java
public class Settings extends SettingsActivity {
public static class BluetoothSettingsActivity extends SettingsActivity { /* empty */ }
public static class WirelessSettingsActivity extends SettingsActivity { /* empty */ }
public static class SimSettingsActivity extends SettingsActivity { /* empty */ }
···
}
可以看出 Settings 继承于 SettingsActivity ,而且其内部都是类的空实现。所以我们看其父类 SettingsActivity。
查看其类会发现,SettingsActivity 便是加载菜单的主 Activity。
它的具体路径为:/packages/apps/Settings/src/com/android/settings/SettingsActivity.java
首先来分析 onCreate() 方法
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
···
//获取AndroidManiFest.xml中mate数据,从Settings(或桌面快捷图标)进入时获取是null
getMetaData();
//布局窗口UI
final Intent intent = getIntent();
if (intent.hasExtra(EXTRA_UI_OPTIONS)) {
getWindow().setUiOptions(intent.getIntExtra(EXTRA_UI_OPTIONS, 0));
}
···
}
getMetaData() 代码如下
private void getMetaData() {
try {
ActivityInfo ai = getPackageManager().getActivityInfo(getComponentName(),
PackageManager.GET_META_DATA);
if (ai == null || ai.metaData == null) return;
//private static final String META_DATA_KEY_FRAGMENT_CLASS =
//"com.android.settings.FRAGMENT_CLASS";
mFragmentClass = ai.metaData.getString(META_DATA_KEY_FRAGMENT_CLASS);
} catch (NameNotFoundException nnfe) {
···
}
}
由此可知,getMetaData() 会在加载对应的设置项时会从 节点下获取到 fragmentclass 的值,即为含有 com.android.settings.FRAGMENT_CLASS 标签的值,进行加载。
接下来看getIntent()代码:
@Override
public Intent getIntent() {
Intent superIntent = super.getIntent();
//获取到要跳转的fragment
String startingFragment = getStartingFragmentClass(superIntent);
if (startingFragment != null) {
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, args);
return modIntent;
}
return superIntent;
}
接着看onCreate()方法
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
···
//获取到要显示的fragment
final String initialFragmentName = intent.getStringExtra(EXTRA_SHOW_FRAGMENT);
//判断是否属于快捷方式
//isShortCutIntent(intent)是获取到配置文件中定义的
//或者判断key为EXTRA_SHOW_FRAGMENT_AS_SHORTCUT中的值是否为true
mIsShortcut = isShortCutIntent(intent) || isLikeShortCutIntent(intent) ||
intent.getBooleanExtra(EXTRA_SHOW_FRAGMENT_AS_SHORTCUT, false);
////获取到组件名
final ComponentName cn = intent.getComponent();
//获取到类名
final String className = cn.getClassName();
//如果类名为Settings的类名,则为正在显示控制面板(设置主页)
mIsShowingDashboard = className.equals(Settings.class.getName());
//获取到所加载的activity是否属于SubSettings
//this instanceof SubSettings:判断activity是否属于SubSettings
//或者获取到EXTRA_SHOW_FRAGMENT_AS_SUBSETTINGS的值,该key会在加载BluetoothSettings时进行赋值
final boolean isSubSettings = this instanceof SubSettings ||
intent.getBooleanExtra(EXTRA_SHOW_FRAGMENT_AS_SUBSETTING, false);
···
//加载布局文件,如果显示的设置主界面,即mIsShowingDashboard为true,所以加载settings_main_dashboard布局
setContentView(mIsShowingDashboard ?
R.layout.settings_main_dashboard : R.layout.settings_main_prefs);
}
接下来看布局文件settings_main_dashboard.xml
具体路径如下:/packages/apps/Settings/res/layout/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"
/>
可以看出是个帧布局,里面什么都没有,ok 接下来继续看 onCreat 代码:
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
//为fragment所存在的栈添加监听事件
getFragmentManager().addOnBackStackChangedListener(this);
//如果当前显示的为主面板,则会进行如下操作
if (mIsShowingDashboard) {
//isLowStorage方法 用于判断我们是否运行在低存储空间
if (!Utils.isLowStorage(this)) {
//如果存储空间足够的话,则进行update,即通过ContentProvider来读取数数据,并添加到一个list列表中,这些list列表中存放的是设置项的相关信息
Index.getInstance(getApplicationContext()).update();
} else {
···
}
}
if (savedState != null) {
···
} else {
if (!mIsShowingDashboard) {
···
} else {
//展示主面板
mDisplayHomeAsUpEnabled = false;
mDisplaySearch = true;
mInitialTitleResId = R.string.dashboard_title;
//切换加载 Fragment
switchToFragment(DashboardSummary.class.getName(), null, false, false,
mInitialTitleResId, mInitialTitle, false);
}
}
}
由于mIsShowingDashboard是true的原因,代码执行了上面的这段代码。即执行了 switchToFragment() 方法
switchToFragment(DashboardSummary.class.getName(), null /* args */, false, false,
mInitialTitleResId, mInitialTitle, false);
switchToFragment 又加载的 DashboardSummary,即切换到 DashboardSummary 这个 Fragment,switchToFragment 代码如下
private Fragment switchToFragment(String fragmentName, Bundle args, boolean validate,
boolean addToBackStack, int titleResId, CharSequence title, boolean withTransition) {
···
Fragment f = Fragment.instantiate(this, fragmentName, args);
FragmentTransaction transaction = getFragmentManager().beginTransaction();
//将fragment加载在控件ID为main_content的位置
transaction.replace(R.id.main_content, f);
···
transaction.commitAllowingStateLoss();
getFragmentManager().executePendingTransactions();
return f;
}
接下来看DashboardSummary.java
具体路径为:/packages/apps/Settings/src/com/android/settings/dashboard/DashboardSummary.java 。其中 onCreateView 方法如下:
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
mLayoutInflater = inflater;
final View rootView = inflater.inflate(R.layout.dashboard, container, false);
mDashboard = (ViewGroup) rootView.findViewById(R.id.dashboard_container);
return rootView;
}
从代码中可以看出来,DashboardSummary这个Fragment加载的dashboard.xml这个布局文件,接下来看下dashboard.xml布局,
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/dashboard"
···
>
<LinearLayout
android:id="@+id/dashboard_container"
···
android:orientation="vertical"
/>
</ScrollView>
最外层是ScrollView,内层是LinearLayout,在上图中的setting界面中,最外层是可滑动的原因就在此,设置选项都是放在LinearLayout中的,并且是动态添加上去的。
接着看DashboardSummary.onResume()方法,发现它最终跑到rebuildUI(context)中,并在这里加载 UI,代码如下:
private void rebuildUI(Context context) {
···
final Resources res = getResources();
//mDashboard为dashboard.xml布局中的LinearLayout的id,此处先移除LinearLayout里的所有控件
mDashboard.removeAllViews();
//获取菜单选项
List<DashboardCategory> categories =
((SettingsActivity) context).getDashboardCategories(true);
···
}
那么接下来看看SettingsActivity.getDashboardCategories函数:
public List<DashboardCategory> getDashboardCategories(boolean forceRefresh) {
if (forceRefresh || mCategories.size() == 0) {
buildDashboardCategories(mCategories);
}
return mCategories;
}
private void buildDashboardCategories(List<DashboardCategory> categories) {
//先清空
categories.clear();
//加载dashboard_categories中的数据,解析xml,将解析好的数据资源存放到了List这个集合中
loadCategoriesFromResource(R.xml.dashboard_categories, categories, this);
//更新categories到界面上,这里可以设置设置中某个categories(比如wifi、蓝牙设置)是否展示。
updateTilesList(categories);
}
到这里可以看出,该方法内部实现是解析dashboard_categories.xml文件,获取到所有的设置项的,所以6.0中Setting的显示内容的条目数量是由资源数量决定的,而这个资源就定义在dashboard_categories.xml中。
它的具体路径为:/packages/apps/Settings/res/xml/dashboard_categories.xml。所以getDashboardCategories会在显示UI之前会去加载资源,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:key="@string/category_key_wireless"
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"
/>
<!-- Bluetooth -->
···
</dashboard-category>
···
</dashboard-categories>
DashboardCategory的代码如下
文件路径为:/packages/apps/Settings/src/com/android/settings/dashboard/DashboardCategory.java
public class DashboardCategory implements Parcelable {
//{@link com.android.settings.dashboard.DashboardCategory#id DashboardCategory.id}
//的标识符为设置时他的默认值为-1. 所有其他值(包括低于-1的值)都有效
public static final long CAT_ID_UNDEFINED = -1;
// 为title设置一个id,当他更新时会和一个新的list关联,默认没有id。
public long id = CAT_ID_UNDEFINED;
//类别title的资源id.Resource ID of title of the category that is shown to the user.
public int titleRes;
//类别title.
public CharSequence title;
//设置额外的 title 时会用到.
public String key;
public int externalIndex = -1;
//类别下面的一系列子设置选项
public List<DashboardTile> tiles = new ArrayList<DashboardTile>();
public DashboardCategory() {
// Empty
}
···
@Override
public void writeToParcel(Parcel dest, int flags) {
···
}
public void readFromParcel(Parcel in) {
···
}
DashboardCategory(Parcel in) {
readFromParcel(in);
}
public static final Creator<DashboardCategory> CREATOR = new Creator<DashboardCategory>() {
public DashboardCategory createFromParcel(Parcel source) {
return new DashboardCategory(source);
}
public DashboardCategory[] newArray(int size) {
return new DashboardCategory[size];
}
};
}
其中dashboard_categories.xml的dashboard-category节点对应的是DashboardCategory的对象,dashboard-tile节点对应的是DashboardTile的对象,而DashboardCategory对象内部有List tiles的集合对象,所解析出来的DashboardTile都有DashboardCategory保管,而每个DashboardCategory对象又由categories来保管,就这样数据资源就得到了.
所以如果想要添加一个设置项,只需要在dashboard_categories.xml文件中添加一个控件,如果要删除一个设置项可以在 SettingsActivity中 的 updateTilesList 方法中将 removetile 设置为 true,然后移除。
接着来看rebuildUI的代码:
private void rebuildUI(Context context) {
···
final int count = categories.size();
for (int n = 0; n < count; n++) {
//每一个大类里设置项的数量
DashboardCategory category = categories.get(n);
//每一个设置项为自定义的dashboard-tile,其布局文件为dashboard_category.xml
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);
//dashboard_categories.xml的dashboard-category节点最终对应了DashboardTileView的对象
DashboardTileView tileView = new DashboardTileView(context);
updateTileView(context, res, tile, tileView.getImageView(),
tileView.getTitleTextView(), tileView.getStatusTextView());
tileView.setTile(tile);
categoryContent.addView(tileView);
}
// 添加一个类别
mDashboard.addView(categoryView);
}
}
可以看出rebuildUI接下来的代码便是循环遍历添加数据显示的过程。
其中 dashboard_category.xml (注意不是 dashboard_categories.xml )代码如下,代码路径为 :/packages/apps/Settings/res/layout/dashboard_category.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/category"
android:orientation="vertical"
android:background="@color/card_background"
···
>
<TextView android:id="@+id/category_title"
···
/>
<com.android.settings.dashboard.DashboardContainerView
android:id="@+id/category_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</LinearLayout>
所以如果想要修改所有设置项的布局,可以修改 dashboard_category.xml 文件
那么 DashboardTileView 和 dashboard-category 到底是如何跳转到具体的某个Fragment呢?来看看 DashboardTileView,代码路径为:/packages/apps/Settings/src/com/android/settings/dashboard/DashboardTileView.java
@Override
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) {
int numUserHandles = mTile.userHandle.size();
if (numUserHandles > 1) {
ProfileSelectDialog.show(((Activity) getContext()).getFragmentManager(), mTile);
} else if (numUserHandles == 1) {
getContext().startActivityAsUser(mTile.intent, mTile.userHandle.get(0));
} else {
getContext().startActivity(mTile.intent);
}
}
}
所以当点击 DashboardTileView 的某个对象时就会跳转至一个具体的 Fragment,而这个 Fragment 已经在 dashboard-category 节点中声明。
跟踪源码到最后,我们会发现 6.0 的设置菜单加载是通过读取 xml 文件完成的,整个过程浑然天成,不禁让人感叹 google 架构的厉害。。
最后,自己的公众号已经同步更新!!二维码如下:
有兴趣的可以关注下,此号会不定期分享一些技术文章,但不止于技术哦~~