我们先看看设置中的一级菜单是怎么加载出来的
首先,从Settings的AndroidManifest.xml中开始:
<!-- Alias for launcher activity only, as this belongs to each profile. -->
<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>
点击Launcher中的Settings图标,会打开别名为Settings的Activity,其实际目标Activity是SettingsHomepageActivity,接着查看SettingsHomepageActivity:
public class SettingsHomepageActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.settings_homepage_container);
final View root = findViewById(R.id.settings_homepage_container);
root.setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
setHomepageContainerPaddingTop();
// 加载顶部的搜索框
final Toolbar toolbar = findViewById(R.id.search_action_bar);
FeatureFactory.getFactory(this).getSearchFeatureProvider()
.initSearchToolbar(this /* activity */, toolbar, SettingsEnums.SETTINGS_HOMEPAGE);
final ImageView avatarView = findViewById(R.id.account_avatar);
getLifecycle().addObserver(new AvatarViewMixin(this, avatarView));
getLifecycle().addObserver(new HideNonSystemOverlayMixin(this));
if (!getSystemService(ActivityManager.class).isLowRamDevice()) {
// Only allow contextual feature on high ram devices.
showFragment(new ContextualCardsFragment(), R.id.contextual_cards_content);
}
// 加载TopLevelSettings
showFragment(new TopLevelSettings(), R.id.main_content);
((FrameLayout) findViewById(R.id.main_content))
.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
}
布局文件对应 settings_homepage_container.xml,加载了顶部搜索框和新创建TopLevelSettings 填充 main_content
public class TopLevelSettings extends DashboardFragment implements
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
private static final String TAG = "TopLevelSettings";
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);
}
@Override
protected int getPreferenceScreenResId() {
return R.xml.top_level_settings;
}
@Override
protected String getLogTag() {
return TAG;
}
先看下top_level_settings布局文件:
<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"/>
<Preference
android:key="top_level_apps_and_notifs" // 应用和通知
android:title="@string/app_and_notification_dashboard_title"
android:summary="@string/app_and_notification_dashboard_summary"
android:icon="@drawable/ic_homepage_apps"
android:order="-100"
android:fragment="com.android.settings.applications.AppAndNotificationDashboardFragment"/>
可以看到都是一个个的Preference,对应设置界面的条目,但是数量不对等,有些项不存在比如Google设置,所以还存在动态添加。
TopLevelSettings继承自DashboardFragment,DashboardFragment继承自SettingsPreferenceFragment,看下DashboardFragment:
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
checkUiBlocker(mControllers);
refreshAllPreferences(getLogTag());
mControllers.stream()
.map(controller -> (Preference) findPreference(controller.getPreferenceKey()))
.filter(Objects::nonNull)
.forEach(preference -> {
// Give all controllers a chance to handle click.
preference.getExtras().putInt(CATEGORY, getMetricsCategory());
});
}
...
/**
* Refresh all preference items, including both static prefs from xml, and dynamic items from
* DashboardCategory.
*/
private void refreshAllPreferences(final String tag) {
final PreferenceScreen screen = getPreferenceScreen();
// First remove old preferences.
if (screen != null) {
// Intentionally do not cache PreferenceScreen because it will be recreated later.
screen.removeAll();
}
// Add resource based tiles.
displayResourceTiles(); // 加载xml中所有的preference
refreshDashboardTiles(tag); // 动态添加preference
final Activity activity = getActivity();
if (activity != null) {
Log.d(tag, "All preferences added, reporting fully drawn");
activity.reportFullyDrawn();
}
updatePreferenceVisibility(mPreferenceControllers);
}
refreshAllPreferences()方法中有两个关键性的方法,一个是displayResourceTiles()从xml布局文件中加载preference,一个是refreshDashboardTiles()动态创建添加preference。
/**
* Displays resource based tiles.
*/
private void displayResourceTiles() {
final int resId = getPreferenceScreenResId();
if (resId <= 0) {
return;
}
addPreferencesFromResource(resId);
final PreferenceScreen screen = getPreferenceScreen();
screen.setOnExpandButtonClickListener(this);
displayResourceTilesToScreen(screen);
}
addPreferencesFromResource()方法类似于activity中的setContentView()方法,加载布局文件中的preference,一级菜单的fragment是TopLevelSettings ,这里getPreferenceScreenResId()获取的就是top_level_settings.xml
/**
* Refresh preference items backed by DashboardCategory.
*/
private void refreshDashboardTiles(final String tag) {
final PreferenceScreen screen = getPreferenceScreen();
// 获取与当前调用者key值相同的DashboardCategory
final DashboardCategory category =
mDashboardFeatureProvider.getTilesForCategory(getCategoryKey());
if (category == null) {
Log.d(tag, "NO dashboard tiles for " + tag);
return;
}
// 获取DashboardCategory下的所有项
final List<Tile> tiles = category.getTiles();
if (tiles == null) {
Log.d(tag, "tile list is empty, skipping category " + category.key);
return;
}
// Create a list to track which tiles are to be removed.
final Map<String, List<DynamicDataObserver>> remove = new ArrayMap(mDashboardTilePrefKeys);
// Install dashboard tiles.
final boolean forceRoundedIcons = shouldForceRoundedIcon();
for (Tile tile : tiles) {
final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile);
if (TextUtils.isEmpty(key)) {
Log.d(tag, "tile does not contain a key, skipping " + tile);
continue;
}
// config_suppress_injected_tile_keys数组中的key不会显示
if (!displayTile(tile)) {
continue;
}
// 首次进入Settings,会走else
if (mDashboardTilePrefKeys.containsKey(key)) {
// Have the key already, will rebind.
final Preference preference = screen.findPreference(key);
mDashboardFeatureProvider.bindPreferenceToTileAndGetObservers(getActivity(),
forceRoundedIcons, getMetricsCategory(), preference, tile, key,
mPlaceholderPreferenceController.getOrder());
} else {
// Don't have this key, add it. 创建Preference并添加
final Preference pref = createPreference(tile);
final List<DynamicDataObserver> observers =
mDashboardFeatureProvider.bindPreferenceToTileAndGetObservers(getActivity(),
forceRoundedIcons, getMetricsCategory(), pref, tile, key,
mPlaceholderPreferenceController.getOrder());
screen.addPreference(pref);
registerDynamicDataObservers(observers);
mDashboardTilePrefKeys.put(key, observers);
}
remove.remove(key);
}
// Finally remove tiles that are gone.
for (Map.Entry<String, List<DynamicDataObserver>> entry : remove.entrySet()) {
final String key = entry.getKey();
mDashboardTilePrefKeys.remove(key);
final Preference preference = screen.findPreference(key);
if (preference != null) {
screen.removePreference(preference);
}
unregisterDynamicDataObservers(entry.getValue());
}
}
refreshDashboardTiles()方法主要有两个点:
①通过getTilesForCategory()获取与当前调用者key值相同的DashboardCategory,接着获取DashboardCategory中存储的所有tile。
②遍历所有tile,构建preference,通过bindPreferenceToTileAndGetObservers方法,将tile中信息与Preference绑定,并将preference添加到PreferenceScreen中,然后显示出来。
主要看下getTilesForCategory(getCategoryKey())方法,分析是如何获取DashboardCategory的
先看getCategoryKey()方法:
/**
* Returns the CategoryKey for loading {@link DashboardCategory} for this fragment.
*/
@VisibleForTesting
public String getCategoryKey() {
return DashboardFragmentRegistry.PARENT_TO_CATEGORY_KEY_MAP.get(getClass().getName());
}
这个方法是直接从DashboardFragmentRegistry.PARENT_TO_CATEGORY_KEY_MAP中获取key为getClass().getName()的value值,而.PARENT_TO_CATEGORY_KEY_MAP是在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);
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";
一级菜单对应的fragment是TopLevelSettings,所以这里key的值是CategoryKey.CATEGORY_HOMEPAGE,也就是com.android.settings.category.ia.homepage(一级菜单的CategoryKey)
再看getTilesForCategory()方法:
DashboardFeatureProviderImpl是DashboardFeatureProvider接口的实现类
@Override
public DashboardCategory getTilesForCategory(String key) {
return mCategoryManager.getTilesByCategory(mContext, key);
}
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) {
if (forceClearCache) {
mTileByComponentCache.clear();
}
mCategoryByKeyMap.clear();
mCategories = TileUtils.getCategories(context, mTileByComponentCache);
for (DashboardCategory category : mCategories) {
mCategoryByKeyMap.put(category.key, category);
}
backwardCompatCleanupForCategory(mTileByComponentCache, mCategoryByKeyMap);
sortCategories(context, mCategoryByKeyMap);
filterDuplicateTiles(mCategoryByKeyMap);
}
}
可以看到通过TileUtils.getCategories()方法获取DashboardCategory集合,然后遍历DashboardCategory集合以键值对的方式添加到mCategoryByKeyMap中 ,外部在根据key值从mCategoryByKeyMap中获取对应的DashboardCategory
再看TileUtils.getCategories()方法:
/**
* Build a list of DashboardCategory.
*/
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()) {
// Only add Settings for this user.
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);
}
}
final HashMap<String, DashboardCategory> categoryMap = new HashMap<>();
for (Tile tile : tiles) {
// 进入一级菜单,categoryKey值是com.android.settings.category.ia.homepage
// 若进入其他二级菜单比如显示,categoryKey值是com.android.settings.category.ia.display
final String categoryKey = tile.getCategory();
// 一个categoryKey对应一个DashboardCategory
DashboardCategory category = categoryMap.get(categoryKey);
if (category == null) {
category = new DashboardCategory(categoryKey);
if (category == null) {
Log.w(LOG_TAG, "Couldn't find category " + categoryKey);
continue;
}
categoryMap.put(categoryKey, category);
}
// 包含有相同categoryKey的tile,都添加到对应的DashboardCategory
category.addTile(tile);
}
final ArrayList<DashboardCategory> categories = new ArrayList<>(categoryMap.values());
for (DashboardCategory category : categories) {
category.sortTiles();
}
if (DEBUG_TIMING) {
Log.d(LOG_TAG, "getCategories took "
+ (System.currentTimeMillis() - startTime) + " ms");
}
return categories;
}
通过loadTilesForAction()方法给tiles赋值,然后遍历tiles数组,categoryMap根据tile的categoryKey判断是否包含DashboardCategory,不包含则往里添加,然后将tile添加进DashboardCategory中,遍历完之后得到categories数组,最后进行排序。
loadTilesForAction()最终是进入到loadTile()方法中
@VisibleForTesting
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);
// 如果intent是com.android.settings.action.SETTINGS将包名设置为com.android.settings
if (requireSettings) {
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();
// 获取所有包含特定Action的ResolveInfo
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) {
String categoryKey = defaultCategory;
// Load category
if ((metaData == null || !metaData.containsKey(EXTRA_CATEGORY_KEY))
&& categoryKey == null) {
Log.w(LOG_TAG, "Found " + componentInfo.name + " for intent "
+ intent + " missing metadata "
+ (metaData == null ? "" : EXTRA_CATEGORY_KEY));
return;
} else {
// 通过com.android.settings.category获取categoryKey的值
// AndoridManifest.xml中定义
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);
}
}
通过 PackageManager 查询系统中所有带指定Action的Intent对应信息 ResolveInfo 集合,然后遍历该集合获取符合条件应用信息包名、类名、categoryKey等构造 tile对象,最终添加进 tiles数组中。
最主要的就是指定的Action和CategoryKey的值
Action
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";
private static final String OPERATOR_SETTINGS = "com.android.settings.OPERATOR_APPLICATION_SETTING";
private static final String MANUFACTURER_SETTINGS = "com.android.settings.MANUFACTURER_APPLICATION_SETTING";
总结一下,首次进入设置主界面时,先加载top_level_settings.xml中的一级菜单项;然后通过TileUtils.getCategories()方法解析带有特定Action的Intent,动态获取所有非布局中定义的一级菜单项(Google和一些其他系统应用)。
下面贴下一级菜单DuraSpeed(快霸)的AndroidManifest.xml中部分代码供参考
<activity
android:name=".DuraSpeedMainActivity"
android:configChanges="orientation|keyboardHidden|screenSize|mcc|mnc|navigation"
android:label="@string/app_name"
android:launchMode="singleInstance"
android:permission="com.mediatek.duraspeed.START_DURASPEED_APP">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.INFO" />
</intent-filter>
<intent-filter android:priority="5">
<action android:name="com.android.settings.action.EXTRA_SETTINGS" />
</intent-filter>
<meta-data
android:name="com.android.settings.category"
android:value="com.android.settings.category.ia.homepage" />
<meta-data
android:name="com.android.settings.icon"
android:resource="@drawable/ic_settings_rb" />
</activity>