首语
Android设置应用是Android系统中一个非常重要的系统应用,它允许用户调整和设置系统的各种参数和功能(系统设置/自定义设置/控制应用权限/开发者选项/系统信息等),使用户获得更好的使用体验。同时它一般也是Android系统开发者了解深入的第一个系统级应用,也是用户使用最频繁的系统应用。
源码目录
AOSP源码路径为packages/apps/Settings。src/com/android/settings目录下包含Settings的主要源码。libs目录下的contextualcards.aar包含实现上下文卡片功能的代码和资源,它可以将相关的内容组织在一起,以卡片的形式展现给用户。res目录下包含各种静态资源。Android.bp文件中可以看到模块名为"Settings"。
设计指南
上图是Settings里一个普通的页面,从这个页面可以看出它将许多设置放在一起,设置列表是多个控件的组合。
它有如下优点:
- 提供一个很好的概述。用户应该能够浏览设置屏幕并了解所有单独的设置及其值。
- 直观的设置项目。常用设置放在屏幕顶部。限制一个屏幕上的设置数量。将一些设置移动到单独的屏幕来创建直观的菜单。
- 使用明确的标题和状态。标题简短而有意义。在标题下方,显示状态以突出设置的值,显示具体细节。
关于Settings设计的详细规则及细节,可以参考官网:设计指南
Preference
在Android 常用组件里,存在一个Preference组件,它提供了一个方便的用户界面,用于管理和显示应用程序的各种设置选项,让用户可以轻松浏览和更改应用程序的设置。Preference还通过SharedPreference实现保存读取数据,以其key作为SharedPreference的键,实现持久化数据。Settings中大多数菜单都是通过Preference去实现,且使用的是androidx包的Preference,因此首先了解下Preference的使用。
Preference组件和其它页面组件使用类似,区别在于XML 资源必须放置于 res/xml/ 目录,Preference的根标签必须为PreferenceScreen。举例如下:
代码语言:javascript复制<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:andoird="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:key="prefer_category">
<Preference
andoird:key="feedback"
andoird:title="Send feedback"
andoird:summary="Report technical issues or suggest new features"/>
</PreferenceCategory>
</PreferenceScreen>
PreferenceCategory是对Preferences进行分组的标签,显示类别标题,并在视觉上进行分隔Preference。如设计指南中的Settings Display页面的截图中Brightness/Lock display类别标签。
以下是Preference相关属性的介绍:
attr | description |
---|---|
andoird:allowDividerAbove | 在菜单上显示一条分割线 |
andoird:allowDividerBelow | 在菜单下显示一条分割线 |
android:defaultValue | 默认值。 |
android:dependency | 设置此元素附属于另一个元素,依赖的可用则当前元素也可用(enable),反之。 |
andoird:enableCopying | 启用长按复制 |
android:enabled | 设置是否可用。 |
android:fragment | 指定跳转fragment。 |
android:icon | 指定左侧的图标。 |
andoird:iconSpaceReserved | 为图标预留位置,菜单向右偏移,默认false |
andoird:isPreferenceVisible | 菜单是否显示 |
android:key | 选项的名称,也是用来存储时唯一的key。 |
android:layout | 给当前元素指定一个自定义布局。 |
android:order | 偏好的顺序。如果不指定,默认的顺序将字母。 |
android:persistent | 是否将其值持久化。 |
android:selectable | 设置是否可以选择操作。 |
android:shouldDisableView | 当enabled设置为false变暗,同时此属性设置为false时disable但不变暗。 |
andoird:singleLineTitle | 菜单title限制为一行,默认为true |
android:summary | 摘要,配置的简要说明,显示在标题下面。 |
android:title | 选项的标题,当没有设置summary时自动垂直居中显示。 |
android:widgetLayout | 控件可调小部件的布局。是为一个优先选择的布局,比如一个复选框选择要指定一个自定义布局(注意:包括的只是复选框)在这里。 |
Setting中扩展的attr如下:
代码语言:javascript复制 <declare-styleable name="Preference">
<!-- 搜索关键词 -->
<attr name="keywords" format="string" />
<!-- 是否可搜索,默认为true -->
<attr name="searchable" format="boolean" />
<!-- Preference controller类 -->
<attr name="controller" format="string" />
<!-- 自定义字幕 -->
<attr name="unavailableSliceSubtitle" format="string" />
<!-- Preference针对work profile,默认为false -->
<attr name="forWork" format="boolean" />
<!-- 用于在双窗格上突出显示菜单首选项的标识符 -->
<attr name="highlightableMenuKey" format="string" />
</declare-styleable>
查看Preference的源码可知,还有一些自定义Preference实现的组件,如CheckBoxPreference/DropDownPreference/EditTextPreference/ListPreference/SwitchPreference等,它是针对不同Android控件(checkbox/dropdown/edittext等)实现的自定义Preference,如需使用只需要在xml引用即可
然后创建一个fragment,继承于PreferenceFragmentCompat。onCreatePreferences方法在PreferenceFragmentCompat的onCreate方法调用,用于创建Prerefence。通过setPreferencesFromResource引用定义的Preference xml资源。这样通过Preference实现的一个简单的菜单就显示在屏幕上了。
代码语言:javascript复制public class SettingsFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.root_preferences, rootKey);
}
//preference点击事件,通过key区分
@Override
public boolean onPreferenceTreeClick(@NonNull Preference preference) {
return super.onPreferenceTreeClick(preference);
}
}
看下Preference的点击事件实现,如果对应的fragment实现了OnPreferenceStartFragmentCallback,重写了onPreferenceStartFragment方法,那么Preference的跳转实现就在onPreferenceStartFragment方法里,并返回处理结果,如果没有实现OnPreferenceStartFragmentCallback,则去获取xml中设置的android:fragment或者setFragment设置的fragment跳转。
代码语言:javascript复制public abstract class PreferenceFragmentCompat extends Fragment implements
PreferenceManager.OnPreferenceTreeClickListener,
PreferenceManager.OnDisplayPreferenceDialogListener,
PreferenceManager.OnNavigateToScreenListener,
DialogPreference.TargetFragment {
@Override
public boolean onPreferenceTreeClick(@NonNull Preference preference) {
if (preference.getFragment() != null) {
boolean handled = false;
if (getCallbackFragment() instanceof OnPreferenceStartFragmentCallback) {
handled = ((OnPreferenceStartFragmentCallback) getCallbackFragment())
.onPreferenceStartFragment(this, preference);
}
Fragment callbackFragment = this;
while (!handled && callbackFragment != null) {
if (callbackFragment instanceof OnPreferenceStartFragmentCallback) {
handled = ((OnPreferenceStartFragmentCallback) callbackFragment)
.onPreferenceStartFragment(this, preference);
}
callbackFragment = callbackFragment.getParentFragment();
}
if (!handled && getContext() instanceof OnPreferenceStartFragmentCallback) {
handled = ((OnPreferenceStartFragmentCallback) getContext())
.onPreferenceStartFragment(this, preference);
}
if (!handled && getActivity() instanceof OnPreferenceStartFragmentCallback) {
handled = ((OnPreferenceStartFragmentCallback) getActivity())
.onPreferenceStartFragment(this, preference);
}
if (!handled) {
final FragmentManager fragmentManager = getParentFragmentManager();
final Bundle args = preference.getExtras();
final Fragment fragment = fragmentManager.getFragmentFactory().instantiate(
requireActivity().getClassLoader(), preference.getFragment());
fragment.setArguments(args);
fragment.setTargetFragment(this, 0);
fragmentManager.beginTransaction()
.replace(((View) requireView().getParent()).getId(), fragment)
.addToBackStack(null)
.commit();
}
return true;
}
return false;
}
}
androidx包中Preference只有针对Fragment的实现,没有针对Activity的实现。还有针对Dialog实现的PreferenceDialogFragmentCompat。在dialog里引用preference。
页面加载分析
本文以Android 13 Settings源码为例进行分析。
首页加载流程
在AndroidManifest.xml中可以看到,启动activity为Settings,Settings中包含大量的静态类继承于SettingsActivity。
代码语言:javascript复制 <activity-alias android:name="Settings"
android:label="@string/settings_label_launcher"
android:taskAffinity="com.android.settings.root"
android:launchMode="singleTask"
android:exported="true"
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>
查看SettingsActivity的onCreate方法,首先读取fragment class和HighlightMenuKey,设置布局为settings_main_prefs.xml,通过intent传递的数据显示不同的布局。切换fragment或根据之前保存状态显示页面。settings_main_prefs.xml中包含顶部的switch bar,底部的button(Back/Skip/Next),和中间的framelayout显示Fragment,switch bar和button默认隐藏。
代码语言:javascript复制public class SettingsActivity extends SettingsBaseActivity
implements PreferenceManager.OnPreferenceTreeClickListener,
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
ButtonBarHandler, FragmentManager.OnBackStackChangedListener {
@Override
protected void onCreate(Bundle savedState) {
// Should happen before any call to getIntent()
getMetaData();
final Intent intent = getIntent();
if (shouldShowTwoPaneDeepLink(intent) && tryStartTwoPaneDeepLink(intent)) {
finish();
super.onCreate(savedState);
return;
}
super.onCreate(savedState);
Log.d(LOG_TAG, "Starting onCreate");
createUiFromIntent(savedState, intent);
}
private void getMetaData() {
try {
ActivityInfo ai = getPackageManager().getActivityInfo(getComponentName(),
PackageManager.GET_META_DATA);
if (ai == null || ai.metaData == null) return;
//读取设置的fragment
mFragmentClass = ai.metaData.getString(META_DATA_KEY_FRAGMENT_CLASS);
mHighlightMenuKey = ai.metaData.getString(META_DATA_KEY_HIGHLIGHT_MENU_KEY);
} catch (NameNotFoundException nnfe) {
// No recovery
Log.d(LOG_TAG, "Cannot get Metadata for: " getComponentName().toString());
}
}
protected void createUiFromIntent(Bundle savedState, Intent intent) {
...
setContentView(R.layout.settings_main_prefs);
...
if (savedState != null) {
// We are restarting from a previous saved state; used that to initialize, instead
// of starting fresh.
setTitleFromIntent(intent);
ArrayList<DashboardCategory> categories =
savedState.getParcelableArrayList(SAVE_KEY_CATEGORIES);
if (categories != null) {
mCategories.clear();
mCategories.addAll(categories);
setTitleFromBackStack();
}
} else {
//加载fragment
launchSettingFragment(initialFragmentName, intent);
}
}
}
AndroidManifest中并没有传递Settings对应的fragment数据,而是指定targetActivity为SettingsHomepageActivity,查看SettingsHomepageActivity的onCreate方法,布局为settings_homepage_container.xml,然后初始化搜索栏。设备不是低内存的情况下加载Suggestion菜单,设置fragment为TopLevelSettings。
代码语言:javascript复制@Override
protected void onCreate(Bundle savedInstanceState) {
...
setContentView(R.layout.settings_homepage_container);
...
initSearchBarView();
...
if (!getSystemService(ActivityManager.class).isLowRamDevice()) {
initAvatarView();
final boolean scrollNeeded = mIsEmbeddingActivityEnabled
&& !TextUtils.equals(getString(DEFAULT_HIGHLIGHT_MENU_KEY), highlightMenuKey);
showSuggestionFragment(scrollNeeded);
if (FeatureFlagUtils.isEnabled(this, FeatureFlags.CONTEXTUAL_HOME)) {
showFragment(() -> new ContextualCardsFragment(), R.id.contextual_cards_content);
((FrameLayout) findViewById(R.id.main_content))
.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
}
}
mMainFragment = showFragment(() -> {
final TopLevelSettings fragment = new TopLevelSettings();
fragment.getArguments().putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY,
highlightMenuKey);
return fragment;
}, R.id.main_content);
...
}
private void initSearchBarView() {
final Toolbar toolbar = findViewById(R.id.search_action_bar);
FeatureFactory.getFactory(this).getSearchFeatureProvider()
.initSearchToolbar(this /* activity */, toolbar, SettingsEnums.SETTINGS_HOMEPAGE);
if (mIsEmbeddingActivityEnabled) {
final Toolbar toolbarTwoPaneVersion = findViewById(R.id.search_action_bar_two_pane);
//初始化搜索实现
FeatureFactory.getFactory(this).getSearchFeatureProvider()
.initSearchToolbar(this /* activity */, toolbarTwoPaneVersion,
SettingsEnums.SETTINGS_HOMEPAGE);
}
}
在搜索的实现类SearchFeatureProviderImpl中可以看到构造的搜索跳转intent如下,可以发现Settings的搜索核心实现在另外一个app内,包名为com.android.settings.intelligence。在后面会对搜索及这个模块进行深入分析,继续分析Settings页面加载。
代码语言:javascript复制 @Override
public Intent buildSearchIntent(Context context, int pageId) {
return new Intent(Settings.ACTION_APP_SEARCH_SETTINGS)
.setPackage(getSettingsIntelligencePkgName(context))
.putExtra(Intent.EXTRA_REFERRER, buildReferrer(context, pageId));
}
default String getSettingsIntelligencePkgName(Context context) {
return context.getString(R.string.config_settingsintelligence_package_name);
}
<!-- Settings intelligence package name -->
<string name="config_settingsintelligence_package_name" translatable="false">
com.android.settings.intelligence
</string>
首先看下TopLevelSettings的继承关系,TopLevelSettings继承于DashboardFragment,DashboardFragment是静态和动态Settings 菜单的基类,Settings中大多数菜单对应的fragment继承于DashboardFragment,它继承于SettingsPreferenceFragment,SettingsPreferenceFragment是Settings fragment的基类,它继承于InstrumentedPreferenceFragment,它记录fragment显示状态,继承于ObservablePreferenceFragment,ObservablePreferenceFragment是在SettingsLib里定义的,模块路径:frameworks/base/packages/SettingsLib,在后面会对这个模块深入分析。ObservablePreferenceFragment继承于PreferenceFragmentCompat。
代码语言:javascript复制public class TopLevelSettings extends DashboardFragment implements SplitLayoutListener,
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
public TopLevelSettings() {
final Bundle args = new Bundle();
// Disable the search icon because this page uses a full search view in actionbar.
args.putBoolean(NEED_SEARCH_ICON_IN_ACTION_BAR, false);
setArguments(args);
}
//设置preference对应xml资源
@Override
protected int getPreferenceScreenResId() {
return R.xml.top_level_settings;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
HighlightableMenu.fromXml(context, getPreferenceScreenResId());
use(SupportPreferenceController.class).setActivity(getActivity());
}
}
首先查看onAttach方法,调用DashboardFragment的use方法获取SupportPreferenceController实例。SupportPreferenceController存储在mPreferenceControllers中,通过addPreferenceController方法将PreferenceController添加到mPreferenceControllers中,在DashboardFragment的onAttach方法中会调用addPreferenceController,通过createPreferenceControllers方法将代码设置的controller添加到mControllers集合,然后读取xml中设置的controller添加到mControllers集合。
然后看下加载Preference的onCreatePreferences方法,首先通过getPreferenceScreenResId获取对应的xml资源,TopLevelSettings对应的是top_level_settings.xml,xml中定义了Settings首页菜单,最终通过addPreferencesFromResource方法显示Preference。在TopLevelSettings的onCreatePreferences方法还对图标颜色进行了处理。Preference点击事件调用Preferencecontroller的handlePreferenceTreeClick方法。
代码语言:javascript复制public abstract class DashboardFragment extends SettingsPreferenceFragment
implements CategoryListener, Indexable, PreferenceGroup.OnExpandButtonClickListener,
BasePreferenceController.UiBlockListener {
@Override
public void onAttach(Context context) {
super.onAttach(context);
...
// Load preference controllers from code
final List<AbstractPreferenceController> controllersFromCode =
createPreferenceControllers(context);
// Load preference controllers from xml definition
final List<BasePreferenceController> controllersFromXml = PreferenceControllerListHelper
.getPreferenceControllersFromXml(context, getPreferenceScreenResId());
// Filter xml-based controllers in case a similar controller is created from code already.
final List<BasePreferenceController> uniqueControllerFromXml =
PreferenceControllerListHelper.filterControllers(
controllersFromXml, controllersFromCode);
// Add unique controllers to list.
if (controllersFromCode != null) {
mControllers.addAll(controllersFromCode);
}
mControllers.addAll(uniqueControllerFromXml);
for (AbstractPreferenceController controller : mControllers) {
addPreferenceController(controller);
}
}
//获取对应Preference controller实例
protected <T extends AbstractPreferenceController> T use(Class<T> clazz) {
List<AbstractPreferenceController> controllerList = mPreferenceControllers.get(clazz);
if (controllerList != null) {
if (controllerList.size() > 1) {
Log.w(TAG, "Multiple controllers of Class " clazz.getSimpleName()
" found, returning first one.");
}
return (T) controllerList.get(0);
}
return null;
}
protected void addPreferenceController(AbstractPreferenceController controller) {
if (mPreferenceControllers.get(controller.getClass()) == null) {
mPreferenceControllers.put(controller.getClass(), new ArrayList<>());
}
mPreferenceControllers.get(controller.getClass()).add(controller);
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
checkUiBlocker(mControllers);
refreshAllPreferences(getLogTag());
...
}
private void refreshAllPreferences(final String tag) {
...
// Add resource based tiles.
displayResourceTiles();
...
}
private void displayResourceTiles() {
final int resId = getPreferenceScreenResId();
if (resId <= 0) {
return;
}
addPreferencesFromResource(resId);
final PreferenceScreen screen = getPreferenceScreen();
screen.setOnExpandButtonClickListener(this);
displayResourceTilesToScreen(screen);
}
@Override
public boolean onPreferenceTreeClick(Preference preference) {
final Collection<List<AbstractPreferenceController>> controllers =
mPreferenceControllers.values();
for (List<AbstractPreferenceController> controllerList : controllers) {
for (AbstractPreferenceController controller : controllerList) {
if (controller.handlePreferenceTreeClick(preference)) {
// log here since calling super.onPreferenceTreeClick will be skipped
writePreferenceClickMetric(preference);
return true;
}
}
}
return super.onPreferenceTreeClick(preference);
}
}
接下来我们以SupportPreferenceController为例,分析下PreferenceController。它继承于BasePreferenceController,BasePreferenceController继承于AbstractPreferenceController。
代码语言:javascript复制public class SupportPreferenceController extends BasePreferenceController {
//指定显示状态
@Override
public int getAvailabilityStatus() {
return mSupportFeatureProvider == null ? UNSUPPORTED_ON_DEVICE : AVAILABLE;
}
//点击事件
@Override
public boolean handlePreferenceTreeClick(Preference preference) {
if (preference == null || mActivity == null ||
!TextUtils.equals(preference.getKey(), getPreferenceKey())) {
return false;
}
mSupportFeatureProvider.startSupport(mActivity);
return true;
}
}
AbstractPreferenceController是一个抽象类,主要方法如下:
代码语言:javascript复制public abstract class AbstractPreferenceController {
//preference是否有效
public abstract boolean isAvailable();
//preference点击事件
public boolean handlePreferenceTreeClick(Preference preference) {
return false;
}
//显示preference
public void displayPreference(PreferenceScreen screen) {
final String prefKey = getPreferenceKey();
if (TextUtils.isEmpty(prefKey)) {
Log.w(TAG, "Skipping displayPreference because key is empty:" getClass().getName());
return;
}
if (isAvailable()) {
setVisible(screen, prefKey, true /* visible */);
if (this instanceof Preference.OnPreferenceChangeListener) {
final Preference preference = screen.findPreference(prefKey);
preference.setOnPreferenceChangeListener(
(Preference.OnPreferenceChangeListener) this);
}
} else {
setVisible(screen, prefKey, false /* visible */);
}
}
//更新preference状态(summary)
public void updateState(Preference preference) {
refreshSummary(preference);
}
//preference key
public abstract String getPreferenceKey();
}
BasePreferenceController对AbstractPreferenceController进行了简单封装,对Preference状态进行处理,共有6中状态,其次对Preference搜索支持也进行了处理。
代码语言:javascript复制public abstract class BasePreferenceController extends AbstractPreferenceController implements Sliceable {
@Retention(RetentionPolicy.SOURCE)
@IntDef({AVAILABLE, AVAILABLE_UNSEARCHABLE, UNSUPPORTED_ON_DEVICE, DISABLED_FOR_USER,
DISABLED_DEPENDENT_SETTING, CONDITIONALLY_UNAVAILABLE})
public @interface AvailabilityStatus {
}
//preference 有效
public static final int AVAILABLE = 0;
//preference 有效不能搜索
public static final int AVAILABLE_UNSEARCHABLE = 1;
//当前不可用,将来可能可用
public static final int CONDITIONALLY_UNAVAILABLE = 2;
//设备不支持
public static final int UNSUPPORTED_ON_DEVICE = 3;
//当前用户不支持
public static final int DISABLED_FOR_USER = 4;
//preference置灰,无法更改,依赖其它设置
public static final int DISABLED_DEPENDENT_SETTING = 5;
//指定Preference状态
@AvailabilityStatus
public abstract int getAvailabilityStatus();
//preference 有效的实现
@Override
public final boolean isAvailable() {
if (mIsForWork && mWorkProfileUser == null) {
return false;
}
final int availabilityStatus = getAvailabilityStatus();
return (availabilityStatus == AVAILABLE
|| availabilityStatus == AVAILABLE_UNSEARCHABLE
|| availabilityStatus == DISABLED_DEPENDENT_SETTING);
}
//针对DISABLED_DEPENDENT_SETTING状态进行置灰
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
if (getAvailabilityStatus() == DISABLED_DEPENDENT_SETTING) {
// Disable preference if it depends on another setting.
final Preference preference = screen.findPreference(getPreferenceKey());
if (preference != null) {
preference.setEnabled(false);
}
}
}
}
从上面可以看出来,Preferencecontroller它是Preference的控制器,控制Preference的显示,点击事件,搜索。Settings中大多数Preference的控制器都继承于BasePreferenceController。以上就是首页菜单加载的流程。
那SupportPreferenceContoller是那个菜单的控制器呢,它是首页Tips & support菜单的控制器。top_level_settings.xml有定义这个preference。Settings中大多数是以这样的实现构成的,xml定义Preference和引用Preferencecontroller,Preferencecontroller去实现对应菜单的逻辑。
二级页面加载流程
首先查看首页菜单的点击事件,它是获取了Preference controller的handlePreferenceTreeClick方法处理点击事件。
代码语言:javascript复制public abstract class DashboardFragment extends SettingsPreferenceFragment
implements CategoryListener, Indexable, PreferenceGroup.OnExpandButtonClickListener,
BasePreferenceController.UiBlockListener {
@Override
public boolean onPreferenceTreeClick(Preference preference) {
final Collection<List<AbstractPreferenceController>> controllers =
mPreferenceControllers.values();
for (List<AbstractPreferenceController> controllerList : controllers) {
for (AbstractPreferenceController controller : controllerList) {
if (controller.handlePreferenceTreeClick(preference)) {
// log here since calling super.onPreferenceTreeClick will be skipped
writePreferenceClickMetric(preference);
return true;
}
}
}
return super.onPreferenceTreeClick(preference);
}
}
Preference controller的点击基础实现如下:
代码语言:javascript复制public abstract class BasePreferenceController extends AbstractPreferenceController implements Sliceable {
@Override
public boolean handlePreferenceTreeClick(Preference preference) {
if (!TextUtils.equals(preference.getKey(), getPreferenceKey())) {
return super.handlePreferenceTreeClick(preference);
}
if (!mIsForWork || mWorkProfileUser == null) {
return super.handlePreferenceTreeClick(preference);
}
final Bundle extra = preference.getExtras();
extra.putInt(EXTRA_USER_ID, mWorkProfileUser.getIdentifier());
new SubSettingLauncher(preference.getContext())
.setDestination(preference.getFragment())
.setSourceMetricsCategory(preference.getExtras().getInt(CATEGORY,
SettingsEnums.PAGE_UNKNOWN))
.setArguments(preference.getExtras())
.setUserHandle(mWorkProfileUser)
.launch();
return true;
}
}
如果Preference controller不处理,则通过onPreferenceStartFragment方法,TopLevelSettings实现了OnPreferenceStartFragmentCallback,
代码语言:javascript复制public class TopLevelSettings extends DashboardFragment implements SplitLayoutListener,
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
@Override
public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref) {
new SubSettingLauncher(getActivity())
.setDestination(pref.getFragment())
.setArguments(pref.getExtras())
.setSourceMetricsCategory(caller instanceof Instrumentable
? ((Instrumentable) caller).getMetricsCategory()
: Instrumentable.METRICS_CATEGORY_UNKNOWN)
.setTitleRes(-1)
.setIsSecondLayerPage(true)
.launch();
return true;
}
}
可以发现,跳转二级页面的实现都是通过SubSettingLauncher来传递参数并且跳转目标fragment。toIntent方法构造调整intent,可以看到跳转的类是SubSettings,launcher方法进行跳转。
代码语言:javascript复制public class SubSettingLauncher {
public void launch() {
...
final Intent intent = toIntent();
boolean launchAsUser = mLaunchRequest.mUserHandle != null
&& mLaunchRequest.mUserHandle.getIdentifier() != UserHandle.myUserId();
boolean launchForResult = mLaunchRequest.mResultListener != null;
if (launchAsUser && launchForResult) {
launchForResultAsUser(intent, mLaunchRequest.mUserHandle,
mLaunchRequest.mResultListener, mLaunchRequest.mRequestCode);
} else if (launchAsUser && !launchForResult) {
launchAsUser(intent, mLaunchRequest.mUserHandle);
} else if (!launchAsUser && launchForResult) {
launchForResult(mLaunchRequest.mResultListener, intent, mLaunchRequest.mRequestCode);
} else {
launch(intent);
}
}
public Intent toIntent() {
final Intent intent = new Intent(Intent.ACTION_MAIN);
copyExtras(intent);
intent.setClass(mContext, SubSettings.class);
if (TextUtils.isEmpty(mLaunchRequest.mDestinationName)) {
throw new IllegalArgumentException("Destination fragment must be set");
}
intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT, mLaunchRequest.mDestinationName);
if (mLaunchRequest.mSourceMetricsCategory < 0) {
throw new IllegalArgumentException("Source metrics category must be set");
}
intent.putExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY,
mLaunchRequest.mSourceMetricsCategory);
intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, mLaunchRequest.mArguments);
intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE_RES_PACKAGE_NAME,
mLaunchRequest.mTitleResPackageName);
intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE_RESID,
mLaunchRequest.mTitleResId);
intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE, mLaunchRequest.mTitle);
intent.addFlags(mLaunchRequest.mFlags);
intent.putExtra(SettingsBaseActivity.EXTRA_PAGE_TRANSITION_TYPE,
mLaunchRequest.mTransitionType);
intent.putExtra(SettingsActivity.EXTRA_IS_SECOND_LAYER_PAGE,
mLaunchRequest.mIsSecondLayerPage);
return intent;
}
@VisibleForTesting
void launch(Intent intent) {
mContext.startActivity(intent);
}
}
SubSettings继承于SettingsActivity,说明首页菜单除了自定义实现页面跳转逻辑的之外,其它都是跳转到SubSettings这个Activity,这里有一个小技巧,正常情况下我们抓取当前页面的Activity可以通过以下命令:
代码语言:javascript复制adb shell dumpsys window | grep mCurrentFocus
但是我们不清楚这个页面对应的fragment,通过上面命令都是SubSettings。在跳转时可以通过以下命令获取fragment:
代码语言:javascript复制adb logcat -s "SubSettings"
这样就打印出了具体的启动fragment。
代码语言:javascript复制public class SubSettings extends SettingsActivity {
@Override
public boolean onNavigateUp() {
finish();
return true;
}
@Override
protected boolean isValidFragment(String fragmentName) {
//打印页面
Log.d("SubSettings", "Launching fragment " fragmentName);
return true;
}
}
剩下的相关逻辑都和TopLevelSettings类似,这里不继续展开分析了。
动态插入菜单
在Settings里的一些菜单,我们会发现一些菜单在xml和代码中并未添加,但实际上显示在页面上,这是为什么呢?原来是Settings支持动态插入菜单。实现逻辑如下:
在创建Preference的时候,refreshAllPreferences方法刷新Preference,包括来自xml的静态Preference和动态Preference。动态Preference添加实现在refreshDashboardTiles方法中。
代码语言:javascript复制public abstract class DashboardFragment extends SettingsPreferenceFragment
implements CategoryListener, Indexable, PreferenceGroup.OnExpandButtonClickListener,
BasePreferenceController.UiBlockListener {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
checkUiBlocker(mControllers);
refreshAllPreferences(getLogTag());
...
}
private void refreshAllPreferences(final String tag) {
...
// Add resource based tiles.
displayResourceTiles();
//动态Preference
refreshDashboardTiles(tag);
}
private void refreshDashboardTiles(final String tag) {
final PreferenceScreen screen = getPreferenceScreen();
final DashboardCategory category =
mDashboardFeatureProvider.getTilesForCategory(getCategoryKey());
...
final List<Tile> tiles = category.getTiles();
// Create a list to track which tiles are to be removed.
final Map<String, List<DynamicDataObserver>> remove = new ArrayMap(mDashboardTilePrefKeys);
// Install dashboard tiles and collect pending observers.
final boolean forceRoundedIcons = shouldForceRoundedIcon();
final List<DynamicDataObserver> pendingObservers = new ArrayList<>();
for (Tile tile : tiles) {
final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile);
...
final List<DynamicDataObserver> observers;
if (mDashboardTilePrefKeys.containsKey(key)) {
// Have the key already, will rebind.
final Preference preference = screen.findPreference(key);
observers = mDashboardFeatureProvider.bindPreferenceToTileAndGetObservers(
getActivity(), this, forceRoundedIcons, preference, tile, key,
mPlaceholderPreferenceController.getOrder());
} else {
// Don't have this key, add it.
final Preference pref = createPreference(tile);
observers = mDashboardFeatureProvider.bindPreferenceToTileAndGetObservers(
getActivity(), this, forceRoundedIcons, pref, tile, key,
mPlaceholderPreferenceController.getOrder());
//添加Preference
screen.addPreference(pref);
registerDynamicDataObservers(observers);
mDashboardTilePrefKeys.put(key, observers);
}
if (observers != null) {
pendingObservers.addAll(observers);
}
remove.remove(key);
...
//类别key
public String getCategoryKey() {
return DashboardFragmentRegistry.PARENT_TO_CATEGORY_KEY_MAP.get(getClass().getName());
}
}
首先获取了类别 key,PARENT_TO_CATEGORY_KEY_MAP中实现了页面和key的对应。通过页面class name来确定页面对应的key。
代码语言:javascript复制public class DashboardFragmentRegistry {
static {
PARENT_TO_CATEGORY_KEY_MAP = new ArrayMap<>();
PARENT_TO_CATEGORY_KEY_MAP.put(TopLevelSettings.class.getName(),
CategoryKey.CATEGORY_HOMEPAGE);
PARENT_TO_CATEGORY_KEY_MAP.put(
NetworkDashboardFragment.class.getName(), CategoryKey.CATEGORY_NETWORK);
PARENT_TO_CATEGORY_KEY_MAP.put(ConnectedDeviceDashboardFragment.class.getName(),
CategoryKey.CATEGORY_CONNECT);
PARENT_TO_CATEGORY_KEY_MAP.put(AdvancedConnectedDeviceDashboardFragment.class.getName(),
...
}
}
页面key的定义在CategoryKey类中。这样通过key就清楚当前页面是否动态加载那些菜单。
代码语言:javascript复制public final class CategoryKey {
// Activities in this category shows up in Settings homepage.
public static final String CATEGORY_HOMEPAGE = "com.android.settings.category.ia.homepage";
// Top level category.
public static final String CATEGORY_NETWORK = "com.android.settings.category.ia.wireless";
public static final String CATEGORY_CONNECT = "com.android.settings.category.ia.connect";
public static final String CATEGORY_DEVICE = "com.android.settings.category.ia.device";
public static final String CATEGORY_APPS = "com.android.settings.category.ia.apps";
...
}
getTilesForCategory方法的实现在DashboardFeatureProviderImpl类中,它是通过CategoryManager类的getTilesByCategory方法实现。mCategories是获取所有动态菜单的集合。
代码语言:javascript复制public class CategoryManager {
public synchronized DashboardCategory getTilesByCategory(Context context, String categoryKey) {
tryInitCategories(context);
return mCategoryByKeyMap.get(categoryKey);
}
private synchronized void tryInitCategories(Context context) {
// Keep cached tiles by default. The cache is only invalidated when InterestingConfigChange
// happens.
tryInitCategories(context, false /* forceClearCache */);
}
private synchronized void tryInitCategories(Context context, boolean forceClearCache) {
if (mCategories == null) {
final boolean firstLoading = mCategoryByKeyMap.isEmpty();
if (forceClearCache) {
mTileByComponentCache.clear();
}
mCategoryByKeyMap.clear();
//获取categories list
mCategories = TileUtils.getCategories(context, mTileByComponentCache);
for (DashboardCategory category : mCategories) {
mCategoryByKeyMap.put(category.key, category);
}
backwardCompatCleanupForCategory(mTileByComponentCache, mCategoryByKeyMap);
sortCategories(context, mCategoryByKeyMap);
filterDuplicateTiles(mCategoryByKeyMap);
if (firstLoading) {
logTiles(context);
final DashboardCategory homepageCategory = mCategoryByKeyMap.get(
CategoryKey.CATEGORY_HOMEPAGE);
if (homepageCategory == null) {
return;
}
for (Tile tile : homepageCategory.getTiles()) {
final String key = tile.getKey(context);
if (TextUtils.isEmpty(key)) {
Log.w(TAG, "Key hint missing for homepage tile: " tile.getTitle(context));
continue;
}
HighlightableMenu.addMenuKey(key);
}
}
}
}
}
从loadActivityTiles方法里可以看出,在Settings里动态插入菜单只能是系统应用。
源码路径: frameworks/base/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java
代码语言:javascript复制public class TileUtils {
public static final String EXTRA_SETTINGS_ACTION = "com.android.settings.action.EXTRA_SETTINGS";
public static final String IA_SETTINGS_ACTION = "com.android.settings.action.IA_SETTINGS";
private static final String SETTINGS_ACTION = "com.android.settings.action.SETTINGS";
public static List<DashboardCategory> getCategories(Context context,
Map<Pair<String, String>, Tile> cache) {
final long startTime = System.currentTimeMillis();
final boolean setup =
Global.getInt(context.getContentResolver(), Global.DEVICE_PROVISIONED, 0) != 0;
final ArrayList<Tile> tiles = new ArrayList<>();
final UserManager userManager = (UserManager) context.getSystemService(
Context.USER_SERVICE);
for (UserHandle user : userManager.getUserProfiles()) {
// TODO: Needs much optimization, too many PM queries going on here.
if (user.getIdentifier() == ActivityManager.getCurrentUser()) {
loadTilesForAction(context, user, SETTINGS_ACTION, cache, null, tiles, true);
loadTilesForAction(context, user, OPERATOR_SETTINGS, cache,
OPERATOR_DEFAULT_CATEGORY, tiles, false);
loadTilesForAction(context, user, MANUFACTURER_SETTINGS, cache,
MANUFACTURER_DEFAULT_CATEGORY, tiles, false);
}
if (setup) {
loadTilesForAction(context, user, EXTRA_SETTINGS_ACTION, cache, null, tiles, false);
loadTilesForAction(context, user, IA_SETTINGS_ACTION, cache, null, tiles, false);
}
}
...
return categories;
}
static void loadTilesForAction(Context context,
UserHandle user, String action, Map<Pair<String, String>, Tile> addedCache,
String defaultCategory, List<Tile> outTiles, boolean requireSettings) {
final Intent intent = new Intent(action);
if (requireSettings) {
// 只允许settings通过SETTINGS_ACTION添加
intent.setPackage(SETTING_PKG);
}
loadActivityTiles(context, user, addedCache, defaultCategory, outTiles, intent);
loadProviderTiles(context, user, addedCache, defaultCategory, outTiles, intent);
}
private static void loadActivityTiles(Context context,
UserHandle user, Map<Pair<String, String>, Tile> addedCache,
String defaultCategory, List<Tile> outTiles, Intent intent) {
final PackageManager pm = context.getPackageManager();
final List<ResolveInfo> results = pm.queryIntentActivitiesAsUser(intent,
PackageManager.GET_META_DATA, user.getIdentifier());
for (ResolveInfo resolved : results) {
//系统应用
if (!resolved.system) {
// Do not allow any app to add to settings, only system ones.
continue;
}
final ActivityInfo activityInfo = resolved.activityInfo;
final Bundle metaData = activityInfo.metaData;
loadTile(user, addedCache, defaultCategory, outTiles, intent, metaData, activityInfo);
}
}
private static void loadTile(UserHandle user, Map<Pair<String, String>, Tile> addedCache,
String defaultCategory, List<Tile> outTiles, Intent intent, Bundle metaData,
ComponentInfo componentInfo) {
// Skip loading tile if the component is tagged primary_profile_only but not running on
// the current user.
if (user.getIdentifier() != ActivityManager.getCurrentUser()
&& Tile.isPrimaryProfileOnly(componentInfo.metaData)) {
Log.w(LOG_TAG, "Found " componentInfo.name " for intent "
intent " is primary profile only, skip loading tile for uid "
user.getIdentifier());
return;
}
String categoryKey = defaultCategory;
// Load category
categoryKey = metaData.getString(EXTRA_CATEGORY_KEY);
final boolean isProvider = componentInfo instanceof ProviderInfo;
final Pair<String, String> key = isProvider
? new Pair<>(((ProviderInfo) componentInfo).authority,
metaData.getString(META_DATA_PREFERENCE_KEYHINT))
: new Pair<>(componentInfo.packageName, componentInfo.name);
Tile tile = addedCache.get(key);
if (tile == null) {
tile = isProvider
? new ProviderTile((ProviderInfo) componentInfo, categoryKey, metaData)
: new ActivityTile((ActivityInfo) componentInfo, categoryKey);
addedCache.put(key, tile);
} else {
tile.setMetaData(metaData);
}
if (!tile.userHandle.contains(user)) {
tile.userHandle.add(user);
}
if (!outTiles.contains(tile)) {
outTiles.add(tile);
}
}
}
然后遍历tiles集合,Tile类里包含Preference的数据(Key/order/intent等等),也可以设置这些字段的key。
最后一个动态菜单就被成功添加到当前页面了。我们以System->Developer options 菜单为例,它是被动态添加到Settings里的菜单。它定义在Settings的AndroidManifest.xml中。
代码语言:javascript复制<activity
android:name="Settings$DevelopmentSettingsDashboardActivity"
android:label="@string/development_settings_title"
android:icon="@drawable/ic_settings_development"
android:exported="true"
android:enabled="false">
<intent-filter android:priority="1">
<action android:name="android.settings.APPLICATION_DEVELOPMENT_SETTINGS" />
<action android:name="com.android.settings.APPLICATION_DEVELOPMENT_SETTINGS" />
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="com.android.settings.action.SETTINGS" />
</intent-filter>
<meta-data android:name="com.android.settings.order" android:value="-40"/>
<meta-data android:name="com.android.settings.category"
android:value="com.android.settings.category.ia.system" />
<meta-data android:name="com.android.settings.summary"
android:resource="@string/summary_empty"/>
<meta-data android:name="com.android.settings.icon"
android:resource="@drawable/ic_settings_development" />
<meta-data android:name="com.android.settings.FRAGMENT_CLASS"
android:value="com.android.settings.development.DevelopmentSettingsDashboardFragment" />
<meta-data android:name="com.android.settings.HIGHLIGHT_MENU_KEY"
android:value="@string/menu_key_system"/>
<meta-data android:name="com.android.settings.PRIMARY_PROFILE_CONTROLLED"
android:value="true" />
</activity>
首先它设置action为com.android.settings.action.SETTING,从前面TileUtils类分析知道这个action只能Settings里添加时设置。然后设置菜单顺序,菜单category为com.android.settings.category.ia.system,查阅DashboardFragmentRegistry类中PARENT_TO_CATEGORY_KEY_MAP的对应关系,可知对应页面fragment为SystemDashboardFragment,即System页面,接着指定了Summary,icon,fragment等等。这样开发者选项菜单就被插入到了System页面下。
Settings中还存在其它动态插入的选项,例如Google GMS插入的首页菜单Google和Digital Wellbeing & parental controlls。
因为很多应用需要在Settings中增加菜单,作为应用的入口,这种不需要修改Settings代码,而直接修改应用的AndroidManifest.xml文件,实现解耦并自动适配。当然只有系统应用可以动态在Settings插入菜单。
SettingsLib
在分析Settings页面加载分析的时候,发现有部分类来自SettingsLib模块,这个模块是干嘛的呢?
SettingsLib是Android系统中一个专注于为Settings应用提供服务的库。它包含了许多Settings基础功能,并封装了一些操作。
源码路径:frameworks/base/packages/SettingsLib
从bp文件可知,编译后会生成一个SettingsLib的jar包。SettingsLib下根据不同功能,UI基础实现有许多模块,SettingsLib引用这些模块。
SettingsLib模块只有具有系统级别权限如系统应用,framework等才可以调用,第三方应用无法使用。
此时在想,为什么不直接在Settings中直接实现呢?因为将不同功能,UI等的基础实现放在一个公共模块中,可以方便其它与Settings交互的模块或framework使用,进行定制使用,因此,SettingsLib虽专注于为Settings,但它服务于系统,供系统进行Settings相关扩展使用,例如SystemUI模块就在使用SettingsLib相关实现。
相关资料
官方文档:Android“设置”菜单
总结
AndroidSettings具有以下优势:
- 界面。引入Preference显示菜单设置项。统一的页面风格,页面简单,标题状态清晰。
- 扩展性。Android Settings页面采用单个Activity(SubSettings),多Fragment,支持其它系统应用在Settings添加菜单,可扩展性强。 Preference和PreferenceController的配合使用,方便定制新的设置项和页面,厂商定制性高。
- 使用。添加了搜索,让用户可以轻松快速找到设置项。界面也决定了用户可以轻松修改各种设置项。
Settings Android 13 SettingsLib AOSP Preference