本篇主要探讨Android 11中系统设置的简单实现,主要包括一下几点:
- 系统设置首页
- 系统设置其他界面
- 数据控制
一,系统设置首页
Android系统设置的主界面是com.android.settings.Settings
, 但是它只是一个activity-alias
, 指向的是.homepage.SettingsHomepageActivity
需要注意的是通过命令(adb shell "dumpsys window | grep mCurrentFocus")查看手机顶层activity时,打印出的不是targetActivity,而是这个activity-alias.
.homepage.SettingsHomepageActivity
中的逻辑并不复杂,直接加载了TopLevelSettings
这个Fragment
showFragment(new TopLevelSettings(), R.id.main_content);
TopLevelSettings
通过AndroidX
的Preference
来展示设置项列表,设置项列表的内容通过静态配置+动态添加的方式获取。
1,静态配置
所谓静态配置就是通过xml来配置。
如果你还不了解Preference
,可以移步:https://www.jianshu.com/p/348eb0928af7 简单了解一下
TopLevelSettings
继承自抽象类DashboardFragment
, 实现抽象方法getPreferenceScreenResId()
并返回preference的配置文件即可完成静态配置。
@Override
protected int getPreferenceScreenResId() {
return R.xml.top_level_settings;
}
top_level_settings
中配置了页面需要展示的配置项:
...
其中:
-
key
:该配置项的主键 -
title
:配置项的标题 -
summary
:概要标题下面的文字 -
icon
:前面的图标 -
order
:用来做排序的,值越小则排行越靠前 -
fragment
:点击该item要跳转的界面 -
controller
:该item的控制器,控制它的内容展示,是否可用,也可以控制它的点击事件
2,动态添加
动态获取是根据特殊的action标记,通过packageManger查询系统中安装的符合对应action的应用,将其动态添加到列表中。
例如:网络流量监控,存储空间管理,默认应用等配置项都是动态添加的。
具体实现可以参看文章:Android 11 Settings动态加载之快霸是如何被加载的
二,系统设置其他界面
系统设置中除了.homepage.SettingsHomepageActivity
,其他大部分的Activity都定义在Settings
中, 并且继承自SettingsActivity
, 但其中并没有实现任何逻辑。因此,这些Activity的逻辑都是在SettingsActivity
中实现。
/**
* 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中并没有实现任何逻辑,那它是怎么加载到自己应有的布局的呢?
在父类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);
...
}
跟着上面的三个步骤:
第一步
首先通过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中是怎么配置的呢?如下:
由此可知WifiInfoActivity
这个Acitivity对应的fragment是:com.android.settings.wifi.WifiInfo
第二步
通过getIntent()构造包含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
为空的情况,暂时先不管。
第三步
通过launchSettingFragment()
启动对应Fragment,这里的initialFragmentName
参数就是第二步Intent中包含的EXTRA_SHOW_FRAGMENT
参数,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);
}
// 反射创建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);
}
// 提交事务
transaction.commitAllowingStateLoss();
getSupportFragmentManager().executePendingTransactions();
Log.d(LOG_TAG, "Executed frag manager pendingTransactions");
return f;
}
三,数据控制
在首页-电池设置项中可以显示实时电量。那么它是如果实现的呢?
首先看下它是如何配置的:
配置项中配置了TopLevelBatteryPreferenceController
控制器,它继承自AbstractPreferenceController
,这个抽象类用于对所有菜单项进行统一管理(例如展示或隐藏,监听点击事件等)。
TopLevelBatteryPreferenceController
代码如下:
public class TopLevelBatteryPreferenceController extends BasePreferenceController implements
LifecycleObserver, OnStart, OnStop {
// 电量改变广播
private final BatteryBroadcastReceiver mBatteryBroadcastReceiver;
// 当前配置项
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 */);
});
}
// 控制该项是否可用
@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);
// 获取当前的配置项
mPreference = screen.findPreference(getPreferenceKey());
}
@Override
public void onStart() {
// 注册广播
mBatteryBroadcastReceiver.register();
}
@Override
public void onStop() {
// 取消注册广播
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;
}
}
代码比较简单:
- 在构造方法中初始化电量改变广播
- 在
onStart()
和onStop()
中注册和取消注册广播 - 一旦收到电量改变广播,则把电量信息保存在
mBatteryInfo
中 - 然后执行
updateState()
,该方法会调用getSummary()
把信息设置给当前配置项 -
getSummary()
中将mBatteryInfo
保存的电量信息解析出来
小结:
菜单项的展示、隐藏、监听点击事件等都是通过继承自AbstractPreferenceController
的控制器完成,这个控制器有时候实在xml中配置,有些时候则是在fragment中动态添加。
免责声明:最近做了一段时间Android 11系统设置应用相关的开发,将相关代码简单梳理总结了一下,讲解不当的地方还请大佬们指出
参考:https://blog.csdn.net/qq_34149526/article/details/83239567