JetPack WindowManager详解

1,992 阅读6分钟

一、JetPack架构

Android Jetpack 是Android官方提供的一套组件、工具和指导,可以帮助开发者摆脱编写样板代码并简化复杂任务,并且Jetpack组件提供向后的兼容性, 能够帮助开发者更快的开发更稳定且易维护的应用。

Jetpack大体分为4类:Architecture(架构)、Foundationy(基础)、Behavior(行为)、UI(界面)

image.png

二、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 支持多窗口模式,或者叫分屏模式,即在屏幕上可以同时显示多个窗口。多窗口模式允许多个应用同时共享同一屏幕。在这种模式下,系统可以左右或上下并排显示两个应用(分屏模式),在应用中用小窗口叠加显示其他应用(画中画模式),或者让各个应用分别在可移动且可调整大小的窗口中显示(自由窗口模式)。

image.png 在分屏模式下,各个窗口的应用都可以正常运行,但是只能有一个窗口获得焦点,而另外的窗口则属于暂停状态。Android用户可以通过步骤打开手机分屏模式:

  1. 用户打开 Overview 屏幕并长按 Activity 标题,可以拖动该 Activity 至屏幕突出显示的区域,使 Activity 进入多窗口模式。
  2. 用户长按 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过小。

image.png

获取屏幕大小

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 来手动设置显示方向。

image.png

具体内容可以参考: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"。

image.png

用户折叠手机时,屏幕 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成全屏不生效。

image.png

采用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>

image.png