写在前面

最近客户有个需求,要求增加操作Setting列表配置项的功能,是不是一脸懵,没关系,一图胜千言,接下来就上图。诺,就是这么个意思。



 

原来的列表配置项

 

 

增加了单个配置项

 



 

增加了多个配置项

 

老铁们看懂了么,就是在原有的列表项中增加客户想要的项,来我给你们分析下,Setting是系统级APP,到时候直接打包进Room里这不用我多说吧。重点来了,如果你把这个功能写死了,那么恭喜你,准备迎接一波又一波的系统打包发更新版本吧。客户今天加个列表项,明天减个列表项啥的,不很正常么(虽然你的内心是一万只奔腾在草原),但还是得乖乖去打升级包。

那么,今天老司机就来带你解决这一烦恼,坐稳了,要发车了。

进入正题

先说下我的思路,广播是个好东西(系统App和其它App直接交换数据或者执行命令什么的,大有用处),快拿小本本记下来,假设要增加单条配置项,广播无疑是首选项,增加和删除都很方便,如果要增加多条配置项,广播就不再适用了,当然你也可以构造复杂的数据集合,通过广播来传递解析也是可以的。

多条配置项,我们将采用xml文件配置的方式(别问我怎么想到的,看了Setting的源码你就知道了),和系统设置一样的节点名称,方便解析和理解。

 

先献上我的分析过程图(精华都在图里了)

之前说过Hierarchy View是个好扳手,这一次我们依旧使用它来定位Setting的布局文件,搜索过程图我就不贴了,最终根据id我们定位到

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"
/>

通过查找布局文件的引用,接下来我们跟到了SettingsActivity中

源码位置 packages\apps\Settings\src\com\android\settings\SettingsActivity.java

 @Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
...
final ComponentName cn = intent.getComponent();
final String className = cn.getClassName();
//目前显示的就是设置的主界面,mIsShowingDashboard=true
mIsShowingDashboard = className.equals(Settings.class.getName());
...
//此处加载的就是刚刚的settings_main_dashboard布局
setContentView(mIsShowingDashboard ?
R.layout.settings_main_dashboard : R.layout.settings_main_prefs);
//settings_main_dashboard布局中的mContent需要被替换填充
mContent = (ViewGroup) findViewById(R.id.main_content);
...
if (!mIsShowingDashboard) {
mDisplaySearch = false;
// UP will be shown only if it is a sub settings
if (mIsShortcut) {
mDisplayHomeAsUpEnabled = isSubSettings;
} else if (isSubSettings) {
mDisplayHomeAsUpEnabled = true;
} else {
mDisplayHomeAsUpEnabled = false;
}
setTitleFromIntent(intent); Bundle initialArguments = intent.getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
switchToFragment(initialFragmentName, initialArguments, true, false,
mInitialTitleResId, mInitialTitle, false);
} else {//进入这,通过switchToFragment方法替换
// 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方法中

private Fragment switchToFragment(String fragmentName, Bundle args, boolean validate,
boolean addToBackStack, int titleResId, CharSequence title, boolean withTransition) {
if (validate && !isValidFragment(fragmentName)) {
throw new IllegalArgumentException("Invalid fragment for this activity: "
+ fragmentName);
}
Fragment f = Fragment.instantiate(this, fragmentName, args);
FragmentTransaction transaction = getFragmentManager().beginTransaction();
//通过DashboardSummary来替换id为main_content的FrameLayout
transaction.replace(R.id.main_content, f);
if (withTransition) {
TransitionManager.beginDelayedTransition(mContent);
}
if (addToBackStack) {
transaction.addToBackStack(SettingsActivity.BACK_STACK_PREFS);
}
if (titleResId > 0) {
transaction.setBreadCrumbTitle(titleResId);
} else if (title != null) {
transaction.setBreadCrumbTitle(title);
}
transaction.commitAllowingStateLoss();
getFragmentManager().executePendingTransactions();
return f;
}

到这里我们找到了Setting主界面显示的真正内容是DashboardSummary这个类,跳到这个类,让我们来一探究竟

源码位置 packages\apps\Settings\src\com\android\settings\dashboard\DashboardSummary.java

首先看到onResume()方法中

@Override
public void onResume() {
super.onResume();
//方法名和UI相关,应该是我们要找的
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()方法通过Handler发送一个MSG_REBUILD_UI消息,找到消息接收地方

private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_REBUILD_UI: {
final Context context = getActivity();
rebuildUI(context);
} break;
}
}
}; private HomePackageReceiver mHomePackageReceiver = new HomePackageReceiver();
private class HomePackageReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
rebuildUI(context);
}
}

不难发现,最终都调用了同一个方法rebuildUI(),肯定是这货无疑了。

private void rebuildUI(Context context) {
if (!isAdded()) {
Log.w(LOG_TAG, "Cannot build the DashboardSummary UI yet as the Fragment is not added");
return;
} long start = System.currentTimeMillis();
final Resources res = getResources();
//添加之前先移除所有原来的View
mDashboard.removeAllViews();
//通过SettingsActivity中的getDashboardCategories方法获取所有配置项
List<DashboardCategory> categories =
((SettingsActivity) context).getDashboardCategories(true); final int count = categories.size();
Log.i(LOG_TAG, "new categories=" + count);
//遍历配置项列表集合,逐个添加(这块可结合上面的分析图看比较容易理解一些)
for (int n = 0; n < count; n++) { DashboardCategory category = categories.get(n);
//categoryView整个大类,例如无线和网络、设备
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();
//大类中添加对应的小类,例如 WLAN、蓝牙、SIM卡
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); categoryContent.addView(tileView);
} // Add the category
mDashboard.addView(categoryView);
}
long delta = System.currentTimeMillis() - start;
Log.d(LOG_TAG, "rebuildUI took: " + delta + " ms");
}

我们再回到SettingsActivity中的getDashboardCategories方法

public List<DashboardCategory> getDashboardCategories(boolean forceRefresh) {
if (forceRefresh || mCategories.size() == 0) {
buildDashboardCategories(mCategories);
}
return mCategories;
}

实际调用buildDashboardCategories()方法,再来

/**
* Called when the activity needs its list of categories/tiles built.
*
* @param categories The list in which to place the tiles categories.
*/
private void buildDashboardCategories(List<DashboardCategory> categories) {
categories.clear();
//通过解析dashboard_categories.xml文件,添加到categories中
loadCategoriesFromResource(R.xml.dashboard_categories, categories, this);
updateTilesList(categories);
}

dashboard_categories.xml文件内容如下

源码位置 packages\apps\Settings\res\xml\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-tile
android:id="@+id/bluetooth_settings"
android:title="@string/bluetooth_settings_title"
android:fragment="com.android.settings.bluetooth.BluetoothSettings"
android:icon="@drawable/ic_settings_bluetooth"
/>
.....
</dashboard-category> <!-- DEVICE -->
<dashboard-category
android:id="@+id/device_section"
android:key="@string/category_key_device"
android:title="@string/header_category_device" > <!-- Home -->
<dashboard-tile
android:id="@+id/home_settings"
android:title="@string/home_settings"
android:fragment="com.android.settings.HomeSettings"
android:icon="@drawable/ic_settings_home"
/>
....
</dashboard-category> ...

看完这个xml文件是不是有一种恍然大明白的感觉,那就对了,这就对应了Setting的主界面,看到注释Wifi和Bluetooth等,注意观察上面的xml,dashboard-category节点为一个大类,dashboard-tile节点为里面的一个小类,对应的属性id不用多说,title即显示的标题文字,fragment对应点击时跳转的页面,icon为标题文字左边对应的图标。

回到文章开头的需求,如果只是简单的增加项或者删除项,只需在dashboard_categories.xml中增加对应的节点或者删除对应的节点,然后你就可以编译查看效果,舒舒服服的下班了。

⑧特以后你可就不舒服了,于是我灵机一动,既然系统原来是通过解析dashboard_categories.xml所有配置项,每次在onResume()中重新addView(),那么我们可以在这里做手脚(快,夸我机智),在解析系统dashboard_categories.xml得到的List添加我们想添加的配置项。

我们可以仿照谷歌工程师的做法,同样解析客户提供的xml来动态增加配置项。既然是解析,就得固定模板,肯定是我们需要给客户提供xml模板,他们来修改就好啦。

接下来,我们来看下系统是如何解析dashboard_categories.xml的,回到loadCategoriesFromResource()

public static void loadCategoriesFromResource(int resid, List<DashboardCategory> target,
Context context) {
XmlResourceParser parser = null;
try {
parser = context.getResources().getXml(resid);
... while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
continue;
} nodeName = parser.getName();
//大类的配置节点
if ("dashboard-category".equals(nodeName)) {
//大类对应的bean
DashboardCategory category = new DashboardCategory(); TypedArray sa = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.PreferenceHeader);
//id赋值
category.id = sa.getResourceId(
com.android.internal.R.styleable.PreferenceHeader_id,
(int)DashboardCategory.CAT_ID_UNDEFINED); TypedValue tv = sa.peekValue(
com.android.internal.R.styleable.PreferenceHeader_title);
if (tv != null && tv.type == TypedValue.TYPE_STRING) {
if (tv.resourceId != 0) {
category.titleRes = tv.resourceId;
} else {
//title赋值
category.title = tv.string;
}
}
sa.recycle();
sa = context.obtainStyledAttributes(attrs,
com.android.internal.R.styleable.Preference);
tv = sa.peekValue(
com.android.internal.R.styleable.Preference_key);
if (tv != null && tv.type == TypedValue.TYPE_STRING) {
if (tv.resourceId != 0) {
category.key = context.getString(tv.resourceId);
} else {
category.key = tv.string.toString();
}
}
sa.recycle(); final int innerDepth = parser.getDepth();
while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth)) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
continue;
} String innerNodeName = parser.getName();
//大类中对应的小类
if (innerNodeName.equals("dashboard-tile")) {
//小类对应的bean
DashboardTile tile = new DashboardTile(); sa = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.PreferenceHeader);
tile.id = sa.getResourceId(
com.android.internal.R.styleable.PreferenceHeader_id,
(int)TILE_ID_UNDEFINED);
tv = sa.peekValue(
com.android.internal.R.styleable.PreferenceHeader_title);
if (tv != null && tv.type == TypedValue.TYPE_STRING) {
if (tv.resourceId != 0) {
tile.titleRes = tv.resourceId;
} else {
//小类的title
tile.title = tv.string;
}
}
tv = sa.peekValue(
com.android.internal.R.styleable.PreferenceHeader_summary);
if (tv != null && tv.type == TypedValue.TYPE_STRING) {
if (tv.resourceId != 0) {
tile.summaryRes = tv.resourceId;
} else {
tile.summary = tv.string;
}
}
//小类的icon
tile.iconRes = sa.getResourceId(
com.android.internal.R.styleable.PreferenceHeader_icon, 0);
//小类的fragment
tile.fragment = sa.getString(
com.android.internal.R.styleable.PreferenceHeader_fragment);
sa.recycle(); ... // Show the SIM Cards setting if there are more than 2 SIMs installed.
if(tile.id != R.id.sim_settings || Utils.showSimCardTile(context)){
category.addTile(tile);
...
} target.add(category);
} else {
XmlUtils.skipCurrentTag(parser);
}
}
...
}

从系统的解析方法中,我提取了重要的有用的节点进而简化了解析方法,代码如下(在DashboardSummary.java中新增)

XmlPullParser xmlPullParser;
XmlPullParserFactory xmlPullParserFactory;
FileInputStream fileInputStream;
private void loadCategoriesFromXml(List<DashboardCategory> categories){
String xmlPath = Environment.getExternalStorageDirectory().getAbsolutePath()
+"/Android/dashboard.xml";
File file = new File(xmlPath);
if (file.exists() && file.canRead()){
try {
xmlPullParserFactory = XmlPullParserFactory.newInstance();
xmlPullParserFactory.setNamespaceAware(true);
xmlPullParser = xmlPullParserFactory.newPullParser();
fileInputStream = new FileInputStream(xmlPath);
xmlPullParser.setInput(fileInputStream, "utf-8"); int mEventType = xmlPullParser.getEventType();
DashboardCategory category = null;
DashboardTile tile = null;
while (mEventType != XmlPullParser.END_DOCUMENT){
switch (mEventType) {
case XmlPullParser.START_DOCUMENT:
break;
case XmlPullParser.START_TAG:
String name = xmlPullParser.getName();
if (name.equals("dashboard-category")){
category = new DashboardCategory();
category.title = xmlPullParser.getAttributeValue(null, "title");
}else if (name.equals("dashboard-tile")){
tile = new DashboardTile();
}else if (name.equals("title")){
tile.title = xmlPullParser.nextText();
}else if (name.equals("action")){
tile.intent = new Intent(xmlPullParser.nextText());
}
break;
case XmlPullParser.END_TAG:
String nameP = xmlPullParser.getName();
if (nameP.equals("dashboard-tile")){
tile.iconRes = R.drawable.ic_settings_meituan;
category.addTile(tile);
}else if (nameP.equals("dashboard-category")){
categories.add(category);
}
break;
}
mEventType = xmlPullParser.next();
}
} catch (Exception e) {
throw new RuntimeException("Error parsing categories", e);
}finally{
if (fileInputStream != null){
try{
fileInputStream.close();
xmlPullParserFactory = null;
xmlPullParser = null;
}catch(Exception e){ }
}
}
}else {
Log.i(LOG_TAG, ".dashboard.xml don't exists");
}
}

对应的xml模板如下,到时候需要将dashboard.xml文件放置在SD卡的Android目录下

<?xml version="1.0" encoding="utf-8"?>
<dashboard-categories>
<dashboard-category title="title one">
<dashboard-tile>
<title>红</title>
<action>com.android.settings.SCHEDULE_POWER_ON_OFF_SETTING</action>
</dashboard-tile>
<dashboard-tile>
<title>黄</title>
<action>android.settings.ZEN_MODE_PRIORITY_SETTINGS</action>
</dashboard-tile>
<dashboard-tile>
<title>蓝</title>
<action>android.settings.DEVICE_INFO_SETTINGS</action>
</dashboard-tile>
</dashboard-category>
<dashboard-category title="title two">
<dashboard-tile>
<title>哈哈哈</title>
<action>com.android1.settings.SCHEDULE_POWER_ON_OFF_SETTING</action>
</dashboard-tile>
</dashboard-category>
</dashboard-categories>

好了,还差最后一步,在rebuildUI方法中增加我们自己的xml解析方法调用。

private void rebuildUI(Context context) {
... mDashboard.removeAllViews(); List<DashboardCategory> categories =
((SettingsActivity) context).getDashboardCategories(true); /////////////////////////////////
List<DashboardCategory> myCategories = new ArrayList<DashboardCategory>();
myCategories.clear();
loadCategoriesFromXml(myCategories);
for (int i = 0; i < myCategories.size(); i++) {
categories.add(i, myCategories.get(i));
}
/////////////////////////// final int count = categories.size();
Log.i(LOG_TAG, "new categories=" + count);
for (int n = 0; n < count; n++) {
...
}
}

干的漂亮,这样就实现了文章开头的效果。


欢迎关注我的英文公众号,每日1首英文金曲+10句英文,伴你共同进步。

微信扫一扫下方二维码即可关注:

Android6.0 源码修改之Setting列表配置项动态添加和静态添加的更多相关文章

  1. Android6.0 源码修改之 Contacts应用

    一.Contacts应用的主界面和联系人详情界面增加顶部菜单添加退出按钮 通过Hierarchy View 工具可以发现 主界面对应的类为 PeopleActivity 联系人详情界面对应的类为 Qu ...

  2. Android6.0 源码修改之 仿IOS添加全屏可拖拽浮窗返回按钮

    前言 之前写过屏蔽系统导航栏功能的文章,具体可看Android6.0 源码修改之屏蔽导航栏虚拟按键(Home和RecentAPP)/动态显示和隐藏NavigationBar 在某些特殊定制的版本中要求 ...

  3. Android6.0 源码修改之屏蔽系统短信功能和来电功能

    一.屏蔽系统短信功能 1.屏蔽所有短信 android 4.2 短信发送流程分析可参考这篇 戳这 源码位置 vendor\mediatek\proprietary\packages\apps\Mms\ ...

  4. Android6.0 源码修改之Settings音量调节界面增加通话音量调节

    前言 今天客户提了个需求,因为我们的设备在正常情况下无法调节通话音量,只有在打电话过程中,按物理音量加减键才能出现调节通话音量seekBar,很不方便,于是乎需求就来了.需要优化两个地方 1.在正常情 ...

  5. Android6.0 源码修改之屏蔽导航栏虚拟按键(Home和RecentAPP)/动态显示和隐藏NavigationBar

    场景分析, 为了完全实现沉浸式效果,在进入特定的app后可以将导航栏移除,当退出app后再次将导航栏恢复.(下面将采用发送广播的方式来移除和恢复导航栏) ps:不修改源码的情况下,简单的沉浸式效果实现 ...

  6. 在Ubuntu Server14.04上编译Android6.0源码

    此前编译过Android4.4的源码,但是现在Android都到了7.0的版本,不禁让我感叹Google的步伐真心难跟上,趁这周周末时间比较充裕,于是在过去的24小时里,毅然花了9个小时编译了一把An ...

  7. Android6.0源码下载编译刷入真机

    编译环境是Ubuntu12.04.手机nexus 5,编译安卓6.0.1源码并烧录到真机. 源码用的是科大的镜像:http://mirrors.ustc.edu.cn/aosp-monthly/,下载 ...

  8. Ubuntu16.04下编译android6.0源码

    http://blog.csdn.net/cnliwy/article/details/52189349 作为一名合格的android开发人员,怎么能不会编译android源码呢!一定要来一次说编译就 ...

  9. Android6.0源码分析之录音功能(一)【转】

    本文转载自:http://blog.csdn.net/zrf1335348191/article/details/54949549 从现在开始一周时间研究录音,下周出来一个完整的博客,监督,激励!!! ...

随机推荐

  1. git提交时忽略指定文件

    git提交时忽略指定文件 我们在项目开发过程中经常用到git来管理自己的项目,使用git版本控制进行多人协作开发具有许多优势,这里就不一一阐述了,有兴趣的同学可以自己去查找资料进行系统的学习.而本篇文 ...

  2. ORACLE spool打印

    问题描述:spool让我想起来了spooling假脱机,但是这个spool是oracle下的命令,将select查询出来的数据打印出来 1.linuxi下 spool +路径+文件名,这里的文件如果不 ...

  3. mariadb 学习笔记

    安装:yum install mariadb-server mariadb vim /etc/my.cnf.d/server.cnfinnodb_file_per_table = on#设置后当创建数 ...

  4. Nacos 集群部署

    关于nacos 集群部署,网上的示例往往不全或不可用,而官方的教程太简单了.官方也提供了一个 docker  + nacos 的伪集群的 部署示例.但毕竟是 伪, 不能实际生产使用. 全网就几乎就没有 ...

  5. oracle中创建用户、角色、权限、表空间简单使用

    一.数据库用户 创建数据库用户 create user 用户名 identified by 密码; 授权 grant 权限名 to 用户名; 查看当前用户权限 select * from sessio ...

  6. bootstrap-table 常用总结-1

    两种表格工具,今天都用到了,一种是我前几篇写到过的jqgrid,(传送门)另一个就是bootstrap-table了.用过之后会发现,两种表格的方式大同小异,但是为什么这次要换成bootstrap-t ...

  7. 通过传XML格式导入到ORACLE的销售订单

    procedure IMPORT_OM(p_unid varchar2, --流程ID p_CUSTOMER_PO varchar2, --合同编号 p_xmlstr varchar2, --clob ...

  8. 详解Python函数参数定义及传参(必备参数、关键字参数、默认可省略参数、可变不定长参数、*args、**kwargs)

    详解Python函数参数定义及传参(必备参数.关键字参数.默认可省略参数.可变不定长参数.*args.**kwargs) Python函数参数传参的种类   Python中函数参数定义及调用函数时传参 ...

  9. spring boot 加入mail邮件支持

    一.添加依赖 <!-- 邮件整合 --> <dependency> <groupId>org.springframework.boot</groupId> ...

  10. weblogic解决jar包冲突

    前言 在项目中利用POI开发excel.word的导入导出功能,而POI在解析xlsx及docx两种高版本文档时需要依赖xmlbeans包,但weblogic容器中提供了低版本的xmlbeans,从而 ...