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,展示分屏效果
相关推荐
拭心11 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
带电的小王13 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
梦想平凡14 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道14 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
阿甘知识库15 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道15 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频
MuYe16 小时前
Android Hook - 动态加载so库
android
居居飒16 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
Henry_He19 小时前
桌面列表小部件不能点击的问题分析
android
工程师老罗19 小时前
Android笔试面试题AI答之Android基础(1)
android