Android6.0 源碼修改之Setting列表配置項動態添加和靜態添加


寫在前面

最近客戶有個需求,要求增加操作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句英文,伴你共同進步。

微信掃一掃下方二維碼即可關注:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM