android framework13-settings[03 activity 嵌入]

1.简介

简单点说就是在一个窗口上显示2个activity,这里学习的是同一个app的不同页面, 其实不同的app也可以同时显示在桌面上,这里不研究

  • 官方文档:

developer.android.google.cn/guide/topic...

source.android.google.cn/docs/core/d...

  • 效果可以简单理解为一个屏幕上显示2个activity页面,如下图
  • 手机目前应该是不支持的,平板,可折叠设备才可能支持。
  • 大多数搭载 Android 12L(API 级别 32)及更高版本的大屏幕设备均支持 activity 嵌入。
  • activity 嵌入无需重构代码。至于应用如何显示其 activity(是并排还是堆叠)时,可以通过创建 XML 配置文件或进行 Jetpack WindowManager API 调用来确定。
  • 简单学习下这个库的用法,最后再看下settings app里如何使用的

2.基本需求

2.1. 库

首先需要这个库

arduino 复制代码
implementation 'androidx.window:window:1.1.0-beta02'

最新版本查看

需要注意的是,引用这个库可能和其他库冲突,自己需要解决冲突,这个库里引用的kotlin库是1.8.10的,所以工程里的kotlin版本改一下

bash 复制代码
plugins {
//..
    id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
}

2.2.清单文件

告知系统您的应用已实现 activity 嵌入

android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED 属性添加到应用清单文件中的 <application> 元素,然后将该值设置为 true

ini 复制代码
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application>
        <property
            android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"
            android:value="true" />
    </application>
</manifest>

3.两种规则配置

现在就是添加规则了,有3种规则可以添加,分两种方法:

  • 一种是写个xml文件,然后工具类直接解析使用,
  • 一种是自己java代码里写配置属性

3.1.xml文件

在res/xml目录下,新建一个资源文件 xxx.xml,内容如下

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<!-- main_split_config.xml -->
<resources
    xmlns:window="http://schemas.android.com/apk/res-auto">

    <!-- Define a split for the named activities. -->
    <SplitPairRule
        window:splitRatio="0.33"
        window:splitLayoutDirection="locale"
        window:splitMinWidthDp="840"
        window:splitMaxAspectRatioInPortrait="alwaysAllow"
        window:finishPrimaryWithSecondary="never"
        window:finishSecondaryWithPrimary="always"
        window:clearTop="false">
        <SplitPairFilter
            window:primaryActivityName=".ListActivity"
            window:secondaryActivityName=".DetailActivity"/>
    </SplitPairRule>

    <!-- Specify a placeholder for the secondary container when content is
         not available. -->
    <SplitPlaceholderRule
        window:placeholderActivityName=".PlaceholderActivity"
        window:splitRatio="0.33"
        window:splitLayoutDirection="locale"
        window:splitMinWidthDp="840"
        window:splitMaxAspectRatioInPortrait="alwaysAllow"
        window:stickyPlaceholder="false">
        <ActivityFilter
            window:activityName=".ListActivity"/>
    </SplitPlaceholderRule>

    <!-- Define activities that should never be part of a split. Note: Takes
         precedence over other split rules for the activity named in the
         rule. -->
    <ActivityRule
        window:alwaysExpand="true">
        <ActivityFilter
            window:activityName=".ExpandedActivity"/>
    </ActivityRule>

</resources>

主要有3种规则,下边简单的介绍下作用,里边枚举参数的值,自己可以打开库文件里的res文件values.xml里有

>SplitPairRule

字面意思,分屏配对规则,就是配置下什么条件下可以分屏(比如宽度满足多少dp),如何分屏(左右分屏显示,上下分屏显示),页面关闭规则,主屏和副屏对应的activity是啥

  • splitRatio :主屏和分屏页面分割比列,默认0.5
  • splitLayoutDirection :屏幕分割方向,可以是左右,也可以是上下,有4种,具体见后边
  • splitMinWidthDp:指定两个 activity 在屏幕上同时显示所需的最小显示宽度 (默认600dp)
  • splitMaxAspectRatioInPortrait :垂直方向最大宽高比的限制,默认1.4
  • splitMaxAspectRatioInLandscape :横屏的时候宽高比最大值限制,默认不限制
  • finishPrimaryWithSecondary :指定当辅助容器中的所有 activity 结束时,主要分屏容器中的 activity 是否结束(永不)。
  • finishSecondaryWithPrimary :指定当主要容器中的所有 activity 结束时,辅助分屏容器中的 activity 是否结束(始终)。
  • clearTop :whether the existing secondary container on top and all activities in it should be destroyed when a new split is created using this rule

>>splitLayoutDirection

locale表示的是系统自己推断当前的方向,是ltr和rtl其中一种

ini 复制代码
<attr format="enum" name="splitLayoutDirection">
    
    <enum name="locale" value="0"/>
    
    <enum name="ltr" value="1"/>
    
    <enum name="rtl" value="2"/>
    
    <enum name="topToBottom" value="3"/>
    
    <enum name="bottomToTop" value="4"/>
</attr>

>>SplitPairFilter

  • primaryActivityName : 表明主屏的activity是哪个
  • secondaryActivityName : 表明副屏的activity是哪个
  • 可以有多个SplitPairFilter
ini 复制代码
        <SplitPairFilter
            window:primaryActivityName=".ListActivity"
            window:secondaryActivityName=".DetailActivity"/>

>SplitPlaceholderRule

这个就是副屏页面没有设置的时候,默认显示的activity,配置一般和SplitPairRule一样

  • placeholderActivityName :指定一个占位的activity
  • window:splitRatio="0.33"
  • window:splitLayoutDirection="locale"
  • window:splitMinWidthDp="840"
  • window:splitMaxAspectRatioInPortrait="alwaysAllow"
  • window:stickyPlaceholder="false">

ActivityFilter

指定哪些主屏需要这个占位activity

  • window:activityName :主屏的class name

>ActivityRule

这个规则的优先级最高,比如这里配置某个activity A是全屏模式,那么在SplitPairRule里配置的A的分屏模式规则就失效了。

  • alwaysExpand :一般设置为ture,表示指定的activity永远是全屏模式,不进行分屏

  • ActivityFilter :指定哪些activity 需要永远全屏显示。

3.1.1.RuleController

如下,我们可以在application里添加如下代码,解析xml,自动配置好规则

scss 复制代码
RuleController.getInstance(context).apply {
         setRules(RuleController.parseRules(context, R.xml.main_split_config))
     }

3.2.代码

和上边的xml对应,有3种rule可以添加

>代码

使用起来比较简单,我们一般写个工具类,完事在自己的application里初始化即可。

kotlin 复制代码
class MyApplication:Application() {


    override fun onCreate() {
        super.onCreate()
        MyEmbeddingController(this).initRules()
    }

>工具类

scss 复制代码
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import androidx.window.embedding.*
//..
class MyEmbeddingController(context: Context) {

    private val mContext: Context = context

    private val mRuleController: RuleController

    init {
        mRuleController = RuleController.getInstance(context)
    }

    fun initRules() {
 //这里最好判断下系统是否支持分屏,不支持的话下边的初始化就没啥意义
 println("log========support==${SplitController.getInstance(mContext).splitSupportStatus}")
        mRuleController.clearRules()


        //2个参数,分别是主屏和分屏对应的组件名
        val splitPairFilter = SplitPairFilter(ComponentName(mContext, HomeActivity::class.java),
            ComponentName(mContext, TestActivity::class.java),
            null)
        val splitPairFilter2 = SplitPairFilter(ComponentName(mContext, MyActivity::class.java),
            ComponentName(mContext, HomeActivity::class.java),
            null)
        //集合,可以添加多个filter
        val filterSet = setOf(splitPairFilter,splitPairFilter2)

        //分屏效果配置,type参数指明主屏所占的比列,direction表示布局方向,上下,左右
        val splitAttributes: SplitAttributes =
            SplitAttributes.Builder().setSplitType(SplitAttributes.SplitType.ratio(0.33f))
                .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT).build()

        val splitPairRule =
            SplitPairRule.Builder(filterSet)
                .setDefaultSplitAttributes(splitAttributes)
                .setMinWidthDp(600)
                .setMinSmallestWidthDp(400)
                .setMaxAspectRatioInLandscape(EmbeddingAspectRatio.ratio(1.5f))
                .setFinishPrimaryWithSecondary(SplitRule.FinishBehavior.NEVER)
                .setFinishSecondaryWithPrimary(SplitRule.FinishBehavior.ALWAYS)
                .setClearTop(false)
                .build()

        //添加规则
        mRuleController.addRule(splitPairRule)

        //一个参数,指明要匹配的组件名
        val activityFilter = ActivityFilter(ComponentName(mContext, HomeActivity::class.java), null)
        val activityFilter2 = ActivityFilter(ComponentName(mContext, MyActivity::class.java), null)
        //集合,可以添加多个
        val actFilterSet = setOf(activityFilter,activityFilter2)
        //builder的第二个参数就是占位activity的intent
        val splitPlaceholderRule =
            SplitPlaceholderRule.Builder(actFilterSet, Intent(mContext, PlaceholderActivity::class.java))
                .setDefaultSplitAttributes(splitAttributes)
                .setMinWidthDp(600)
                .setMinSmallestWidthDp(400)
                .setMaxAspectRatioInLandscape(EmbeddingAspectRatio.ratio(1.5f))
                .setSticky(true)
                .build()
        //添加规则
        mRuleController.addRule(splitPlaceholderRule)


        val activityFilter3 = ActivityFilter(ComponentName(mContext, MyActivity::class.java), null)
        val activityFilterSet = setOf(activityFilter3)
        //就一个setAlwaysExpand方法,一般设置为true,表明这些activity需要全屏,不进行分屏
        val activityRule =ActivityRule.Builder(activityFilterSet).setAlwaysExpand(true).build()
        //添加规则
        //mRuleController.addRule(activityRule)
    }
}

>SplitPairRule

  • equla条件
kotlin 复制代码
override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (other !is SplitPairRule) return false

    if (!super.equals(other)) return false
    if (filters != other.filters) return false
    if (finishPrimaryWithSecondary != other.finishPrimaryWithSecondary) return false
    if (finishSecondaryWithPrimary != other.finishSecondaryWithPrimary) return false
    if (clearTop != other.clearTop) return false

    return true
}

>SplitPlaceholderRule

  • 这里有个问题,除非是同一个placeholderIntent对象,否则肯定是不一样的。
  • Intent没有重写equal方法,所以判断相等与否应该是地址比较,所以如果不是指向同一个对象,那结果都是false
kotlin 复制代码
override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (other !is SplitPlaceholderRule) return false
    if (!super.equals(other)) return false

    if (placeholderIntent != other.placeholderIntent) return false
    if (isSticky != other.isSticky) return false
    if (finishPrimaryWithPlaceholder != other.finishPrimaryWithPlaceholder) return false
    if (filters != other.filters) return false

    return true
}

简单点说下就是下边这个是false

csharp 复制代码
new Intent().equals(new Intent()))

>结论

  • SplitPlaceholderRule有可能添加多个数据一样的,因为Intent的比较不相等,所以认为是新的rule
  • SplitPairRule 数据一样的,不会重复添加

3.2.1.SplitPairFilter

  • SplitPairRule的过滤器,用于查找一对活动是否应该放在分屏中。
  • 当新活动从主活动启动时使用它,先判断过滤器是否匹配主屏activity
  • 启动新活动的时候判断是否匹配副屏activity,都满足的话就会保存这个过滤器
kotlin 复制代码
class SplitPairFilter internal constructor(
    private val _primaryActivityName: ActivityComponentInfo,
    private val _secondaryActivityName: ActivityComponentInfo,
    val secondaryActivityIntentAction: String?
) 
  • secondaryActivityIntentAction :增加一个action,其实就是对副屏组件再增加一个action的判断条件,可为null

3.2.2.ComponentName

有3个构造方法,最终其实都是获取一个包名和class名

  • 这里主要说下,第二个参数可以传个"*"表示所有的class
  • 也可以是 com.xxx.xxx.ui.*,表示包名的ui目录下的所有activity
less 复制代码
public ComponentName(@NonNull String pkg, @NonNull String cls) {
    if (pkg == null) throw new NullPointerException("package name is null");
    if (cls == null) throw new NullPointerException("class name is null");
    mPackage = pkg;
    mClass = cls;
}
less 复制代码
public ComponentName(@NonNull Context pkg, @NonNull String cls) {
    if (cls == null) throw new NullPointerException("class name is null");
    mPackage = pkg.getPackageName();
    mClass = cls;
}
less 复制代码
public ComponentName(@NonNull Context pkg, @NonNull Class<?> cls) {
    mPackage = pkg.getPackageName();
    mClass = cls.getName();
}

还有2个静态方法获取实例,不带点的其实就是普通的构造方法,带点的会拼接上包名

less 复制代码
public static @NonNull ComponentName createRelative(@NonNull String pkg, @NonNull String cls) {
    final String fullName;
    if (cls.charAt(0) == '.') {
        // Relative to the package. Prepend the package name.
        fullName = pkg + cls;
    } else {
        // Fully qualified package name.
        fullName = cls;
    }
    return new ComponentName(pkg, fullName);
}
less 复制代码
public static @NonNull ComponentName createRelative(@NonNull Context pkg, @NonNull String cls) {
    return createRelative(pkg.getPackageName(), cls);
}

用法如下

arduino 复制代码
ComponentName.createRelative("com.xxx.xxx",".ui.*)
//其实就相当于 ComponentName("com.xxx.xxx","com.xxx.xxx.ui.*)

3.2.3.SplitType

主屏和副屏的分割比例

scss 复制代码
SplitAttributes.SplitType.ratio(0.33f)

默认值是这个

ini 复制代码
val SPLIT_TYPE_EQUAL = ratio(0.5f)

还有两种类型,以后再看,暂时不清楚啥意思

3.2.3.LayoutDirection

布局方向,有4种,其中LOCALE表示的是LEFT_TO_RIGHT和RIGHT_TO_LEFT其中的一种,具体哪种按照应用当前的方向决定的

ini 复制代码
companion object {

    val LOCALE = LayoutDirection("LOCALE", 0)

    val LEFT_TO_RIGHT = LayoutDirection("LEFT_TO_RIGHT", 1)

    val RIGHT_TO_LEFT = LayoutDirection("RIGHT_TO_LEFT", 2)

    val TOP_TO_BOTTOM = LayoutDirection("TOP_TO_BOTTOM", 3)

    val BOTTOM_TO_TOP = LayoutDirection("BOTTOM_TO_TOP", 4)

3.2.4.FinishBehavior

less 复制代码
companion object {
    /** Never finish the associated container. */
    @JvmField
    val NEVER = FinishBehavior("NEVER", 0)
    /**
     * Always finish the associated container independent of the current presentation mode.
     */
    @JvmField
    val ALWAYS = FinishBehavior("ALWAYS", 1)
    /**
     * Only finish the associated container when displayed adjacent to the one being
     * finished. Does not finish the associated one when containers are stacked on top of
     * each other.
     */
    @JvmField
    val ADJACENT = FinishBehavior("ADJACENT", 2)

3.2.5.其他方法

  • setMaxAspectRatioInPortrait :垂直方向最大宽高比,比这个大的话,那么取消分屏,默认值 ratio(1.4f)
  • setMaxAspectRatioInLandscape :横屏的时候最大宽高比,比这个大的话,取消分屏, 默认值always_allow
  • setMinWidthDp :分屏要求的容器窗口的最小宽度,dp值,默认值600dp
  • setMinHeightDp: 分屏要求的容器窗口的最小高度,dp值,默认值600dp
  • setMinSmallestWidthDp:这个是任意方向的最小宽度的最小值,默认值600dp
scss 复制代码
//这种方法的参数必须大于1
EmbeddingAspectRatio.ratio(1.6f)
//永远允许,也就是不限制宽高比
val ALWAYS_ALLOW = EmbeddingAspectRatio("ALWAYS_ALLOW", 0f)
//永远不允许,啥宽高比都不允许
val ALWAYS_DISALLOW = EmbeddingAspectRatio("ALWAYS_DISALLOW", -1f)
  • setSticky(true) :试了下,姿势不对,没发现效果,以后再补充说明

4.问题记录

.1.startActivityForResult

startActivityForResult 的页面不要用分屏,会出问题的,比如第一次点击跳转黑屏一下,页面跳转成功了,不过显示在底部了,再次点击才出现分屏效果(其实相当于页面打开了2次)

5.settings app

这里看下settings app里如何使用这个库的

5.1.库引用

首先看下settings目录下的Android.bp文件,如下,可以看到引用了window库

arduino 复制代码
// Build the Settings APK
android_library {
    name: "Settings-core",//lib的名字
    //..
    static_libs: [
        //..
        "androidx.window_window",
//..
android_app {
    name: "Settings",
    //..
    static_libs: ["Settings-core"], //引用上述lib

5.2.SettingsApplication

在自定义的application里使用自定义的工具类进行分屏规则初始化

scala 复制代码
public class SettingsApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();

        final ActivityEmbeddingRulesController controller =
                new ActivityEmbeddingRulesController(this);
        controller.initRules();
    }

5.3.ActivityEmbeddingRulesController

ini 复制代码
    public ActivityEmbeddingRulesController(Context context) {
        mContext = context;
        mRuleController = RuleController.getInstance(context);
    }

>initRules

可以看到,初始化只设置了主页的placeholder activity,以及不需要分屏的页面,并没有设置splitPair,那应该是其他地方设置了?

scss 复制代码
    public void initRules() {
        if (!ActivityEmbeddingUtils.isEmbeddingActivityEnabled(mContext)) {
            //不支持分屏,就不用初始化了
            return;
        }

        mRuleController.clearRules();

        //为homepage页设置 placeholder rule
        registerHomepagePlaceholderRule();
        //添加一些不需要分屏的activity规则
        registerAlwaysExpandRule();
    }

>isEmbeddingActivityEnabled

arduino 复制代码
    public static boolean isEmbeddingActivityEnabled(Context context) {
        //这个是配置里的设定
        final boolean isFlagEnabled = FeatureFlagUtils.isEnabled(context,
                FeatureFlagUtils.SETTINGS_SUPPORT_LARGE_SCREEN);
       //这个是判断系统是否支持分屏         
        final boolean isSplitSupported = SplitController.getInstance(context).isSplitSupported();

        return isFlagEnabled && isSplitSupported;
    }

>FeatureFlagUtils

先判断global setting,再判断Systemproperties,最后是默认配置(这个默认是true)

scss 复制代码
    public static boolean isEnabled(Context context, String feature) {
        // Override precedence:
        // Settings.Global -> sys.fflag.override.* -> static list

        // Step 1: check if feature flag is set in Settings.Global.
        String value;
        if (context != null) {
            value = Settings.Global.getString(context.getContentResolver(), feature);
            if (!TextUtils.isEmpty(value)) {
                return Boolean.parseBoolean(value);
            }
        }

        // Step 2: check if feature flag has any override.
        // Flag name: [persist.]sys.fflag.override.<feature>
        value = SystemProperties.get(getSystemPropertyPrefix(feature) + feature);
        if (!TextUtils.isEmpty(value)) {
            return Boolean.parseBoolean(value);
        }
        // Step 3: check if feature flag has any default value.
        value = getAllFeatureFlags().get(feature);
        return Boolean.parseBoolean(value);
    }

>registerHomepagePlaceholderRule

  • 分屏模式,homePage页面,右侧默认显示NetworkDashboardActivity页面
scss 复制代码
    private void registerHomepagePlaceholderRule() {
        final Set<ActivityFilter> activityFilters = new HashSet<>();
        //为下边2个主屏类设置占位activity
        addActivityFilter(activityFilters, SettingsHomepageActivity.class);
        addActivityFilter(activityFilters, Settings.class);
        //这个是占位activity,就是右侧默认显示的页面
        final Intent intent = new Intent(mContext, Settings.NetworkDashboardActivity.class);
        intent.putExtra(SettingsActivity.EXTRA_IS_SECOND_LAYER_PAGE, true);
        SplitAttributes attributes = new SplitAttributes.Builder()
                .setSplitType(SplitAttributes.SplitType.ratio(
                        ActivityEmbeddingUtils.getSplitRatio(mContext)))//0.3636
                .build();
        final SplitPlaceholderRule placeholderRule = new SplitPlaceholderRule.Builder(
                activityFilters, intent)
                .setMinWidthDp(ActivityEmbeddingUtils.getMinCurrentScreenSplitWidthDp())
                .setMinSmallestWidthDp(ActivityEmbeddingUtils.getMinSmallestScreenSplitWidthDp())
                .setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ALWAYS_ALLOW)
                .setSticky(false)
                .setFinishPrimaryWithPlaceholder(SplitRule.FinishBehavior.ADJACENT)
                .setDefaultSplitAttributes(attributes)
                .build();

        mRuleController.addRule(placeholderRule);
    }

>registerAlwaysExpandRule

指定几个需要全屏显示的页面

scss 复制代码
    private void registerAlwaysExpandRule() {
        final Set<ActivityFilter> activityFilters = new HashSet<>();
        if (FeatureFlagUtils.isEnabled(mContext, FeatureFlags.SETTINGS_SEARCH_ALWAYS_EXPAND)) {
        //搜索页
            final Intent searchIntent = FeatureFactory.getFactory(mContext)
                    .getSearchFeatureProvider()
                    .buildSearchIntent(mContext, SettingsEnums.SETTINGS_HOMEPAGE);
            addActivityFilter(activityFilters, searchIntent);
        }
        //指纹相关的几个页面
        addActivityFilter(activityFilters, FingerprintEnrollIntroduction.class);
        addActivityFilter(activityFilters, FingerprintEnrollIntroductionInternal.class);
        addActivityFilter(activityFilters, FingerprintEnrollEnrolling.class);
        //头像选择页面
        addActivityFilter(activityFilters, AvatarPickerActivity.class);
        ActivityRule activityRule = new ActivityRule.Builder(activityFilters).setAlwaysExpand(true)
                .build();
        mRuleController.addRule(activityRule);
    }又

5.4.分屏规则添加的地方

  • 初始化工具ActivityEmbeddingRulesController只添加了SplitPlaceholderRule以及ActivityRule
  • 8.7小节添加了分屏规则
  • AvatarViewMixin类里,头像的点击事件里,添加了分屏规则(homepage对account)
  • home page menu点击的时候会注册,见5.5

5.5.TopLevelSettings.java

如下,点击的时候会为homepage注册分屏subSetting,而我们跳转的页面都是subSetting

scss 复制代码
    public boolean onPreferenceTreeClick(Preference preference) {
        if (isDuplicateClick(preference)) {
            return true;
        }

        // Register SplitPairRule for SubSettings.
        //每次preference点击的时候会注册一次,见5.6
        ActivityEmbeddingRulesController.registerSubSettingsPairRule(getContext(),
                true /* clearTop */);

        setHighlightPreferenceKey(preference.getKey());
        return super.onPreferenceTreeClick(preference);
    }

    @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;
    }

5.6.registerSubSettingsPairRule

  • 参考5.5,homepage页面的选项点击跳转的activity都是SubSettings
  • 给主屏 Settings,SettingsHomepageActivity 设置副屏SubSettings
  • 给主屏 DeepLinkHomepageActivity,DeepLinkHomepageActivityInternal 设置副屏SubSettings
arduino 复制代码
    public static void registerSubSettingsPairRule(Context context, boolean clearTop) {
        if (!ActivityEmbeddingUtils.isEmbeddingActivityEnabled(context)) {
            return;
        }

        registerTwoPanePairRuleForSettingsHome(
                context,
                new ComponentName(context, SubSettings.class),
                null /* secondaryIntentAction */,
                clearTop);

        registerTwoPanePairRuleForSettingsHome(
                context,
                COMPONENT_NAME_WILDCARD,//通配符,所有的activity都可以
                Intent.ACTION_SAFETY_CENTER,//这里只检查action
                clearTop
        );
    }
    

    public static void registerTwoPanePairRuleForSettingsHome(Context context,
            ComponentName secondaryComponent,
            String secondaryIntentAction,
            boolean clearTop) {
        if (!ActivityEmbeddingUtils.isEmbeddingActivityEnabled(context)) {
            return;
        }

        registerTwoPanePairRuleForSettingsHome(
                context,
                secondaryComponent,
                secondaryIntentAction,
                true /* finishPrimaryWithSecondary */,
                true /* finishSecondaryWithPrimary */,
                clearTop);
    }
    
    public static void registerTwoPanePairRuleForSettingsHome(Context context,
            ComponentName secondaryComponent,
            String secondaryIntentAction,
            boolean finishPrimaryWithSecondary,
            boolean finishSecondaryWithPrimary,
            boolean clearTop) {
        if (!ActivityEmbeddingUtils.isEmbeddingActivityEnabled(context)) {
            return;
        }
    
        registerTwoPanePairRule(
                context,
                new ComponentName(context, Settings.class),
                secondaryComponent,
                secondaryIntentAction,
                finishPrimaryWithSecondary ? SplitRule.FinishBehavior.ADJACENT
                        : SplitRule.FinishBehavior.NEVER,
                finishSecondaryWithPrimary ? SplitRule.FinishBehavior.ADJACENT
                        : SplitRule.FinishBehavior.NEVER,
                clearTop);

        registerTwoPanePairRule(
                context,
                new ComponentName(context, SettingsHomepageActivity.class),
                //...

        // We should finish HomePageActivity altogether even if it shows in single pane for all deep
        // link cases.
        通过deep link 这种打开的,副屏关闭的时候主屏也关闭,FinishBehavior.ALWAYS
        registerTwoPanePairRule(
                context,
                new ComponentName(context, DeepLinkHomepageActivity.class),
                secondaryComponent,
                secondaryIntentAction,
                finishPrimaryWithSecondary ? SplitRule.FinishBehavior.ALWAYS
                        : SplitRule.FinishBehavior.NEVER,
                finishSecondaryWithPrimary ? SplitRule.FinishBehavior.ALWAYS
                        : SplitRule.FinishBehavior.NEVER,
                clearTop);

        registerTwoPanePairRule(
                context,
                new ComponentName(context, DeepLinkHomepageActivityInternal.class),
                //...
    }    

看下图片好说明,点击桌面快捷方式打开的wifi,如下图有个后退箭头的,点击后主屏也一起关闭了。

点击settings桌面图标打开的,如下,副屏是没有箭头的

6.SettingsHomepageActivity

这里测试分屏逻辑,平板横屏的时候有效果,先比较下分屏和普通情况的区别,看图

  • 当前展示的preference多了一个高亮显示的效果
  • settings那个标题没了,只剩下搜索框显示在顶部
  • 背景颜色变了,preference的图标没了,

6.1.清单文件

如下,启动页是这个,这个是别名,目标看 targetActivity属性

ini 复制代码
        <!-- 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: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>

6.2.onCreate

scss 复制代码
    static final int DEFAULT_HIGHLIGHT_MENU_KEY = R.string.menu_key_network;
    //..
        setContentView(R.layout.settings_homepage_container);

        mActivityEmbeddingController = ActivityEmbeddingController.getInstance(this);
        //横屏模式,第一次进来这个返回的是false,onConfigurationChanged里会变为true
        mIsTwoPane = mActivityEmbeddingController.isActivityEmbedded(this);
        
        updateAppBarMinHeight();
        initHomepageContainer();
        updateHomepageAppBar();
        updateHomepageBackground();
        mLoadedListeners = new ArraySet<>();

        initSearchBarView();
        //获取默认高亮显示的menu key,分屏模式才用得到
        final String highlightMenuKey = getHighlightMenuKey();
        //..
        mMainFragment = showFragment(() -> {
        //加载fragment
            final TopLevelSettings fragment = new TopLevelSettings();
            fragment.getArguments().putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY,
                    highlightMenuKey);
            return fragment;
        }, R.id.main_content);

        // Launch the intent from deep link for large screen devices.
        //打开deep link intent,具体看 8.7
        launchDeepLinkIntentToRight();
        updateHomepagePaddings();
        updateSplitLayout();
    }        

>onConfigurationChanged

scss 复制代码
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        final boolean newTwoPaneState = mActivityEmbeddingController.isActivityEmbedded(this);
        //这里会重新更新ui
        if (mIsTwoPane != newTwoPaneState) {
            mIsTwoPane = newTwoPaneState;
            updateHomepageAppBar();
            updateHomepageBackground();
            updateHomepagePaddings();
        }
        updateSplitLayout();
    }

>updateHomepageAppBar

appBar里有两套布局,普通的和分屏模式的,根据当前状态隐藏显示对应的,相关布局见5.4.0以及5.4.1

scss 复制代码
    private void updateHomepageAppBar() {
        if (!mIsEmbeddingActivityEnabled) {
            return;
        }
        updateAppBarMinHeight();
        if (mIsTwoPane) {
            findViewById(R.id.homepage_app_bar_regular_phone_view).setVisibility(View.GONE);
            findViewById(R.id.homepage_app_bar_two_pane_view).setVisibility(View.VISIBLE);
            findViewById(R.id.suggestion_container_two_pane).setVisibility(View.VISIBLE);
        } else {
            findViewById(R.id.homepage_app_bar_regular_phone_view).setVisibility(View.VISIBLE);
            findViewById(R.id.homepage_app_bar_two_pane_view).setVisibility(View.GONE);
            findViewById(R.id.suggestion_container_two_pane).setVisibility(View.GONE);
        }
    }

//updateAppBarMinHeight 分屏模式下,上下间距变小了

scss 复制代码
    private void updateAppBarMinHeight() {
        final int searchBarHeight = getResources().getDimensionPixelSize(R.dimen.search_bar_height);
        final int margin = getResources().getDimensionPixelSize(
                mIsEmbeddingActivityEnabled && mIsTwoPane
                        ? R.dimen.homepage_app_bar_padding_two_pane //6dp
                        : R.dimen.search_bar_margin); //16dp
        findViewById(R.id.app_bar_container).setMinimumHeight(searchBarHeight + margin * 2);
    }

>updateHomepageBackground

修改背景颜色,效果图可以看到,分屏模式背景颜色比较深

scss 复制代码
    private void updateHomepageBackground() {
        if (!mIsEmbeddingActivityEnabled) {
            return;
        }

        final Window window = getWindow();
        final int color = mIsTwoPane
                ? getColor(R.color.settings_two_pane_background_color)
                : Utils.getColorAttrDefaultColor(this, android.R.attr.colorBackground);

        window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
        // Update status bar color
        window.setStatusBarColor(color);
        // Update content background.
        findViewById(android.R.id.content).setBackgroundColor(color);
    }

>updateHomepagePaddings

scss 复制代码
    private void updateHomepagePaddings() {
        if (!mIsEmbeddingActivityEnabled) {
            return;
        }
        if (mIsTwoPane) {
            int padding = getResources().getDimensionPixelSize(
                    R.dimen.homepage_padding_horizontal_two_pane);
            //分屏模式rv左右两侧增加了padding
            mMainFragment.setPaddingHorizontal(padding);
        } else {
            mMainFragment.setPaddingHorizontal(0);
        }
        //设置的是preference的iconPaddingStart以及textPaddingStart
        mMainFragment.updatePreferencePadding(mIsTwoPane);
    }

>initSearchBarView

主要是给searchbar设置点击要跳转的intent,手机模式和分屏模式是两套布局,所以下边设置了两种

java 复制代码
    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);
        }
    }

>updateSplitLayout

  • mIsRegularLayout: 默认是true,普通模式,为false(宽度不够)是简洁模式(这里会隐藏图标)
  • 判断条件宽度大于380dp,具体见下边isRegularHomepageLayout方法
scss 复制代码
    private void updateSplitLayout() {
        if (!mIsEmbeddingActivityEnabled) {
            return;
        }

        if (mIsTwoPane) {
            if (mIsRegularLayout == ActivityEmbeddingUtils.isRegularHomepageLayout(this)) {
                // 分屏模式下,主屏的宽度符合常规模式,不做特殊处理
                return;
            }
        } else if (mIsRegularLayout) {
            // One pane mode with the regular layout, not needed to change
            return;
        }
        //能走到这里说明是非常规模式,默认值是ture,这里取反,为false
        mIsRegularLayout = !mIsRegularLayout;

        // Update search title padding
        View searchTitle = findViewById(R.id.search_bar_title);
        if (searchTitle != null) {
            int paddingStart = getResources().getDimensionPixelSize(
                    mIsRegularLayout
                            ? R.dimen.search_bar_title_padding_start_regular_two_pane
                            : R.dimen.search_bar_title_padding_start);
            //修改下start padding
            searchTitle.setPaddingRelative(paddingStart, 0, 0, 0);
        }
        // Notify fragments
        getSupportFragmentManager().getFragments().forEach(fragment -> {
            if (fragment instanceof SplitLayoutListener) {
            //这个具体作用就是隐藏preference左侧的图标
                ((SplitLayoutListener) fragment).onSplitLayoutChanged(mIsRegularLayout);
            }
        });
    }

//TopLevelSettings.java

scss 复制代码
    public void onSplitLayoutChanged(boolean isRegularLayout) {
        iteratePreferences(preference -> {
            if (preference instanceof HomepagePreferenceLayout) {
            //可以看到,常规模式图标才可见
                ((HomepagePreferenceLayout) preference).getHelper().setIconVisible(isRegularLayout);
            }
        });
    }

>isRegularHomepageLayout

判断条件,宽度大于380dp,这里需要注意一下,分屏模式下,主屏的宽度不是屏幕的宽度,而是屏幕宽度乘以规则里setSplitType规定的那个ratio(这里是0.3636)

java 复制代码
    public static boolean isRegularHomepageLayout(Activity activity) {
        DisplayMetrics dm = activity.getResources().getDisplayMetrics();
        return dm.widthPixels >= (int) TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_DIP, MIN_REGULAR_HOMEPAGE_LAYOUT_WIDTH_DP, dm);
    }

6.3.settings_homepage_container.xml

ini 复制代码
<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/settings_homepage_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.core.widget.NestedScrollView
        android:id="@+id/main_content_scrollable_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="com.android.settings.widget.HomepageAppBarScrollingViewBehavior">

        <LinearLayout
            android:id="@+id/homepage_container"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <include
                android:id="@+id/suggestion_container_two_pane"
                layout="@layout/suggestion_container_two_pane"
                android:visibility="gone"/>

            <FrameLayout
                android:id="@+id/contextual_cards_content"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginStart="@dimen/contextual_card_side_margin"
                android:layout_marginEnd="@dimen/contextual_card_side_margin"/>

            <FrameLayout
                android:id="@+id/main_content"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:animateLayoutChanges="true"/>

        </LinearLayout>
    </androidx.core.widget.NestedScrollView>

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:touchscreenBlocksFocus="false"
        android:keyboardNavigationCluster="false">
        <LinearLayout
            android:id="@+id/app_bar_container"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <include
                android:id="@+id/homepage_app_bar_regular_phone_view"
                layout="@layout/settings_homepage_app_bar_regular_phone_layout"/>

            <include
                android:id="@+id/homepage_app_bar_two_pane_view"
                layout="@layout/settings_homepage_app_bar_two_pane_layout"
                android:visibility="gone"/>
        </LinearLayout>
    </com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

6.4.appBar里的2种布局

手机模式以及分屏模式

>settings_homepage_app_bar_regular_phone_layout

这个是手机模式的,线性布局,

  • 第一行右侧显示用户头像(不一定会显示,有判断条件)
  • 第二行显示标题
  • 第三行是一些提示内容,默认是空的,有条件显示
  • 第四行就是search bar了
ini 复制代码
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="?android:attr/colorBackground"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/account_avatar"
        android:layout_width="@dimen/avatar_length"
        android:layout_height="@dimen/avatar_length"
        android:layout_marginTop="@dimen/avatar_margin_top"
        android:layout_marginEnd="@dimen/avatar_margin_end"
        android:layout_gravity="end"
        android:visibility="invisible"
        android:accessibilityTraversalAfter="@id/homepage_title"
        android:contentDescription="@string/search_bar_account_avatar_content_description"/>

    <TextView
        android:id="@+id/homepage_title"
        android:text="@string/settings_label"
        style="@style/HomepageTitleText"/>

    <FrameLayout
        android:id="@+id/suggestion_content"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <include layout="@layout/search_bar"/>

</LinearLayout>

>settings_homepage_app_bar_two_pane_layout

分屏模式下,水平方向,左侧是搜索框,右侧是用户头像(不一定显示)

ini 复制代码
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/app_bar_content"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginHorizontal="@dimen/homepage_app_bar_margin_horizontal_two_pane"
    android:padding="@dimen/homepage_app_bar_padding_two_pane"
    android:orientation="horizontal"
    android:background="@drawable/homepage_app_bar_background">

    <include layout="@layout/search_bar_two_pane_version"/>

    <ImageView
        android:id="@+id/account_avatar_two_pane_version"
        android:layout_width="@dimen/avatar_length"
        android:layout_height="@dimen/avatar_length"
        android:layout_gravity="center"
        android:contentDescription="@string/search_bar_account_avatar_content_description"/>

</LinearLayout>

>头像显示与否

默认是false,要显示的话改为true即可

arduino 复制代码
    public static boolean isAvatarSupported(Context context) {
        if (!context.getResources().getBoolean(R.bool.config_show_avatar_in_homepage)) {
            return false;
        }
        return true;
    }

7.打开副屏页,主屏自动打开

7.1.wifi快捷键

比如下边这个是打开wifi的快捷方式的intent定义,正常来讲启动这个intent应该是打开一个wifi页面,可是在可以分屏的情况下,我们左侧还会同时打开小节6的homepage页面,并高亮显示wifi条目,如何做到的?

>shortcut

ini 复制代码
    <shortcut
        android:shortcutId="manifest-shortcut-wifi"
        android:icon="@drawable/ic_shortcut_wireless"
        android:shortcutShortLabel="@string/wifi_settings" >
        <intent android:action="android.settings.WIFI_SETTINGS" />
    </shortcut>

>清单文件

ini 复制代码
        <activity
            android:name="Settings$WifiSettingsActivity"
            android:label="@string/wifi_settings"
            android:icon="@drawable/ic_homepage_network"
            android:exported="true"
            android:configChanges="orientation|keyboardHidden|screenSize">
            <intent-filter android:priority="1">
                <action android:name="android.settings.WIFI_SETTINGS"/>
                <category android:name="android.intent.category.BROWSABLE" />
                <category android:name="android.intent.category.DEFAULT"/>
            </intent-filter>
            <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
                       android:value="com.android.settings.network.NetworkProviderSettings"/>
            <meta-data android:name="com.android.settings.HIGHLIGHT_MENU_KEY"
                       android:value="@string/menu_key_network"/>
            <meta-data android:name="com.android.settings.PRIMARY_PROFILE_CONTROLLED"
                       android:value="true"/>
        </activity>

>WifiSettingsActivity

scala 复制代码
    public static class WifiSettingsActivity extends SettingsActivity { /* empty */ }

7.2.battery

>shortcut

ini 复制代码
    <shortcut
        android:shortcutId="manifest-shortcut-battery"
        android:icon="@drawable/ic_shortcut_battery"
        android:shortcutShortLabel="@string/power_usage_summary_title" >
        <intent android:action="android.intent.action.POWER_USAGE_SUMMARY" />
    </shortcut>

>清单文件

ini 复制代码
        <activity
            android:name="Settings$PowerUsageSummaryActivity"
            android:label="@string/power_usage_summary_title"
            android:exported="true"
            android:icon="@drawable/ic_homepage_battery">
            <intent-filter android:priority="1">
                <action android:name="android.intent.action.POWER_USAGE_SUMMARY" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
            <intent-filter android:priority="51">
                <action android:name="android.intent.action.MAIN" />
                <category android:name="com.android.settings.SHORTCUT" />
            </intent-filter>
            <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
                android:value="com.android.settings.fuelgauge.batteryusage.PowerUsageSummary" />
            <meta-data android:name="com.android.settings.HIGHLIGHT_MENU_KEY"
                       android:value="@string/menu_key_battery"/>
        </activity>

>PowerUsageSummaryActivity

scala 复制代码
    public static class PowerUsageSummaryActivity extends SettingsActivity { /* empty */ }

7.3.DataUsage

>shortcut

ini 复制代码
    <shortcut
        android:shortcutId="manifest-shortcut-data-usage"
        android:icon="@drawable/ic_shortcut_data_usage"
        android:shortcutShortLabel="@string/data_usage_summary_title">
        <intent
            android:action="android.intent.action.MAIN"
            android:targetPackage="com.android.settings"
            android:targetClass="com.android.settings.Settings$DataUsageSummaryActivity" />
    </shortcut>

>清单文件

ini 复制代码
        <activity
            android:name="Settings$DataUsageSummaryActivity"
            android:label="@string/data_usage_summary_title"
            android:exported="true"
            android:icon="@drawable/ic_homepage_data_usage">
            <intent-filter android:priority="1">
                <action android:name="android.settings.DATA_USAGE_SETTINGS" />
                <category android:name="android.intent.category.BROWSABLE" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
            <intent-filter android:priority="3">
                <action android:name="android.intent.action.MAIN" />
                <category android:name="com.android.settings.SHORTCUT" />
            </intent-filter>
            <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
                android:value="com.android.settings.datausage.DataUsageSummary" />
            <meta-data android:name="com.android.settings.HIGHLIGHT_MENU_KEY"
                       android:value="@string/menu_key_network"/>
        </activity>

>DataUsageSummaryActivity

scala 复制代码
    public static class DataUsageSummaryActivity extends SettingsActivity { /* empty */ }

可以上到,上边启动的3个activity都是SettingsActivity的空实现,所以先研究下SettingsActivity的启动流程

8.SettingsActivity

  • 在settings类里有上百个继承SettingsAcitity的静态类。
  • 这些类的清单文件里,都有meta-data属性。
  • name为com.android.settings.FRAGMENT_CLASS的,其value值就是activity内部要加载的Fragment
  • name为com.android.settings.HIGHLIGHT_MENU_KEY(这个name不一定有,大部分是有的),其value值就是homepage要高亮显示的menu key

8.1.onCreate

小节 7里的3种activity,最终会走onCreate里的if方法,跳转到home页面,关闭当前页面,在home页面再次打开相关的页面

scss 复制代码
    protected void onCreate(Bundle savedState) {
        //这个很重要,先解析meta data 获取要加载的fragment以及高亮显示的menu key
        getMetaData();
        //注意,这个getIntent方法重写了,不看的话你会奇怪它里边咋多了很多值
        final Intent intent = getIntent();

        if (shouldShowTwoPaneDeepLink(intent) && tryStartTwoPaneDeepLink(intent)) {
            finish();
            //小节7里的几个快捷方式会走这里,具体逻辑见 8.3之后
            super.onCreate(savedState);
            return;
        }

        super.onCreate(savedState);
        //通过intent加载ui
        createUiFromIntent(savedState, intent);
    }

>getMetaData

ini 复制代码
    private void getMetaData() {
        try {
            ActivityInfo ai = getPackageManager().getActivityInfo(getComponentName(),
                    PackageManager.GET_META_DATA);
             //meta data为null的话就不用解析了       
            if (ai == null || ai.metaData == null) return;
            //读取我们要的2个值
            mFragmentClass = ai.metaData.getString(META_DATA_KEY_FRAGMENT_CLASS);
            mHighlightMenuKey = ai.metaData.getString(META_DATA_KEY_HIGHLIGHT_MENU_KEY);
        } 
    }

>getIntent

ini 复制代码
    public Intent getIntent() {
        Intent superIntent = super.getIntent();
        String startingFragment = getStartingFragmentClass(superIntent);
        if (startingFragment != null) {
            //创建一个新的intent,赋值旧intent的所有数据
            Intent modIntent = new Intent(superIntent);
            //要启动的fragment,正常情况就是meta data里解析的那个
            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();
            }
            //把原本的intent放到bundle里了
            args.putParcelable("intent", superIntent);
            modIntent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args);
            return modIntent;
        }
        return superIntent;
    }

>getStartingFragmentClass

typescript 复制代码
    private String getStartingFragmentClass(Intent intent) {
        //正常走这里,我们在getMetaData里应该能获取到这个值
        if (mFragmentClass != null) return mFragmentClass;
        //..
        return intentClass;
    }

8.2.createUiFromIntent

scss 复制代码
    protected void createUiFromIntent(Bundle savedState, Intent intent) {
//..
        // Getting Intent properties can only be done after the super.onCreate(...)
        final String initialFragmentName = getInitialFragmentName(intent);
//..
        setContentView(R.layout.settings_main_prefs);

        getSupportFragmentManager().addOnBackStackChangedListener(this);

        if (savedState != null) {
//..
        } else {
        //加载fragment
            launchSettingFragment(initialFragmentName, intent);
        }
        //判断下是否需要显示后退键
        final boolean isActionBarButtonEnabled = isActionBarButtonEnabled(intent);

        final ActionBar actionBar = getActionBar();
        if (actionBar != null) {
            actionBar.setDisplayHomeAsUpEnabled(isActionBarButtonEnabled);
            actionBar.setHomeButtonEnabled(isActionBarButtonEnabled);
            actionBar.setDisplayShowTitleEnabled(true);
        }
        //这个是一个自定义的swtich bar,
        mMainSwitch = findViewById(R.id.switch_bar);

        // see if we should show Back/Next buttons
        if (intent.getBooleanExtra(EXTRA_PREFS_SHOW_BUTTON_BAR, false)) {
    //这里一堆是系统首次启动的时候设置页面,底部的几个按钮, 上一步,下一步,跳过等。
        }

    }

>launchSettingFragment

scss 复制代码
    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

就是根据传入的fragmentName,实例化以后进行加载

scss 复制代码
    private void switchToFragment(String fragmentName, Bundle args, boolean validate,
            int titleResId, CharSequence title) {
//..
        Fragment f = Utils.getTargetFragment(this, fragmentName, args);
        if (f == null) {
            return;
        }
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        //加载fragment
        transaction.replace(R.id.main_content, f);
        if (titleResId > 0) {
            transaction.setBreadCrumbTitle(titleResId);
        } else if (title != null) {
            transaction.setBreadCrumbTitle(title);
        }
        transaction.commitAllowingStateLoss();
        getSupportFragmentManager().executePendingTransactions();
    }

对于3个快捷方式,这里的结果是true

kotlin 复制代码
    private boolean shouldShowTwoPaneDeepLink(Intent intent) {
        if (!ActivityEmbeddingUtils.isEmbeddingActivityEnabled(this)) {
            return false;
        }
        //如果直接打开这个activity,那么isTaskRoot就是true,不满足
        if (!isTaskRoot() && (intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) == 0) {
            return false;
        }
        //快捷方式那3个的action都不是null
        if (intent.getAction() == null) {
            return false;
        }

        ActivityInfo info = intent.resolveActivityInfo(getPackageManager(),
                PackageManager.MATCH_DEFAULT_ONLY);
                //launch  mode 是默认值 0
        if (info.launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE) {
        //单例模式不允许分屏,会返回
            return false;
        }

        if (intent.getBooleanExtra(EXTRA_IS_FROM_SLICE, false)) {
            // Slice deep link starts the Intent using SubSettingLauncher. Returns true to show
            // 2-pane deep link.
            return true;
        }
        if (isSubSettings(intent)) {
            return false;
        }

        if (intent.getBooleanExtra(SettingsHomepageActivity.EXTRA_IS_FROM_SETTINGS_HOMEPAGE,
                /* defaultValue */ false)) {
            return false;
        }

        if (TextUtils.equals(intent.getAction(), Intent.ACTION_CREATE_SHORTCUT)) {
            return false;
        }
    //最终走到这里的
        return true;
    }
java 复制代码
    private boolean tryStartTwoPaneDeepLink(Intent intent) {
        intent.putExtra(EXTRA_INITIAL_CALLING_PACKAGE, PasswordUtils.getCallingAppPackageName(
                getActivityToken()));
        final Intent trampolineIntent;
        
        if (intent.getBooleanExtra(EXTRA_IS_FROM_SLICE, false)) {
        //..
        } else {
        //走这里
            trampolineIntent = getTrampolineIntent(intent, mHighlightMenuKey);
        }
       
        try {
            if (userInfo.isManagedProfile()) {
            } else {
            //..
                startActivity(trampolineIntent);
            }
        } catch (ActivityNotFoundException e) {
            return false;
        }
        return true;
    }

8.5.getTrampolineIntent

  • 对大屏设备使用,返回一个带深层链接的跳床性质的intent
  • 简单举例说明下,比如当前页面的intent是A,完事我们创建一个新的intent B,把intentA的信息封装在B里边,之后跳到B以后,在B里边再打开intent A
scss 复制代码
    public static Intent getTrampolineIntent(Intent intent, String highlightMenuKey) {
        final Intent detailIntent = new Intent(intent);
        // Guard against the arbitrary Intent injection.
        if (detailIntent.getSelector() != null) {
            detailIntent.setSelector(null);
        }
        // 根据这个action可以搜到8.6的信息
        final Intent trampolineIntent = new Intent(ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY)
                .setPackage(Utils.SETTINGS_PACKAGE_NAME)
                .replaceExtras(detailIntent);

        // Relay detail intent data to prevent failure of Intent#ParseUri.
        // If Intent#getData() is not null, Intent#toUri will return an Uri which has the scheme of
        // Intent#getData() and it may not be the scheme of an Intent.
        trampolineIntent.putExtra(
                SettingsHomepageActivity.EXTRA_SETTINGS_LARGE_SCREEN_DEEP_LINK_INTENT_DATA,
                detailIntent.getData());
        detailIntent.setData(null);

        trampolineIntent.putExtra(EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI,
                detailIntent.toUri(Intent.URI_INTENT_SCHEME));

        trampolineIntent.putExtra(EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY,
                highlightMenuKey);
        trampolineIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
        return trampolineIntent;
    }

8.6.deep intent

ini 复制代码
        <!-- Activity for launching deep link page in 2-pane. -->
        <activity android:name=".homepage.DeepLinkHomepageActivity"
                  android:label="@string/settings_label_launcher"
                  android:theme="@style/Theme.Settings.Home"
                  android:launchMode="singleTask"
                  android:exported="true"
                  android:enabled="false"
                  android:configChanges="orientation|keyboard|keyboardHidden|screenSize|screenLayout|smallestScreenSize"
                  android:permission="android.permission.LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK">
            <intent-filter>
                <action android:name="android.settings.SETTINGS_EMBED_DEEP_LINK_ACTIVITY" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
            <meta-data android:name="com.android.settings.PRIMARY_PROFILE_CONTROLLED"
                       android:value="true" />
        </activity>

//

scala 复制代码
public class DeepLinkHomepageActivity extends SettingsHomepageActivity {
}

8.7.launchDeepLinkIntentToRight

回到SettingsHomepageActivity的这个方法重新看看

  • 主要看下默认添加的分屏规则
scss 复制代码
    private void launchDeepLinkIntentToRight() {
        if (!mIsEmbeddingActivityEnabled) {
            return;
        }
        
        final Intent intent = getIntent();
        //下边一堆intent的判断就不看了,就是8.4里设置的各种参数,这里验证下,确定是deep link的intent
        if (intent == null || !TextUtils.equals(intent.getAction(),
                ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY)) {
            return;
        }

        if (!(this instanceof DeepLinkHomepageActivity
                || this instanceof DeepLinkHomepageActivityInternal)) {
            Log.e(TAG, "Not a deep link component");
            finish();
            return;
        }

        final String intentUriString = intent.getStringExtra(
                EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI);
        if (TextUtils.isEmpty(intentUriString)) {
            finish();
            return;
        }

        final Intent targetIntent;
        try {
            targetIntent = Intent.parseUri(intentUriString, Intent.URI_INTENT_SCHEME);
        } catch (URISyntaxException e) {
            finish();
            return;
        }

        final ComponentName targetComponentName = targetIntent.resolveActivity(getPackageManager());
        if (targetComponentName == null) {
            finish();
            return;
        }

        ActivityInfo targetActivityInfo = null;
        try {
            targetActivityInfo = getPackageManager().getActivityInfo(targetComponentName,
                    /* flags= */ 0);
        } catch (PackageManager.NameNotFoundException e) {
            finish();
            return;
        }

        int callingUid = -1;
        try {
            callingUid = ActivityManager.getService().getLaunchedFromUid(getActivityToken());
        } catch (RemoteException re) {
            finish();
            return;
        }

        if (!hasPrivilegedAccess(callingUid, targetActivityInfo)) {
            if (!targetActivityInfo.exported) {
                finish();
                return;
            }

            if (!isCallingAppPermitted(targetActivityInfo.permission)) {
                finish();
                return;
            }
        }

        targetIntent.setComponent(targetComponentName);

        // To prevent launchDeepLinkIntentToRight again for configuration change.
        intent.setAction(null);

        targetIntent.removeFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
        targetIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);

        // Sender of intent may want to send intent extra data to the destination of targetIntent.
        targetIntent.replaceExtras(intent);

        targetIntent.putExtra(EXTRA_IS_FROM_SETTINGS_HOMEPAGE, true);
        targetIntent.putExtra(SettingsActivity.EXTRA_IS_FROM_SLICE, false);

        targetIntent.setData(intent.getParcelableExtra(
                SettingsHomepageActivity.EXTRA_SETTINGS_LARGE_SCREEN_DEEP_LINK_INTENT_DATA));

        // Only allow FLAG_GRANT_READ/WRITE_URI_PERMISSION if calling app has the permission to
        // access specified Uri.
        int uriPermissionFlags = targetIntent.getFlags()
                & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        if (targetIntent.getData() != null
                && uriPermissionFlags != 0
                && checkUriPermission(targetIntent.getData(), /* pid= */ -1, callingUid,
                        uriPermissionFlags) == PackageManager.PERMISSION_DENIED) {
            Log.e(TAG, "Calling app must have the permission to access Uri and grant permission");
            finish();
            return;
        }
        //这里添加了分屏规则
        // Set 2-pane pair rule for the deep link page.
        ActivityEmbeddingRulesController.registerTwoPanePairRule(this,
                new ComponentName(getApplicationContext(), getClass()),
                targetComponentName,
                targetIntent.getAction(),
                SplitRule.FinishBehavior.ALWAYS,
                SplitRule.FinishBehavior.ALWAYS,
                true /* clearTop */);
        ActivityEmbeddingRulesController.registerTwoPanePairRule(this,
                new ComponentName(getApplicationContext(), Settings.class),
                targetComponentName,
                targetIntent.getAction(),
                SplitRule.FinishBehavior.ALWAYS,
                SplitRule.FinishBehavior.ALWAYS,
                true /* clearTop */);
        //启动原本的activity
        final UserHandle user = intent.getParcelableExtra(EXTRA_USER_HANDLE, UserHandle.class);
        if (user != null) {
            startActivityAsUser(targetIntent, user);
        } else {
            startActivity(targetIntent);
        }
    }

9.总结

  • 学习如何在支持分屏的设备上,让app支持分屏
  • 分屏规则有3种,可以通过xml设置,也可以通过代码添加
  • 简单研究下settings app里如何添加分屏规则的,主要就是小节 5.5里添加的
  • 学习了SettingsActivity里添加deep link的逻辑,关闭当前页面A,跳转到home page,再重新打开页面A,展示分屏效果
相关推荐
andr_gale30 分钟前
04_rc文件语法规则
android·framework·aosp
祖国的好青年2 小时前
VS Code 搭建 React Native 开发环境(Windows 实战指南)
android·windows·react native·react.js
黄林晴2 小时前
警惕!AGP 9.2 别只改版本号,R8 规则与构建链路全线收紧
android·gradle
小米渣的逆袭2 小时前
Android ADB 完全使用指南
android·adb
儿歌八万首2 小时前
Jetpack Compose Canvas 进阶:结合 animateFloatAsState 让自定义图形动起来
android·动画·compose
zhangphil3 小时前
Android Page 3 Flow读sql数据库媒体文件,Kotlin
android·kotlin
神探小白牙4 小时前
echarts,3d堆叠图
android·3d·echarts
李白的天不白4 小时前
如何项目发布到github上
android·vue.js
summerkissyou19874 小时前
Android-RTC、NTP 和 System Time(系统时间)
android