一、JetPack架构
Android Jetpack 是Android官方提供的一套组件、工具和指导,可以帮助开发者摆脱编写样板代码并简化复杂任务,并且Jetpack组件提供向后的兼容性, 能够帮助开发者更快的开发更稳定且易维护的应用。
Jetpack大体分为4类:Architecture(架构)、Foundationy(基础)、Behavior(行为)、UI(界面)
二、WindowManager
此WindowManager并非系统 的那个WMS,此WindowManager是 Jetpack 的新成员,是Jetpack UI的一部分。它可以帮助我们适配日益增多的可折叠设备,满足多窗口环境下的开发需求。 可折叠设备通常分为两类:单屏可折叠设备(一个整体的柔性屏幕)和双屏可折叠设备(两个屏幕由合页相连)。
2.1 获取折叠屏状态
多屏设备下,一个窗口可能会跨越物理屏幕显示,这样窗口中会出现铰链等不连续部分,FoldingFeature (DisplayFeature 的子类)对铰链这类的物理部件进行抽象,从中可以获取铰链在窗口中的准确位置,帮助我们避免将关键交互按钮布局在其中。另外 FoldingFeature 还提供了可以感知当前折叠状态的 API,我们可以根据这些状态改变应用的布局。
//铰链处于半开状态且位置水平,适合切换到平板模式
fun isTableTopMode(foldFeature: FoldingFeature) =
foldFeature.isSeparating &&
foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
//铰链处于半开状态且位置垂直,适合切换到阅读模式
fun isBookMode(foldFeature: FoldingFeature) =
foldFeature.isSeparating &&
foldFeature.orientation == FoldingFeature.Orientation.VERTICAL
而WindowManager也允许我们通过 Flow 持续观察显示特性的变化。
lifecycleScope.launch(Dispatchers.Main) {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
WindowInfoTracker.getOrCreate(this@SampleActivity)
.windowLayoutInfo(this@SampleActivity)
.collect { newLayoutInfo ->
// Use newLayoutInfo to update the layout.
}
}
}
当显示特性变化时,我们能获取 newLayoutInfo的信息,它是一个 WindowLayoutInfo 类型,内部持有了 FoldingFeature 信息。
2.2 感知窗口大小变化
应用窗口可能跟随设备配置变化时(例如折叠屏的展开、旋转,或窗口在多窗口模式下调整大小)发生变化,我们可以通过 WIndowManger 的 WindowMetrics 获取窗口大小,我们有两种获取当前 WindowMetrics 的方式,同步获取和异步监听:
//异步监听
lifecycleScope.launch(Dispatchers.Main) {
windowInfoRepository().currentWindowMetrics.flowWithLifecycle(lifecycle)
.collect { windowMetrics: WindowMetrics ->
val currentBounds = windowMetrics.bounds
val width = currentBounds.width()
val height = currentBounds.height()
}
}
//同步获取
val windowMetrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity)
val currentBounds = windowMetrics.bounds
val width = currentBounds.width()
val height = currentBounds.height()
三、快速上手
3.1 添加依赖
使用之前需要先添加依赖,将WindowManager库依赖项添加到应用的build.gradle文件中:
implementation "androidx.window:window:$window_version"
3.2 配置分屏
3.2.1 XML静态配置
首先,在res资源目录的xml文件夹下,新建一个main_split_config.xml文件,添加如下内容:
<?xml version="1.0" encoding="utf-8"?>
<resources
xmlns:window="http://schemas.android.com/apk/res-auto">
<SplitPairRule
window:splitRatio="0.3"
window:splitMinWidth="600dp"
window:finishPrimaryWithSecondary="always"
window:finishSecondaryWithPrimary="adjacent">
<SplitPairFilter
window:primaryActivityName="com.example.windowmanagersample.embedding.SplitActivityList"
window:secondaryActivityName="com.example.windowmanagersample.embedding.SplitActivityDetail"/>
</SplitPairRule>
<SplitPlaceholderRule
window:placeholderActivityName="com.example.windowmanagersample.embedding.SplitActivityListPlaceholder"
window:splitRatio="0.3"
window:splitMinWidth="600dp"
window:stickyPlaceholder="true"
window:finishPrimaryWithSecondary="adjacent">
<ActivityFilter
window:activityName="com.example.windowmanagersample.embedding.SplitActivityList"/>
</SplitPlaceholderRule>
</resources>
3.2.2 代码动态配置
运行时定义分屏的配置,开发者可以在startActivity或者onCreate时使用。
splitController.registerRule(new SplitPairRule(newFilters));
splitController.unRegisterRule(new SplitPairRule(newFilters));
下面是动态配置分屏的代码:
protected void onCreate(@Nullable Bundle savedInstanceState) {
Set<SplitPairFilter> pairFilters = new HashSet<>();
SplitPairFilter filter = new SplitPairFilter(primaryActivityComponetName,
secondaryActivityComponetName,
null);
pairFilters.add(filter);
SplitPairRule pairRule = new SplitPairRule(pairFilters,
SplitRule.FINISH_ADJACENT,
SplitRule.FINISH_ALWAYS,
true,
600,
600,
0.3f,
LayoutDirection.LOCALE);
SplitController splitController = SplitController.getInstance();
splitController.registerRule(pairRule);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
3.3 将规则定义通知库
使用Jetpack Startup库在加载应用的其他组件和启动 activity 之前执行初始化。如需开启启动功能,请在应用的build文件中添加库依赖项:
implementation("androidx.startup:startup-runtime:1.1.0")
然后,在manifest文件中添加配置:
<provider android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data android:name="com.example.windowmanagersample.embedding.ExampleWindowInitializer"
android:value="androidx.startup" />
</provider>
3.4 添加初始化程序类实现
接下来,将包含定义main_split_config.xml 资源文件的 ID 提供给 SplitController.initialize() 来设置规则。
@OptIn(ExperimentalWindowApi::class)
class ExampleWindowInitializer : Initializer<SplitController> {
override fun create(context: Context): SplitController {
SplitController.initialize(context, R.xml.main_split_config)
return SplitController.getInstance()
}
override fun dependencies(): List<Class<out Initializer<*>>> {
return emptyList()
}
}
3.5 XML配置文件参数解析
以下是xml静态配置的参数说明:
- SplitPairRule:分屏配对情景(两侧容器均有具体activity),对应SplitPairFilter。
- splitRatio:分屏比默认为0.5f,即左右5:5分屏 ,对于IM类应用,可考虑设置分屏比为0.3f,即左右3:7分屏。
- splitMinWidth:默认配置600dp,宽度达到600dp才可以分屏,主窗口可分屏显示的最小窗口宽度。
- splitMinSmallestWidth:默认配置600dp,主窗口可分屏显示的最小sw值。
- finishPrimaryWithSecondary:默认为 false,true:若secondary container中所有activity都finish,则primary container中创建分屏的activity也会finish,不推荐应用主动配置此项。(androidx.window:window:1.1.0-alpha03中默认是SplitRule.FINISH_ADJACENT)
- finishSecondaryWithPrimary 默认为 ture, true:若primary container中所有activity都finish,则secondary container中所有activity也会finish,不推荐应用主动配置此项。(androidx.window:window:1.1.0-alpha03中默认是SplitRule.FINISH_ALWAYS)
- clearTop:默认为 false,true:启动activity窗口分屏,存在相同的primary container,若新建secondary container,则原secondary container中的activity会被finish掉,推荐应用配置,避免右分屏出现多实例。
SplitPairFilter,主要用来设置分屏配对关系,节点的配置如下:
- primaryActivityName:分屏的primay activity component name。
- secondaryActivityName:分屏的secondary activity component name。
- secondaryActivityAction:分屏的secondary activity 启动的action (配置此项需要在启动的时候添加action)。
SplitPlaceholderRule,用来描述分屏下的占位,对应ActivityFilter。对应的配置如下:
- placeholderActivityIntentName:通过同时打开两个activity创建分屏,secondary占位activity component name。
- componentName:组件名
- intentAction:intent
- splitRatio:分屏比默认为0.5f,即左右5:5分屏
- splitMinWidth:默认配置600dp,宽度达到600dp才可以分屏主窗口可分屏显示的最小窗口宽度。
- splitMinSmallestWidth:默认配置600dp,主窗口可分屏显示的最小sw值。
ActivityRule,需要全屏显示的activity,对应ActivityFilter。
- alwaysExpand:取值”true”或false true:启动的activity全屏显示。
- componentName:组件名
- intentAction:intent
3.6 应用适配生效
3.6.1 MIUI meta-data标记
AndroidManifest.xml中声明下列meta-data字段。设置为true表示app已经主动适配,系统不会反向适配。
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
<meta-data android:name="embedded" android:value="true"/>
</application>
3.6.2 Google property标记
Google官方添加property字段,用来标识APP是否允许系统反向适配Activity Embedding。其中,true表示允许系统反向适配,false不允许系统反向适配。需要注意的是:自适配Activity Embedding和不允许系统反向适配Activity Embedding的APP需要设置为false。
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<property
android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE"
android:value="false" />
</applicatio
四、Android手机分屏
Android N 支持多窗口模式,或者叫分屏模式,即在屏幕上可以同时显示多个窗口。多窗口模式允许多个应用同时共享同一屏幕。在这种模式下,系统可以左右或上下并排显示两个应用(分屏模式),在应用中用小窗口叠加显示其他应用(画中画模式),或者让各个应用分别在可移动且可调整大小的窗口中显示(自由窗口模式)。
在分屏模式下,各个窗口的应用都可以正常运行,但是只能有一个窗口获得焦点,而另外的窗口则属于暂停状态。Android用户可以通过步骤打开手机分屏模式:
- 用户打开 Overview 屏幕并长按 Activity 标题,可以拖动该 Activity 至屏幕突出显示的区域,使 Activity 进入多窗口模式。
- 用户长按 Overview 按钮,设备上当前的 Activity 将进入多窗口模式,同时将打开 Overview 屏幕,用户可在该屏幕中选择要共享屏幕的另一个 Activity。
默认情况下,同一个应用的多个 Activity 会共用同一个窗口,且无法分配到不同窗口中。若希望同一个应用的不同窗体可以被分配到不同窗口中,需要在启动新窗体时给 Intent 设置一个 FLAG_ACTIVITY_LAUNCH_ADJACENT 标志,这样新 Activity 就会在新的栈中被启动,独立于原来的 Activity,进而实现两个 Activity 被放置于不同的窗口中。
val intent = Intent(this@WindowDemosActivity, SecondActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT
startActivity(intent)
五、适配指南
5.1 应用横竖屏
5.1.1 PAD 竖屏下进入ActivityEmbedding
平板设备处于竖屏状态时,我们推荐应用采用左下图的全屏显示样式,而不是进入ActivityEmbedding。竖屏下的左右分屏显示会导致信息量过多,难以聚焦。如果您的应用在竖屏下进入了ActivityEmbedding,原因是:应用设置的进入ActivityEmbedding的阈值splitMinWidth过小平板设备处于竖屏状态时,我们推荐应用采用左下图的全屏显示样式,而不是进入ActivityEmbedding。竖屏下的左右分屏显示会导致信息量过多,难以聚焦。如果您的应用在竖屏下进入了ActivityEmbedding,原因是:应用设置的进入ActivityEmbedding的阈值splitMinWidth过小。
获取屏幕大小
R版本之前:
Display.getRealSize()、Display.getRealMetrics();
R版本之后:
WindowManager.getMaximumWindowMetrics();
Jetpack WindowManager中支持使用
WindowMetrics maximumWindowMetrics = WindowMetricsCalculator.
getOrCreate().computeMaximumWindowMetrics(activity);
获取当前窗口大小
R版本之前:
Display.getSize()
R版本之后:
WindowManager.getCurrentWindowMetrics()
Jetpack WindowManager中支持使用
WindowMetrics maximumWindowMetrics = WindowMetricsCalculator.
getOrCreate(). computeCurrentWindowMetrics(activity);
5.1.2 折叠屏控制在外屏显示
为了避免您适配ActivityEmbedding后在折叠屏外屏出现如下图所示的情况,需要控制应用在折叠屏外屏竖屏显示,监听 WindowInfoTracker 来手动设置显示方向。
具体内容可以参考:developer.android.com/guide/topic…
5.1.3 支持分屏
应用需要在 AndroidManifest.xml 文件的 application 或者 actvivity 标签中添加 resizeableActivity=true 的属性。若配置android:resizeableActivity=“false”,会导致无法分屏显示,默认是false。
<application
android:resizeableActivity="true">
<activity
android:resizeableActivity="true" />
</application>
5.1.4 视频分屏
有时候,分屏时请求横屏还是在分屏下显示,无法横屏全屏显示,建议配置activity 始终填满任务窗口。
<ActivityRule
window:alwaysExpand="true">
<ActivityFilter
window:activityName=".FullActivity"/>
</ActivityRule>
5.1.5 clearTop
启动activity窗口分屏,存在相同的primary container,会创建新的secondary container,原来的secondary container中的activity会被finish掉,避免右分屏出现多实例。开发者可以根据需要对单独的SplitPairRule 进行配置clearTop="true"。
用户折叠手机时,屏幕 B 在屏幕 A 之上,屏幕 A 又在菜单之上。当用户从屏幕 B 进行返回导航时,系统会显示屏幕 A 而不是Menu。可以将分屏配置为clearTop来清除之前的辅助容器,并正常启动新的 activity。
<SplitPairRule
window:clearTop="true">
<SplitPairFilter
window:primaryActivityName=".Menu"
window:secondaryActivityName=".ScreenA"/>
<SplitPairFilter
window:primaryActivityName=".Menu"
window:secondaryActivityName=".ScreenB"/>
</SplitPairRule>
5.1.6 多进程规则
问题:同一Task任务下,子进程配置activity成全屏不生效。
采用Jetpack Startup,如果不指定android:process时,默认在主进程初始化SplitController,在xml文件夹中的分屏规则不会在子进程生效。
<provider android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<!-- This entry makes ExampleWindowInitializer discoverable. -->
<meta-data android:name="**androidx.window.sample.embedding.ExampleWindowInitializer**"
android:value="androidx.startup" />
</provider>
目前,解决方案是重写Application在onCreate方法中对需要规则匹配的进程初始化SplitController,并且加载静态规则。
public class MyApplication extends Application {
@Override
public void onCreate() {
SplitController.initialize(getApplicationContext(), R.xml.split_config);
super.onCreate();
}
}
5.1.7 判断是否在左右分屏
左右分屏下,activity中的configuration的mode是multi-window;全屏状态下,activity中的configuration的mode是fullscreen。
判断当前activity是否左右分屏,可以通过SplitController.getInstance().isActivityEmbedded(Activity activity)进行区分,返回true表示处于分屏。
5.1.8 响应分屏状态
为了知道 activity 何时在分屏状态,可以向SplitController注册一个监听器来监听分屏状态的变化,然后相应地调整显示界面。
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
splitController
.addSplitListener(this, mainThreadExecutor, SplitInfoChangeCallback());
}
class SplitInfoChangeCallback extends Consumer<List<SplitInfo>> {
public void accept(List<SplitInfo> splitInfoList) {
findViewById<View>(R.id.infoButton).visibility =
!splitInfoList.isEmpty()) ? View.GONE : View.VISIBLE;
}
}
5.1.9 占位 Placeholder
如需创建带有占位符的分屏,请创建一个占位符并将其与主要 activity 相关联:
<SplitPlaceholderRule
window:placeholderIntentName=".Placeholder">
<ActivityFilter
window:activityName=".Main"/>
</SplitPlaceholderRule>