Android 11 Settings源碼入門

本篇主要探討Android 11中系統(tǒng)設(shè)置的簡(jiǎn)單實(shí)現(xiàn)厢漩,主要包括一下幾點(diǎn):

  • 系統(tǒng)設(shè)置首頁(yè)
  • 系統(tǒng)設(shè)置其他界面
  • 數(shù)據(jù)控制

一,系統(tǒng)設(shè)置首頁(yè)

Android系統(tǒng)設(shè)置的主界面是com.android.settings.Settings, 但是它只是一個(gè)activity-alias, 指向的是.homepage.SettingsHomepageActivity

<activity-alias android:name="Settings"
        android:label="@string/settings_label_launcher"
        android:taskAffinity="com.android.settings.root"
        android:launchMode="singleTask"
        android:targetActivity=".homepage.SettingsHomepageActivity">
    <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>
    <meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts"/>
</activity-alias>

需要注意的是通過(guò)命令(adb shell "dumpsys window | grep mCurrentFocus")查看手機(jī)頂層activity時(shí),打印出的不是targetActivity,而是這個(gè)activity-alias.

.homepage.SettingsHomepageActivity中的邏輯并不復(fù)雜料睛,直接加載了TopLevelSettings這個(gè)Fragment

showFragment(new TopLevelSettings(), R.id.main_content);

TopLevelSettings通過(guò)AndroidXPreference來(lái)展示設(shè)置項(xiàng)列表,設(shè)置項(xiàng)列表的內(nèi)容通過(guò)靜態(tài)配置+動(dòng)態(tài)添加的方式獲取托嚣。

1损趋,靜態(tài)配置

所謂靜態(tài)配置就是通過(guò)xml來(lái)配置患久。
如果你還不了解Preference,可以移步:http://www.reibang.com/p/348eb0928af7 簡(jiǎn)單了解一下

TopLevelSettings繼承自抽象類DashboardFragment, 實(shí)現(xiàn)抽象方法getPreferenceScreenResId()并返回preference的配置文件即可完成靜態(tài)配置。

@Override
protected int getPreferenceScreenResId() {
    return R.xml.top_level_settings;
}

top_level_settings中配置了頁(yè)面需要展示的配置項(xiàng):

<PreferenceScreen
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:settings="http://schemas.android.com/apk/res-auto"
    android:key="top_level_settings">

    <Preference
        android:key="top_level_network"
        android:title="@string/network_dashboard_title"
        android:summary="@string/summary_placeholder"
        android:icon="@drawable/ic_homepage_network"
        android:order="-120"
        android:fragment="com.android.settings.network.NetworkDashboardFragment"
        settings:controller="com.android.settings.network.TopLevelNetworkEntryPreferenceController"/>

    <Preference
        android:key="top_level_connected_devices"
        android:title="@string/connected_devices_dashboard_title"
        android:summary="@string/summary_placeholder"
        android:icon="@drawable/ic_homepage_connected_device"
        android:order="-110"
        android:fragment="com.android.settings.connecteddevice.ConnectedDeviceDashboardFragment"
        settings:controller="com.android.settings.connecteddevice.TopLevelConnectedDevicesPreferenceController"/>
        
    ...
    
</PreferenceScreen>

其中:

  • key:該配置項(xiàng)的主鍵
  • title:配置項(xiàng)的標(biāo)題
  • summary:概要標(biāo)題下面的文字
  • icon:前面的圖標(biāo)
  • order:用來(lái)做排序的蒋失,值越小則排行越靠前
  • fragment:點(diǎn)擊該item要跳轉(zhuǎn)的界面
  • controller:該item的控制器返帕,控制它的內(nèi)容展示,是否可用篙挽,也可以控制它的點(diǎn)擊事件
2荆萤,動(dòng)態(tài)添加

動(dòng)態(tài)獲取是根據(jù)特殊的action標(biāo)記,通過(guò)packageManger查詢系統(tǒng)中安裝的符合對(duì)應(yīng)action的應(yīng)用铣卡,將其動(dòng)態(tài)添加到列表中链韭。

例如:網(wǎng)絡(luò)流量監(jiān)控,存儲(chǔ)空間管理煮落,默認(rèn)應(yīng)用等配置項(xiàng)都是動(dòng)態(tài)添加的敞峭。

具體實(shí)現(xiàn)可以參看文章:Android 11 Settings動(dòng)態(tài)加載之快霸是如何被加載的

二,系統(tǒng)設(shè)置其他界面

系統(tǒng)設(shè)置中除了.homepage.SettingsHomepageActivity蝉仇,其他大部分的Activity都定義在Settings中, 并且繼承自SettingsActivity, 但其中并沒(méi)有實(shí)現(xiàn)任何邏輯旋讹。因此,這些Activity的邏輯都是在SettingsActivity中實(shí)現(xiàn)轿衔。

/**
 * Top-level Settings activity
 */
public class Settings extends SettingsActivity {

    /*
    * Settings subclasses for launching independently.
    */
    public static class AssistGestureSettingsActivity extends SettingsActivity { /* empty */}
    public static class BluetoothSettingsActivity extends SettingsActivity { /* empty */ }
    public static class CreateShortcutActivity extends SettingsActivity { /* empty */ }
    public static class FaceSettingsActivity extends SettingsActivity { /* empty */ }
    public static class FingerprintSettingsActivity extends SettingsActivity { /* empty */ }
    ...
}

這些Activity中并沒(méi)有實(shí)現(xiàn)任何邏輯沉迹,那它是怎么加載到自己應(yīng)有的布局的呢?

在父類SettingsActivityonCreate()中:

@Override
protected void onCreate(Bundle savedState) {
    
    ...
    // Should happen before any call to getIntent()
    // 第一步
    getMetaData();
    // 第二步
    final Intent intent = getIntent();
    if (intent.hasExtra(EXTRA_UI_OPTIONS)) {
        getWindow().setUiOptions(intent.getIntExtra(EXTRA_UI_OPTIONS, 0));
    }

    // Getting Intent properties can only be done after the super.onCreate(...)
    final String initialFragmentName = intent.getStringExtra(EXTRA_SHOW_FRAGMENT);
    ...
    // 第三步
    launchSettingFragment(initialFragmentName, intent);
    
    ...
}

跟著上面的三個(gè)步驟:

第一步

首先通過(guò)getMetaData()獲取該Activity在manifest中配置的fragment, 并賦值給mFragmentClass

public static final String META_DATA_KEY_FRAGMENT_CLASS = "com.android.settings.FRAGMENT_CLASS";
            
private void getMetaData() {
    try {
        ActivityInfo ai = getPackageManager().getActivityInfo(getComponentName(),
                PackageManager.GET_META_DATA);
        if (ai == null || ai.metaData == null) return;
        mFragmentClass = ai.metaData.getString(META_DATA_KEY_FRAGMENT_CLASS);
    } catch (NameNotFoundException nnfe) {
        // No recovery
        Log.d(LOG_TAG, "Cannot get Metadata for: " + getComponentName().toString());
    }
}

那么manifest中是怎么配置的呢呀枢?如下:

<activity android:name=".Settings$WifiInfoActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.DEVELOPMENT_PREFERENCE" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
    <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
               android:value="com.android.settings.wifi.WifiInfo" />
</activity>

由此可知WifiInfoActivity這個(gè)Acitivity對(duì)應(yīng)的fragment是:com.android.settings.wifi.WifiInfo

第二步

通過(guò)getIntent()構(gòu)造包含EXTRA_SHOW_FRAGMENT的intent

public Intent getIntent() {
    Intent superIntent = super.getIntent();
    String startingFragment = getStartingFragmentClass(superIntent);
    // This is called from super.onCreate, isMultiPane() is not yet reliable
    // Do not use onIsHidingHeaders either, which relies itself on this method
    if (startingFragment != null) {
        Intent modIntent = new Intent(superIntent);
        modIntent.putExtra(EXTRA_SHOW_FRAGMENT, startingFragment);
        Bundle args = superIntent.getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
        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;
}

/**
 * Checks if the component name in the intent is different from the Settings class and
 * returns the class name to load as a fragment.
 */
private String getStartingFragmentClass(Intent intent) {
    // 存在mFragmentClass則直接返回
    if (mFragmentClass != null) return mFragmentClass;

    String intentClass = intent.getComponent().getClassName();
    if (intentClass.equals(getClass().getName())) return null;

    if ("com.android.settings.RunningServices".equals(intentClass)
            || "com.android.settings.applications.StorageUse".equals(intentClass)) {
        // Old names of manage apps.
        intentClass = ManageApplications.class.getName();
    }

    return intentClass;
}

這里包含了mFragmentClass為空的情況胚股,暫時(shí)先不管笼痛。

第三步

通過(guò)launchSettingFragment()啟動(dòng)對(duì)應(yīng)Fragment裙秋,這里的initialFragmentName參數(shù)就是第二步Intent中包含的EXTRA_SHOW_FRAGMENT參數(shù),mFragmentClass不為空的情況下傳入的就是mFragmentClass

void launchSettingFragment(String initialFragmentName, Intent intent) {
    if (initialFragmentName != null) {
        setTitleFromIntent(intent);

        Bundle initialArguments = intent.getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
        switchToFragment(initialFragmentName, initialArguments, true,
                mInitialTitleResId, mInitialTitle);
    } else {
        // Show search icon as up affordance if we are displaying the main Dashboard
        mInitialTitleResId = R.string.dashboard_title;
        switchToFragment(TopLevelSettings.class.getName(), null /* args */, false,
                mInitialTitleResId, mInitialTitle);
    }
}

switchToFragment()中將fragment添加到activity中缨伊。

/**
 * Switch to a specific Fragment with taking care of validation, Title and BackStack
 */
private Fragment switchToFragment(String fragmentName, Bundle args, boolean validate,
        int titleResId, CharSequence title) {
    Log.d(LOG_TAG, "Switching to fragment " + fragmentName);
    if (validate && !isValidFragment(fragmentName)) {
        throw new IllegalArgumentException("Invalid fragment for this activity: "
                + fragmentName);
    }
    // 反射創(chuàng)建fragment
    Fragment f = Utils.getTargetFragment(this, fragmentName, args);
    FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
    transaction.replace(R.id.main_content, f);
    if (titleResId > 0) {
        transaction.setBreadCrumbTitle(titleResId);
    } else if (title != null) {
        transaction.setBreadCrumbTitle(title);
    }
    // 提交事務(wù)
    transaction.commitAllowingStateLoss();
    getSupportFragmentManager().executePendingTransactions();
    Log.d(LOG_TAG, "Executed frag manager pendingTransactions");
    return f;
}

三摘刑,數(shù)據(jù)控制

在首頁(yè)-電池設(shè)置項(xiàng)中可以顯示實(shí)時(shí)電量。那么它是如果實(shí)現(xiàn)的呢刻坊?

首先看下它是如何配置的:

<Preference
    android:key="top_level_battery"
    android:title="@string/power_usage_summary_title"
    android:summary="@string/summary_placeholder"
    android:icon="@drawable/ic_homepage_battery"
    android:fragment="com.android.settings.fuelgauge.PowerUsageSummary"
    android:order="-90"
    settings:controller="com.android.settings.fuelgauge.TopLevelBatteryPreferenceController"/>

配置項(xiàng)中配置了TopLevelBatteryPreferenceController控制器枷恕,它繼承自AbstractPreferenceController,這個(gè)抽象類用于對(duì)所有菜單項(xiàng)進(jìn)行統(tǒng)一管理(例如展示或隱藏谭胚,監(jiān)聽(tīng)點(diǎn)擊事件等)徐块。

TopLevelBatteryPreferenceController代碼如下:

public class TopLevelBatteryPreferenceController extends BasePreferenceController implements
        LifecycleObserver, OnStart, OnStop {

    // 電量改變廣播
    private final BatteryBroadcastReceiver mBatteryBroadcastReceiver;
    // 當(dāng)前配置項(xiàng)
    private Preference mPreference;
    // 電量信息
    private BatteryInfo mBatteryInfo;

    public TopLevelBatteryPreferenceController(Context context, String preferenceKey) {
        super(context, preferenceKey);
        mBatteryBroadcastReceiver = new BatteryBroadcastReceiver(mContext);
        mBatteryBroadcastReceiver.setBatteryChangedListener(type -> {
            BatteryInfo.getBatteryInfo(mContext, info -> {
                mBatteryInfo = info;
                updateState(mPreference);
            }, true /* shortString */);
        });
    }

    // 控制該項(xiàng)是否可用
    @Override
    public int getAvailabilityStatus() {
        return mContext.getResources().getBoolean(R.bool.config_show_top_level_battery)
                ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
    }

    @Override
    public void displayPreference(PreferenceScreen screen) {
        super.displayPreference(screen);
        // 獲取當(dāng)前的配置項(xiàng)
        mPreference = screen.findPreference(getPreferenceKey());
    }

    @Override
    public void onStart() {
        // 注冊(cè)廣播
        mBatteryBroadcastReceiver.register();
    }

    @Override
    public void onStop() {
        // 取消注冊(cè)廣播
        mBatteryBroadcastReceiver.unRegister();
    }

    @Override
    public CharSequence getSummary() {
        // 返回電量概覽
        return getDashboardLabel(mContext, mBatteryInfo);
    }

    // 獲取電量信息
    static CharSequence getDashboardLabel(Context context, BatteryInfo info) {
        if (info == null || context == null) {
            return null;
        }
        CharSequence label;
        if (!info.discharging && info.chargeLabel != null) {
            label = info.chargeLabel;
        } else if (info.remainingLabel == null) {
            label = info.batteryPercentString;
        } else {
            label = context.getString(R.string.power_remaining_settings_home_page,
                    info.batteryPercentString,
                    info.remainingLabel);
        }
        return label;
    }
}

代碼比較簡(jiǎn)單:

  1. 在構(gòu)造方法中初始化電量改變廣播
  2. onStart()onStop()中注冊(cè)和取消注冊(cè)廣播
  3. 一旦收到電量改變廣播,則把電量信息保存在mBatteryInfo
  4. 然后執(zhí)行updateState()灾而,該方法會(huì)調(diào)用getSummary()把信息設(shè)置給當(dāng)前配置項(xiàng)
  5. getSummary()中將mBatteryInfo保存的電量信息解析出來(lái)
小結(jié):

菜單項(xiàng)的展示胡控、隱藏、監(jiān)聽(tīng)點(diǎn)擊事件等都是通過(guò)繼承自AbstractPreferenceController的控制器完成旁趟,這個(gè)控制器有時(shí)候?qū)嵲趚ml中配置昼激,有些時(shí)候則是在fragment中動(dòng)態(tài)添加。

免責(zé)聲明:最近做了一段時(shí)間Android 11系統(tǒng)設(shè)置應(yīng)用相關(guān)的開(kāi)發(fā),將相關(guān)代碼簡(jiǎn)單梳理總結(jié)了一下橙困,講解不當(dāng)?shù)牡胤竭€請(qǐng)大佬們指出

參考:https://blog.csdn.net/qq_34149526/article/details/83239567

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末瞧掺,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子凡傅,更是在濱河造成了極大的恐慌辟狈,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,820評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件像捶,死亡現(xiàn)場(chǎng)離奇詭異上陕,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)拓春,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門释簿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人硼莽,你說(shuō)我怎么就攤上這事庶溶。” “怎么了懂鸵?”我有些...
    開(kāi)封第一講書人閱讀 168,324評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵偏螺,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我匆光,道長(zhǎng)套像,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 59,714評(píng)論 1 297
  • 正文 為了忘掉前任终息,我火速辦了婚禮夺巩,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘周崭。我一直安慰自己柳譬,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,724評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布续镇。 她就那樣靜靜地躺著美澳,像睡著了一般。 火紅的嫁衣襯著肌膚如雪摸航。 梳的紋絲不亂的頭發(fā)上制跟,一...
    開(kāi)封第一講書人閱讀 52,328評(píng)論 1 310
  • 那天,我揣著相機(jī)與錄音酱虎,去河邊找鬼雨膨。 笑死,一個(gè)胖子當(dāng)著我的面吹牛逢净,可吹牛的內(nèi)容都是我干的哥放。 我是一名探鬼主播歼指,決...
    沈念sama閱讀 40,897評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼甥雕!你這毒婦竟也來(lái)了踩身?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 39,804評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤社露,失蹤者是張志新(化名)和其女友劉穎挟阻,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體峭弟,經(jīng)...
    沈念sama閱讀 46,345評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡附鸽,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,431評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了瞒瘸。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片坷备。...
    茶點(diǎn)故事閱讀 40,561評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖情臭,靈堂內(nèi)的尸體忽然破棺而出省撑,到底是詐尸還是另有隱情,我是刑警寧澤俯在,帶...
    沈念sama閱讀 36,238評(píng)論 5 350
  • 正文 年R本政府宣布竟秫,位于F島的核電站,受9級(jí)特大地震影響跷乐,放射性物質(zhì)發(fā)生泄漏肥败。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,928評(píng)論 3 334
  • 文/蒙蒙 一愕提、第九天 我趴在偏房一處隱蔽的房頂上張望馒稍。 院中可真熱鬧,春花似錦揪荣、人聲如沸筷黔。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,417評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至椎例,卻和暖如春挨决,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背订歪。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,528評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工脖祈, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人刷晋。 一個(gè)月前我還...
    沈念sama閱讀 48,983評(píng)論 3 376
  • 正文 我出身青樓盖高,卻偏偏與公主長(zhǎng)得像慎陵,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子喻奥,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,573評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • 前言: 只能說(shuō)對(duì)魅族手機(jī)系統(tǒng)很是無(wú)奈跋Α!W膊稀润梯!為了分析魅族手機(jī)無(wú)法通過(guò)常見(jiàn)的方法跳到系統(tǒng)設(shè)置的無(wú)障礙服務(wù),只好通過(guò)豌...
    Ucoon閱讀 3,398評(píng)論 1 1
  • 一.Settings的啟動(dòng)流程 (Settings部分源碼在packages/Settings下) 1.入口 Se...
    BugMonkey閱讀 10,404評(píng)論 0 9
  • 用兩張圖告訴你甥厦,為什么你的 App 會(huì)卡頓? - Android - 掘金 Cover 有什么料纺铭? 從這篇文章中你...
    hw1212閱讀 12,745評(píng)論 2 59
  • Google在今年的IO大會(huì)上宣布,將Android開(kāi)發(fā)的官方語(yǔ)言更換為Kotlin刀疙,作為跟著Google玩兒An...
    藍(lán)灰_q閱讀 76,896評(píng)論 31 489
  • 今天感恩節(jié)哎舶赔,感謝一直在我身邊的親朋好友。感恩相遇谦秧!感恩不離不棄顿痪。 中午開(kāi)了第一次的黨會(huì),身份的轉(zhuǎn)變要...
    迷月閃星情閱讀 10,571評(píng)論 0 11