本篇主要探討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ò)AndroidX
的Preference
來(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)有的布局的呢?
在父類SettingsActivity
的onCreate()
中:
@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)單:
- 在構(gòu)造方法中初始化電量改變廣播
- 在
onStart()
和onStop()
中注冊(cè)和取消注冊(cè)廣播 - 一旦收到電量改變廣播,則把電量信息保存在
mBatteryInfo
中 - 然后執(zhí)行
updateState()
灾而,該方法會(huì)調(diào)用getSummary()
把信息設(shè)置給當(dāng)前配置項(xiàng) -
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