Android 6.0 Settings模块解析(一)

Android 6.0 Setting模块解析(一)

这几天在学习 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 系统的设置菜单到底长什么样,瞅图:
Android 6.0 Settings模块解析(一)_第1张图片

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。

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

查看其类会发现,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 架构的厉害。。


最后,自己的公众号已经同步更新!!二维码如下:
这里写图片描述
有兴趣的可以关注下,此号会不定期分享一些技术文章,但不止于技术哦~~


你可能感兴趣的:(Android,源码)