Android NDK开发详解大屏设备之activity 嵌入二
固定的纵向屏幕方向
android:screenOrientation 清单设置让应用能够将 activity 限制为纵向或横向。为提升平板电脑和可折叠设备等大屏设备上的用户体验,设备制造商 (OEM) 会忽略屏幕方向要求,并采用信箱模式,在横屏下纵向显示应用或在竖屏下横向显示应用。
图 12. 采用信箱模式的 activity:在横屏设备上固定为竖屏显示(左侧),在竖屏设备上固定为横屏显示(右侧)。
同理,启用 activity 嵌入后,OEM 能够自定义设备,通过信箱模式在屏幕方向为横屏的大屏设备(宽度 ≥ 600dp)上呈现固定竖屏的 activity。当固定纵向的 activity 启动第二个 activity 时,设备会在双窗格显示屏中并排显示这两个 activity。
图 13. 固定纵向的 activity A 在侧面启动 activity B。
将 android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED 属性一律添加到您的应用清单文件中,告知设备您的应用支持 activity 嵌入(请参阅下文的分屏配置)。这样一来,OEM 自定义的设备就可以确定是否采用信箱模式呈现固定纵向的 activity。
分屏配置
分屏规则用于配置 activity 分屏。您可以在 XML 配置文件中或通过进行 Jetpack WindowManager API 调用来定义分屏规则。
无论是哪种情况,应用都必须访问 WindowManager 库,并且必须通知系统应用已实现 activity 嵌入。
请执行以下操作:
将最新的 WindowManager 库依赖项添加到应用的模块级 build.gradle 文件中,例如:
implementation 'androidx.window:window:1.1.0-beta02'
注意:如需了解发布版本,请参阅 WindowManager 版本说明。
WindowManager 库提供了 activity 嵌入所需的所有组件。
告知系统您的应用已实现 activity 嵌入。
将 android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED 属性添加到应用清单文件中的 元素,然后将该值设置为 true,例如:
<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>
在 WindowManager 版本 1.1.0-alpha06 及更高版本中,除非将该属性添加到清单中并设置为 true,否则系统会停用 activity 嵌入分屏。
此外,设备制造商会使用该设置来为支持 activity 嵌入的应用启用自定义功能。例如,设备可以在横向显示屏上将仅限纵向模式的 activity 设为信箱模式,以便在第二个 activity 启动时,让该 activity 转换为双窗格布局(请参阅固定的纵向屏幕方向)。
XML 配置
如需创建基于 XML 的 activity 嵌入实现,请完成以下步骤:
创建一个执行以下操作的 XML 资源文件:
定义共享分屏的 activity
配置分屏选项
在没有可用内容时,为分屏的辅助容器创建占位符
指定绝不应属于分屏的 activity
例如:
<!-- 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>
创建初始化程序。
WindowManager RuleController 组件会解析 XML 配置文件,并将规则提供给系统。Jetpack Startup 库 Initializer 会在应用启动时为 RuleController 提供 XML 文件,以便在任何 activity 启动时,这些规则都会生效。
如需创建初始化程序,请执行以下操作:
将最新的 Jetpack Startup 库依赖项添加到模块级 build.gradle 文件中,例如:
implementation 'androidx.startup:startup-runtime:1.1.1'
注意:如需了解发布版本,请参阅 Startup 版本说明。
创建一个实现 Initializer 接口的类。
初始化程序会通过将 XML 配置文件 (main_split_config.xml) 的 ID 传递给 RuleController.parseRules() 方法,将分屏规则提供给 RuleController。
Kotlin
class SplitInitializer : Initializer<RuleController> {
override fun create(context: Context): RuleController {
return RuleController.getInstance(context).apply {
setRules(RuleController.parseRules(context, R.xml.main_split_config))
}
}
override fun dependencies(): List<Class<out Initializer<*>>> {
return emptyList()
}
}
Java
public class SplitInitializer implements Initializer<RuleController> {
@NonNull
@Override
public RuleController create(@NonNull Context context) {
RuleController ruleController = RuleController.getInstance(context);
ruleController.setRules(
RuleController.parseRules(context, R.xml.main_split_config)
);
return ruleController;
}
@NonNull
@Override
public List<Class<? extends Initializer<?>>> dependencies() {
return Collections.emptyList();
}
}
为规则定义创建 content provider。
将 androidx.startup.InitializationProvider 作为 添加到应用清单文件中。添加对 RuleController 初始化程序实现 SplitInitializer 的引用:
<!-- AndroidManifest.xml -->
<provider android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<!-- Make SplitInitializer discoverable by InitializationProvider. -->
<meta-data android:name="${applicationId}.SplitInitializer"
android:value="androidx.startup" />
</provider>
InitializationProvider 会在调用应用的 onCreate() 方法之前发现并初始化 SplitInitializer。因此,分屏规则会在应用的主要 activity 启动时生效。
WindowManager API
您可以通过一些 API 调用程序化地实现 activity 嵌入。在 Application 的子类的 onCreate() 方法中进行调用,确保这些规则在任何 activity 启动之前生效。
注意:对于简单实现,您可以将规则添加到应用主要 activity 或分屏主要 activity 的 onCreate() 方法的 RuleController 中。不过,深层链接以及各种应用销毁和重新创建用例可以绕过以这种方式实现的初始化。为了在所有用例中实现可靠的初始化,请将规则添加到 Application 子类中的 RuleController(如果您使用的是 XML 配置文件,则添加到 Jetpack Startup 初始化程序,详情请参阅 XML 配置)。
如需程序化地创建 activity 分屏,请执行以下操作:
创建分屏规则:
创建一个 SplitPairFilter,用于标识共享分屏的 activity:
Kotlin
val splitPairFilter = SplitPairFilter(
ComponentName(this, ListActivity::class.java),
ComponentName(this, DetailActivity::class.java),
null
)
Java
SplitPairFilter splitPairFilter = new SplitPairFilter(
new ComponentName(this, ListActivity.class),
new ComponentName(this, DetailActivity.class),
null
);
将过滤条件添加到过滤条件集:
Kotlin
val filterSet = setOf(splitPairFilter)
Java
Set<SplitPairFilter> filterSet = new HashSet<>();
filterSet.add(splitPairFilter);
为分屏创建布局属性:
Kotlin
val splitAttributes: SplitAttributes = SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.ratio(0.33f))
.setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
.build()
Java
final SplitAttributes splitAttributes = new SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.ratio(0.33f))
.setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
.build();
SplitAttributes.Builder 会创建一个包含布局属性的对象:
setSplitType:定义将可用显示区域分配给每个 activity 容器的方式。宽高比分屏类型指定分配给主要容器的可用显示区域比例;辅助容器则会占据剩余的可用显示区域。
setLayoutDirection:指定 activity 容器相对于另一种容器的布局方式,主要容器优先。
构建 SplitPairRule:
Kotlin
val splitPairRule = SplitPairRule.Builder(filterSet)
.setDefaultSplitAttributes(splitAttributes)
.setMinWidthDp(840)
.setMinSmallestWidthDp(600)
.setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ratio(1.5f))
.setFinishPrimaryWithSecondary(SplitRule.FinishBehavior.NEVER)
.setFinishSecondaryWithPrimary(SplitRule.FinishBehavior.ALWAYS)
.setClearTop(false)
.build()
Java
SplitPairRule splitPairRule = new SplitPairRule.Builder(filterSet)
.setDefaultSplitAttributes(splitAttributes)
.setMinWidthDp(840)
.setMinSmallestWidthDp(600)
.setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ratio(1.5f))
.setFinishPrimaryWithSecondary(SplitRule.FinishBehavior.NEVER)
.setFinishSecondaryWithPrimary(SplitRule.FinishBehavior.ALWAYS)
.setClearTop(false)
.build();
SplitPairRule.Builder 会创建并配置规则:
filterSet:包含分屏对过滤条件,通过确定共享分屏的 activity 以确定何时应用规则。
setDefaultSplitAttributes:将布局属性应用于规则。
setMinWidthDp:设置支持分屏的最小显示宽度(以密度无关像素 dp 为单位)。
setMinSmallestWidthDp:设置支持分屏的最小值(以 dp 为单位),无论设备显示方向如何,都必须确保两个显示屏尺寸中较小的尺寸不低于该值才允许分屏。
setMaxAspectRatioInPortrait:设置在纵向模式下显示 activity 分屏的最大宽高比(高度:宽度)。如果纵向模式显示屏的宽高比超过最大宽高比,则无论显示屏的宽度如何,都会停用分屏。注意:默认值为 1.4,这会使 activity 在大多数平板电脑上以纵向模式占据整个任务窗口。另请参阅 SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT 和 setMaxAspectRatioInLandscape。横向的默认值为 ALWAYS_ALLOW。
setFinishPrimaryWithSecondary:设置结束辅助容器中的所有 activity 会对主要容器中的 activity 有何影响。NEVER 表示在辅助容器中的所有 activity 均结束时,系统不应结束主要 activity(请参阅结束 activity)。
setFinishSecondaryWithPrimary:设置结束主要容器中的所有 activity 会对辅助容器中的 activity 有何影响。ALWAYS 表示当主要容器中的所有 activity 均结束时,系统应始终结束辅助容器中的 activity(请参阅结束 activity)。
setClearTop:指定在辅助容器中启动新 activity 时,是否结束该容器中的所有 activity。false 表示新 activity 会堆叠在辅助容器中已有的 activity 之上。
获取 WindowManager RuleController 的单例实例并添加规则:
Kotlin
val ruleController = RuleController.getInstance(this)
ruleController.addRule(splitPairRule)
Java
RuleController ruleController = RuleController.getInstance(this);
ruleController.addRule(splitPairRule);
当内容不可用时,为辅助容器创建占位符:
创建一个 ActivityFilter,用于标识哪个 activity 会与占位符共享任务窗口分屏:
Kotlin
val placeholderActivityFilter = ActivityFilter(
ComponentName(this, ListActivity::class.java),
null
)
Java
ActivityFilter placeholderActivityFilter = new ActivityFilter(
new ComponentName(this, ListActivity.class),
null
);
将过滤条件添加到过滤条件集:
Kotlin
val placeholderActivityFilterSet = setOf(placeholderActivityFilter)
Java
Set<ActivityFilter> placeholderActivityFilterSet = new HashSet<>();
placeholderActivityFilterSet.add(placeholderActivityFilter);
创建 SplitPlaceholderRule:
Kotlin
val splitPlaceholderRule = SplitPlaceholderRule.Builder(
placeholderActivityFilterSet,
Intent(context, PlaceholderActivity::class.java)
).setDefaultSplitAttributes(splitAttributes)
.setMinWidthDp(840)
.setMinSmallestWidthDp(600)
.setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ratio(1.5f))
.setFinishPrimaryWithPlaceholder(SplitRule.FinishBehavior.ALWAYS)
.setSticky(false)
.build()
Java
SplitPlaceholderRule splitPlaceholderRule = new SplitPlaceholderRule.Builder(
placeholderActivityFilterSet,
new Intent(context, PlaceholderActivity.class)
).setDefaultSplitAttributes(splitAttributes)
.setMinWidthDp(840)
.setMinSmallestWidthDp(600)
.setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ratio(1.5f))
.setFinishPrimaryWithPlaceholder(SplitRule.FinishBehavior.ALWAYS)
.setSticky(false)
.build();
SplitPlaceholderRule.Builder 会创建并配置规则:
placeholderActivityFilterSet:包含 activity 过滤条件,通过确定与占位符 activity 关联的 activity 来确定应用规则的时机。
Intent:指定占位符 activity 的启动。
setDefaultSplitAttributes:将布局属性应用于规则。
setMinWidthDp:设置允许分屏的最小显示宽度(以密度无关像素 dp 为单位)。
setMinSmallestWidthDp:设置允许分屏的最小值(以 dp 为单位),无论设备显示方向如何,都必须确保两个显示屏尺寸中较小的尺寸不低于该值才允许分屏。
setMaxAspectRatioInPortrait:设置在纵向模式下显示 activity 分屏的最大宽高比(高度:宽度)。注意:默认值为 1.4,这会使 activity 在大多数平板电脑上以纵向模式填满任务窗口。另请参阅 SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT 和 setMaxAspectRatioInLandscape。横向的默认值为 ALWAYS_ALLOW。
setFinishPrimaryWithPlaceholder:设置结束占位符 activity 会对主要容器中的 activity 有何影响。ALWAYS 表示在占位符结束时,系统应始终结束主要容器中的 activity(请参阅结束 activity)。
setSticky:用于确定当占位符首次出现在具有足够最小宽度的分屏后,占位符 activity 是否要显示在小显示屏上的 activity 堆栈顶部。
向 WindowManager RuleController 添加规则:
Kotlin
ruleController.addRule(splitPlaceholderRule)
Java
ruleController.addRule(splitPlaceholderRule);
注意:RuleController 已针对上一条规则进行实例化。
指定绝不应属于分屏的 activity:
创建一个 ActivityFilter,用于标识始终应占据整个任务显示区域的 activity:
Kotlin
val expandedActivityFilter = ActivityFilter(
ComponentName(this, ExpandedActivity::class.java),
null
)
Java
ActivityFilter expandedActivityFilter = new ActivityFilter(
new ComponentName(this, ExpandedActivity.class),
null
);
将过滤条件添加到过滤条件集:
Kotlin
val expandedActivityFilterSet = setOf(expandedActivityFilter)
Java
Set<ActivityFilter> expandedActivityFilterSet = new HashSet<>();
expandedActivityFilterSet.add(expandedActivityFilter);
创建 ActivityRule:
Kotlin
val activityRule = ActivityRule.Builder(expandedActivityFilterSet)
.setAlwaysExpand(true)
.build()
Java
ActivityRule activityRule = new ActivityRule.Builder(
expandedActivityFilterSet
).setAlwaysExpand(true)
.build();
ActivityRule.Builder 会创建并配置规则:
expandedActivityFilterSet:包含 activity 过滤条件,通过确定您希望从分屏中排除的 activity,以确定何时应用规则。
setAlwaysExpand:指定 activity 是否应填充整个任务窗口。
向 WindowManager RuleController 添加规则:
Kotlin
ruleController.addRule(activityRule)
Java
ruleController.addRule(activityRule);
注意:RuleController 之前已实例化。