Android6.0 Settings源码解析、配置项动态添加和静态添加

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: Android6.0 Settings源码解析、配置项动态添加和静态添加

转载请注明出处:https://blog.csdn.net/u012932409/article/details/84975178


写在前面


最近客户有个需求,要求增加操作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++) {
      ...
    }
  }

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

目录
相关文章
|
28天前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
63 0
|
28天前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
50 0
|
28天前
|
存储 Java C++
Collection-PriorityQueue源码解析
Collection-PriorityQueue源码解析
58 0
|
28天前
|
安全 Java 程序员
Collection-Stack&Queue源码解析
Collection-Stack&Queue源码解析
74 0
|
9天前
|
消息中间件 缓存 安全
Future与FutureTask源码解析,接口阻塞问题及解决方案
【11月更文挑战第5天】在Java开发中,多线程编程是提高系统并发性能和资源利用率的重要手段。然而,多线程编程也带来了诸如线程安全、死锁、接口阻塞等一系列复杂问题。本文将深度剖析多线程优化技巧、Future与FutureTask的源码、接口阻塞问题及解决方案,并通过具体业务场景和Java代码示例进行实战演示。
28 3
|
26天前
|
存储
让星星⭐月亮告诉你,HashMap的put方法源码解析及其中两种会触发扩容的场景(足够详尽,有问题欢迎指正~)
`HashMap`的`put`方法通过调用`putVal`实现,主要涉及两个场景下的扩容操作:1. 初始化时,链表数组的初始容量设为16,阈值设为12;2. 当存储的元素个数超过阈值时,链表数组的容量和阈值均翻倍。`putVal`方法处理键值对的插入,包括链表和红黑树的转换,确保高效的数据存取。
50 5
|
28天前
|
Java Spring
Spring底层架构源码解析(三)
Spring底层架构源码解析(三)
|
28天前
|
XML Java 数据格式
Spring底层架构源码解析(二)
Spring底层架构源码解析(二)
|
10天前
|
安全 5G Android开发
安卓与iOS的较量:技术深度解析
【10月更文挑战第24天】 在移动操作系统领域,安卓和iOS无疑是两大巨头。本文将深入探讨这两个系统的技术特点、优势和不足,以及它们在未来可能的发展方向。我们将通过对比分析,帮助读者更好地理解这两个系统的本质和内涵,从而引发对移动操作系统未来发展的深思。
22 0
|
28天前
|
算法 Java 程序员
Map - TreeSet & TreeMap 源码解析
Map - TreeSet & TreeMap 源码解析
31 0

推荐镜像

更多