2023 Android 折叠屏适配详解,是时候点亮新技能了

自 2019 年三星发布了第一台(柔宇不算) Galaxy Z Fold 之后,Android 厂商们都陆续跟进了各自的可折叠方案,之后折叠屏手机市场一直保持快速增长,例如 2023 年上半年整体销量 227 万台,同比增长 102.0%。

虽然对比上半年手机总体出货量 1.3 亿台只能算是零头,但是不可否认,如今开发者的 App 遇到可折叠手机的概率并不低,特别这部分用户大概率还属于「高产值」用户。

所以 2023 年开始,折叠屏适配也逐步开始成为 Android 的主流 KPI 之一,那么不适配的话会怎么样?适配的话又是通过什么方式?本篇将带你深入了解这个话题。

⚠️本文超长,可收藏以备不时之需。

Letterboxing 模式

首先,如果不适配的话,你的应用大概率(不一定)会是 Letterboxing 模式的显示方式,可能你会看到 App 以如下图所示的方式存在,也就是当应用的宽高比和屏幕比例不兼容时,App 可能会以 Letterbox 模式打开

一般是 App 锁死旋转方向和采用了不可调整大小。

当然,是否进入 Letterboxing 模式和 TargetSDK 版本、 App 配置和屏幕分辨率都有关系,并且不同 OS 版本上 Letterboxing 模式的呈现方式也可能有所不同,例如:

  • Android 12(API 31)开始引入了 Letterboxing 增强功能,可由手机厂家配置支持:

    • 圆角: 窗口支持圆角
    • 系统栏透明度:覆盖 App 的状态栏和导航栏支持半透明
    • 可配置的宽高比:可以调整 App 的宽高比改善应用的外观
  • 12L(API 32)添加了:

    • 可配置位置:在大屏幕上,设备厂商可以将应用配置在显示屏的左侧或右侧。
    • 重启按钮:设备厂商可以为尺寸兼容模式的重启按钮赋予新的外观。(尺寸兼容模式可以让 App 的宽或者高尽可能充满屏幕)

    当系统确定可以通过重新缩放应用以填充显示窗口来改进 Letterboxing 的显示时,Android 会将 App 置于尺寸兼容模式,这时候系统显示一个重启控件,确定后会重新创建 App 进程、重新创建 Activity 并重绘进行适配。

  • Android 13(API 33)添加了一个用户引导的提示对话框 :

那么什么时候会进入 Letterboxing 模式 ?一般可以简单理解为:

  • android:resizeableActivity=false 下应用声明的宽高比与容器不兼容时(例如屏幕宽度超过 android:maxAspectRatio )。
  • setIgnoreOrientationRequest(true) 下系统设置忽略屏幕方向后,横向打开强制竖屏的界面。

这里的核心点其实是 resizeableActivity ,它用于声明系统是否可以调节 App 大小去适应不同尺寸的屏幕, 其实严格来说 resizeableActivity 不一定会导致应用一定进入 Letterboxing 模式,这也 API 版本有关系:

  • 在 Android 7.0(API 24)引入了分屏模式配置 resizeableActivity
  • 在 Android 11(API 30)及更低版本上,用于配置 App 是否支持多窗口模式,如果 false 就不支持,会进入 Letterboxing 模式。
  • 在 Android 12(API 31)及更高版本上,无论 resizeableActivity 设置什么,App 都会支持大屏幕 (sw >= 600dp) 上的多窗口模式,所以仅用于指定 App 是否支持小屏幕(sw < 600dp)上的多窗口模式。

sw >= 600dp 可以简单理解为你的屏幕的绝对宽度大于 600dp

那有的人就说了,如果我在 Android 12 就使用 android:resizeableActivity=false 然后什么都不适配会怎么样?我只能说,「有一定概率」会如下图所示一样,直接 crash

那是不是我不使用高版本的 TargetSDK 就可以不用工作适配了呢?

也不完全是,至少你需要对你的 App 或者 Activity 进行一些简单的配置,因为早在 Android 7.0(API 24)开始,resizeableActivity 的默认值就被改为 true

所以如果你不想适配大屏模式 UI,希望进入 Letterboxing 模式,还是需要手动在 AndroidManifest 中的 application 或对应的 Activity 配置上 android:resizeableActivity="false"

另外,Letterboxing 模式的显示模式和 maxAspectRatio 也有关,当屏幕比例超过 maxAspectRatio 时才会用黑边填充,一般官方建议把 maxAspectRatio 设为 2.4 (12 : 5),配置方式也和 API Level 有关系:

  • Android 8.0 及以上可以通过 android:maxAspectRatio 配置

    <activity android:name=".MainActivity"
              android:maxAspectRatio="2.4" /> 
    
  • Android 8.0 以下可以通过 meta-data android.max_aspect 配置

    <meta-data android:name="android.max_aspect" android:value="2.4" /> 
    

PS :如果 resizeableActivity 是true, maxAspectRatio 会不生效。

如图是前面提到 Android 12L(API 32)的重启按钮可以让 App 一端尽可能适配屏幕减少黑边。

还有一点,在折叠屏展开和闭合的时候,在屏幕发生了变化时,系统可能会销毁并重新创建整个 Activity ,所以我们需要配置 android:configChanges 来防止重启

android:configChanges="screenLayout|smallestScreenSize|screenSize"

最后还需要注意 supports_size_changes ,如果不想支持多窗口模式,但是又可能会因为系统强迫进入多窗口模式,然后又不希望每次都被重启,那么可以配置 supports_size_changes 来保证运行的连续性。

<meta-data
    android:name="android.supports_size_changes" android:value="true" />

所以这里简单做个总结就是:

  • 当应用的宽高比与其屏幕比例不兼容,App 锁死旋转方向和大小时会进入 Letterboxing 模式

  • resizeableActivity 的效果主要看 TargetSDK 版本, Android 12(API 31)及更高版本上可能还是会进去分屏模式

  • maxAspectRatio 的作用主要看 resizeableActivity

  • 配置 android:configChangessupports_size_changes 防止重启 Activity 保证连续性

官方适配支持

接下来就是介绍适配方案,首先我们看这张图,其实官方已经根据使用场景为我们定义好使用建议,其中关键的几个信息有:

  • Compose
  • Activity Embedding
  • SlidingPaneLayout

另外,在官方的不同屏幕尺寸匹配里设定了窗户尺寸等级规范,例如:

  • Compact: 普通手机设备,宽度 < 600dp
  • Medium:折叠屏或平板的竖屏,600dp < 宽度 < 840dp
  • Expanded:展开屏幕,平板或平板电脑等,宽度 > 840dp

当然还有基于高度去判断的,但是大多数 App 可以通过仅考虑宽度窗口大小类别来构建响应式 UI

Compose

其实 Compose 不必多说,在折叠屏适配上响应式布局本身就具有先天优势,配合 Jetpack WindowManager API 提供的当前的屏幕参数,就可以很灵活地达到适配不同 UI 效果。

例如 Compose 可以使用 material3-window-size-class 库,然后利用 calculateWindowSizeClass() 计算当前窗口的 WindowSizeClass ,从而改变 UI 的布局:

import androidx.activity.compose.setContent
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass

class MyActivity : ComponentActivity() {
    
    
    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContent {
    
    
            // Calculate the window size class for the activity's current window. If the window
            // size changes, for example when the device is rotated, the value returned by
            // calculateSizeClass will also change.
            val windowSizeClass = calculateWindowSizeClass(this)
            // Perform logic on the window size class to decide whether to use a nav rail.
            val useNavRail = windowSizeClass.widthSizeClass > WindowWidthSizeClass.Compact

            // MyScreen knows nothing about window size classes, and performs logic based on a
            // Boolean flag.
            MyScreen(useNavRail = useNavRail)
        }
    }
}

另外还可以通过 com.google.accompanist:accompanist-adaptiveTwoPane 进行适配

TwoPane 提供了两个固定的槽位,两个槽位的默认位置由 TwoPaneStrategy 驱动,它可以决定将两个槽位水平或垂直排列,并可配置它们之间的间隔。

更多可见:https://github.com/google/accompanist/tree/3810fe1182cf52c6660787ae3226dfb7f5ad372a/sample/src/main/java/com/google/accompanist/sample/adaptive

不同场景 Compose 还可以使用 FlowLayout 适配折叠变化 ,FlowLayout 包含 FlowRowFlowColumn ,当一行(或一列)放不下里边的内容时,会自动换行,这在折叠屏展开和收缩场景也非常实用。

关于 Compose 适配折叠屏 Demo 还可以参考 : https://github.com/android/compose-samples/tree/main/JetNews

Activity Embedding

Activity Embedding 就是通过在两个 Activity 或同一 Activity 的两个实例之间拆分窗口,来优化大屏幕的支持。

理论上 Activity Embedding 不需要代码重构,可以通过创建 XML 配置文件或进行 Jetpack WindowManager API 调用来确定 App 如何显示其 Activity(并排或堆叠)

Activity Embedding 默认会自动维护对小屏幕的支持,当应用位于小屏幕设备上时,Activity 会一个一个地堆叠在另一个之上;在大屏幕上,Activity 会展开并排显示。

在这个基础上,它可以适应设备方向的变化,并在可折叠设备上无缝工作,在设备折叠或展开时堆叠被拆开的 Activity,例如在聊天列表和聊天详情页面进行拆分和堆叠。

无论是 Android 12L(API 32)以上的大屏设备,还是更早期折叠屏平台版本的设备,Jetpack WindowManager 都能帮助构建 Activity Embedding 多窗格布局,这种基于多个 Activity 而非 fragment 或基于视图的布局(如 SlidingPaneLayout)的方式可以最简单提供大屏幕用户体验而无需重构源代码

一个常见的示例是列表-详情分屏,为了确保高质量的呈现,系统先启动列表 Activity,然后应用立即启动详情 Activity,过渡系统等到这两个 Activity 都绘制完成后再将它们一起显示出来,对用户来说,这两个 Activity 是作为一个页面启动。

目前大多数运行 Android 12L(API 32)及更高版本的大屏幕设备都支持 Activity Embedding。

使用 Jetpack WindowManager 管理和配置 Activity Embedding 其实相当灵活,可以预先配置 XML 规则,或者直接通过 API 进行管理配置,对于 XML 配置文件中定义的规则,设置以下属性:

  • splitRatio:设置容器比例。该值为开区间 (0.0, 1.0) 内的浮点数。
  • splitLayoutDirection:指定分割容器相对于彼此的布局方式。值包括:
    • ltr: 左到右
    • rtl: 右到左
    • localeltr rtl 由语言环境设置决定

可以看到 Jetpack WindowManager 十分丰富且灵活的配置支持,而不是单纯简单的对 Activity 进行平均分割,甚至你还可以配置一个空白 Placeholder 来进行占位显示。

使用 Activity Embedding 你需要依赖 implementation 'androidx.window:window:xxx' ,然后将该 android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED 属性添加到应用清单文件的 <application> 中,并将值设置为 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>

之后就可以通过 xml 创建各种 Split Rule 或者 WindowManager API 创建 Split Rule 然后调用。

<!-- 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>

更多可见:https://developer.android.com/guide/topics/large-screens/activity-embedding

SlidingPaneLayout

SlidingPaneLayout 支持在大屏幕设备并排显示两个窗格,同时还会自动进行调整,在手机等小屏幕设备只显示一个窗格,所以在可折叠场景下也十分实用。

SlidingPaneLayout 会根据两个窗格的宽度来确定是否并排显示这些窗格,例如:

  • 如果测量后发现列表窗格的最小尺寸为 200dp,而详细信息窗格需要 400dp,那么只要可用宽度不小于 600dp,SlidingPaneLayout 就会自动并排显示两个窗格
  • 如果子视图的总宽度超过了 SlidingPaneLayout 中的可用宽度,这些视图就会重叠在一起。

如果视图没有重叠,那么 SlidingPaneLayout 支持对子视图使用布局参数 layout_weight,以指定在测量结束后如何划分剩余的空间。

例如这个例子使用了 SlidingPaneLayout,布局将 RecyclerView 作为其左侧窗格,将 FragmentContainerView 作为其主要详细信息视图,用于显示左侧窗格中的内容,其实就类似前面介绍的在 Compose 里使用 TwoPane 的 UI。

<!-- two_pane.xml -->
<androidx.slidingpanelayout.widget.SlidingPaneLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:id="@+id/sliding_pane_layout"
   android:layout_width="match_parent"
   android:layout_height="match_parent">

   <!-- The first child view becomes the left pane. When the combined
        desired width (expressed using android:layout_width) would
        not fit on-screen at once, the right pane is permitted to
        overlap the left. -->
   <androidx.recyclerview.widget.RecyclerView
             android:id="@+id/list_pane"
             android:layout_width="280dp"
             android:layout_height="match_parent"
             android:layout_gravity="start"/>

   <!-- The second child becomes the right (content) pane. In this
        example, android:layout_weight is used to expand this detail pane
        to consume leftover available space when the
        the entire window is wide enough to fit both the left and right pane.-->
   <androidx.fragment.app.FragmentContainerView
       android:id="@+id/detail_container"
       android:layout_width="300dp"
       android:layout_weight="1"
       android:layout_height="match_parent"
       android:background="#ff333333"
       android:name="com.example.SelectAnItemFragment" />
</androidx.slidingpanelayout.widget.SlidingPaneLayout>

另外 SlidingPaneLayout 还可以和 Navigation 配合管理 Fragment 事物,并且它现在还会识别和适应折叠和铰链状态,例如:

使用的设备带有遮挡部分屏幕的铰链,它会自动将 App 的内容放置在任一侧。

SlidingPaneLayout 还引入了锁定模式,支持在窗格重叠时控制滑动行为,例如:

为了防止用户滑到空窗格,需要点击击列表项才能加载有关该窗格的信息,但允许他们滑回到列表,在有空间并排显示两个视图的可折叠设备或平板电脑上,锁定模式将被忽略。

更多可见: https://developer.android.com/guide/topics/ui/layout/twopane?hl=zh-cn

自定义适配

除了官方的适配方案,也许我们还需更灵活的自定义适配方案,那么首先第一件事就是我们需要知道如何识别折叠屏。

识别折叠屏

还是前面提到的 Jetpack WindowManager ,Jetpack WindowManager 的 FoldingFeature 提供了有关可折叠显示器的信息的类型,包括:

  • state:设备的折叠状态,FLAT (完全打开) 或 HALF_OPENED (处于打开和关闭状态之间的中间位置)
  • orientation:折叠或铰链的方向,HORIZONTAL 或者 VERTICAL
  • occlusionType:折叠或铰链是否隐藏了部分显示屏,NONE (不遮挡)或者 FULL (遮挡)
  • isSeparating:折叠或铰链是否创建两个显示区域,true(半开/双屏) 或 false

在 Android 11 官方还提供了读取折叠角度的支持:新增的类型 TYPE_HINGE_ANGLE 支持以及新的 SensorEventSensorEvent 可以监控合页角度,并提供设备的两部分之间的角度测量值:

sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
        hingeAngleSensor = sensorManager?.getDefaultSensor(Sensor.TYPE_HINGE_ANGLE)

而关于折叠屏的姿态,我们可以通过 Jetpack WindowManager 的 API 来实现:

  • 设备处于 TableTop 模式,屏幕半开并且铰链处于水平方向

    fun isTableTopMode(foldFeature: FoldingFeature) =
      foldFeature.isSeparating &&
              foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
    

  • 设备处于 Book 模式,屏幕半开并且铰链处于垂直方向

    fun isBookMode(foldFeature: FoldingFeature) =
      foldFeature.isSeparating &&
              foldFeature.orientation == FoldingFeature.Orientation.VERTICAL
    

例如 Google Duo team 就通过 Jetpack WindowManager 识别折叠屏状态,然后根据展开状态在播放过程调整界面 UI。

简单介绍一下,就是在初始化时通过 WindowManager 库获取 Flow<WindowLayoutInfo> ,让手机知道目前处于桌面模式以及如何获取折叠的位置:

    override fun onStart() {
    
    
        super.onStart()
        initializePlayer()
        layoutUpdatesJob = uiScope.launch {
    
    
            windowInfoRepository.windowLayoutInfo
                .collect {
    
     newLayoutInfo ->
                    onLayoutInfoChanged(newLayoutInfo)
                }
        }
    }

    override fun onStop() {
    
    
        super.onStop()
        layoutUpdatesJob?.cancel()
        releasePlayer()
    }

每次获得新的布局信息时,都可以查询显示功能并检查设备在当前显示中是否有折叠或铰链:

private fun onLayoutInfoChanged(newLayoutInfo: WindowLayoutInfo) {
    
    
        if (newLayoutInfo.displayFeatures.isEmpty()) {
    
    
            // The display doesn't have a display feature, we may be on a secondary,
            // non foldable-screen, or on the main foldable screen but in a split-view.
            centerPlayer()
        } else {
    
    
            newLayoutInfo.displayFeatures.filterIsInstance(FoldingFeature::class.java)
                .firstOrNull {
    
     feature -> isInTabletopMode(feature) }
                ?.let {
    
     foldingFeature ->
                    val fold = foldPosition(binding.root, foldingFeature)
                    foldPlayer(fold)
                } ?: run {
    
    
                centerPlayer()
            }
        }
    }

如果方向为水平且 FoldingFeature.isSeparating() 返回 true,则设备可以在桌面模式下使用,在这种情况下,可以计算折叠的相对位置并将控件移动到对应位置,否则将其移动到 0(屏幕底部)。

    private fun centerPlayer() {
    
    
        ConstraintLayout.getSharedValues().fireNewValue(R.id.fold, 0)
        binding.playerView.useController = true // use embedded controls
    }

    private fun foldPlayer(fold: Int) {
    
    
        ConstraintLayout.getSharedValues().fireNewValue(R.id.fold, fold)
        binding.playerView.useController = false // use custom controls
    }

窗口大小适配

折叠设备的适配里,窗口大小获取也是非常重要的一点,但是其实 Android 发展至今,其中一些 API 已经被弃用,或者说还在被误用,针对大屏幕设配的适配上,因为有 Letterboxing 等情况,所以其实旧的 API 已经无法满足需求。

目前已弃用且经常被误用的 Display API 有:

  • getMetrics()
  • getSize()
  • getRealMetrics()
  • getRealSize()
  • getRectSize()
  • getWidth()
  • getHeight()

经常被误用的 View API 有:

  • getWindowVisibleDisplayFrame()
  • getLocationOnScreen

例如 Display getSize() getMetrics() 在 API 30 中已经被弃用,取而代之的是新 WindowManager方法。

Android 12(API 31)弃用了 DisplaygetRealSize()getRealMetrics() ,更新的还有与之相关的 getMaximumWindowMetrics() 方法。

因为折叠屏和多屏幕下,你的 App 实际尺寸和屏幕实际尺寸之间并不一定一致,所以不能依赖物理显示尺寸来定位 UI 元素,现在推荐依赖于 WindowMetrics 的 API :

  • Platform:
    • getCurrentWindowMetrics()
    • getMaximumWindowMetrics()
  • Jetpack:
    • WindowMetricsCalculator#computeCurrentWindowMetrics()
    • WindowMetricsCalculator#computeMaximumWindowMetrics()

这里的 Platform 是 Android 11(API 30)引入了 WindowManager 方法来提供在多窗口模式下运行的应用的边界:

  • getCurrentWindowMetrics() :返回系统当前窗口状态对象 WindowMetrics
  • getMaximumWindowMetrics() :返回系统的最大窗口状态 WindowMetrics

Jetpack WindowManager 库方法 computeCurrentWindowMetrics()computeMaximumWindowMetrics() 分别提供类似的功能,但向后兼容到 API 14。

val windowMetrics = context.createDisplayContext(display)
                    .createWindowContext(WindowManager.LayoutParams.TYPE_APPLICATION, null)
                    .getSystemService(WindowManager::class.java)
                    .maximumWindowMetrics

所以,通过 WindowManager ,我们可以动态去管理窗口的大小变化,识别折叠屏的变化状体,例如在onConfigurationChanged()来配置当前窗口大小的应用布局:

override fun onConfigurationChanged(newConfig: Configuration) {
    
    
    super.onConfigurationChanged(newConfig)
    val windowMetrics = WindowMetricsCalculator.getOrCreate()
        .computeCurrentWindowMetrics(this@MainActivity)
    val bounds = windowMetrics.getBounds()
    ...
}

最后,在窗口自定义适配上,就是老生常谈的话题了,例如:

  • 使用 wrap_contentmatch_parent 避免硬编码
  • 使用 ConstraintLayout 做根布局,方便屏幕尺寸变化,视图自动移动和拉伸
  • 在 App 的 AndroidManifest 里将 applicationactivityandroid:resizeableActivity 属性设置为 true 来支持大小调整并支持响应式/自适应布局。
  • res/layout/ 可以通过创建如 layout-w600dp 的等目录来提供自适应的布局
  • ·····

多窗口和生命周期

既然折叠屏纯在多个区域,就可能存在多窗口,甚至不止两个窗口,这种情况下自然而然就存在生命周期适配的问题,例如多个 App 同时访问 Camera 。

关于多窗口的进程,可以简单介绍下:

  • Android 7.0 支持分屏:左右/上下显示两个窗口

  • Android 8.0 支持画中画模式,此时处于画中画的 Activity 虽处于前台,但处于 Paused 状态

  • Android 9.0 (API 28) 及以下:多窗口下只有获得焦点应用处于 Resumed 状态,其它可见 Activity 仍处于 Paused 状态

  • Android 10.0 (API 29) :多窗口模式时,每个 Acttivity 全部处于Resumed状态

看到没有,不同 API 级别下居然生命周期都不一样,所以为解决 Android 9.0 及以下只有获得焦点应用才处于 Resume 状态问题,App 端可添加下列属性,手动添加开启支持多项 Resumed

<meta-data
    android:name="android.allow_multiple_resumed_activities" android:value="true" />

也就是俗称的 Multi-resume 状态。

为了支持 Multi-resume 状态, 自然就需要一个新的生命周期回调 ,那就是 onTopResumedActivityChanged()

当 Activity 获得或失去顶部 Resume 位置时,系统会调用该方法,例如使用共享单例资源(例如麦克风或摄像头)时:

override fun onTopResumedActivityChanged(topResumed: Boolean) {
    
    
    if (topResumed) {
    
    
        // Top resumed activity
        // Can be a signal to re-acquire exclusive resources
    } else {
    
    
        // No longer the top resumed activity
    }
}

比如对于使用相机的场景,针对上述封装,在 Android 10(API 级别 29)通过CameraManager.AvailabilityCallback#onCameraAccessPrioritiesChanged() 提供了一个回调提示,表明现在可能是可以尝试访问相机的时机。

这里需要注意的是,使用 resizeableActivity=false 并不能保证独占相机访问权限,因为使用相机的其他 App 可能会在多方显示器上打开(分屏)。

所以需要 App 在收到 CameraDevice.StateCallback#onDisconnected() 回调后处理相关行为,如果 onDisconnected 之后还操作 API,系统就会抛出 CameraAccessException.

事实上只要通过回调做好判断,其实这个「焦点」切换体验是无缝的。

在多窗口模式下,Android 可能会禁用或忽略不适用于与其他 Activity 或应用共享设备屏幕的 Activity 的功能。

另外,Activity 也提供了一些方法来支持多窗口模式:

  • isInMultiWindowMode() 是否处于多窗口模式。

  • isInPictureInPictureMode() Activity 是否处于画中画模式。

    注意:画中画模式是多窗口模式的特例,如果isInPictureInPictureMode() 返回 true,则 isInMultiWindowMode() 也会返回 true。

  • onMultiWindowModeChanged() Activity 进入或退出多窗口模式时,系统都会调用此方法。

    如果 Activity 正在进入多窗口模式,则系统向该方法传递一个值 true;如果 Activity 正在离开多窗口模式,则系统向该方法传递一个值 false。

  • onPictureInPictureModeChanged() Activity 进入或退出画中画模式时,系统都会调用此方法。

    如果 Activity 正在进入画中画模式,则系统向该方法传递一个 true 值;如果 Activity 正在离开画中画模式,则系统向该方法传递一个 false 值。

Fragment 同样提供了类似方式,如 Fragment.onMultiWindowModeChanged()

Flutter

3.13 开始 Flutter 也添加了一个新的 API 来匹配显示器的各种属性 #41685,其中新的 FlutterView.display 返回一个 Display 对象,Display 对象会报告显示器的物理尺寸、设备像素比和刷新率:

  
  void didChangeMetrics() {
    
    
    final ui.Display? display = _display;
    if (display == null) {
    
    
      return;
    }
    if (display.size.width / display.devicePixelRatio < kOrientationLockBreakpoint) {
    
    
      SystemChrome.setPreferredOrientations(<DeviceOrientation>[
        DeviceOrientation.portraitUp,
      ]);
    } else {
    
    
      SystemChrome.setPreferredOrientations(<DeviceOrientation>[]);
    }
  }

这个新 API 的主要目的,是前面提到过的内容,因为如果一旦进入了 Letterboxing 模式, Flutter 的 MediaQuery 可能就会无法获取到完整的 avalalbe 屏幕尺寸,所以新的 API 就是提供折叠变化后的真实尺寸给开发者适配的空间。

另外,Flutter 上关于支持多个显示器尺寸的支持还在同步 #125938#125939 ,感兴趣的也可以关注一下。

最后

能看到这里的都是很有耐心的同志,本次调研的涉及的内容较多,覆盖知识点也有点广,有的可能不够深入,大体还是提供了方向和思路,主要涉及:

  • 兼容的 Letterboxing 模式表现
  • resizeableActivity 等配置的不同行为
  • Compose /Activity Embedding /SlidingPaneLayout 的适配方案
  • 折叠屏的判断、窗口适配和生命周期兼容
  • Flutter API

我相信还有很多的 App 没有计划对折叠屏做适配,毕竟「又不是不能用」,但是了解完本篇,至少可以给你提供一些底气,至少看起来如果真要适配,也不是什么做不到的事情。

如果你还有什么想说的,欢迎留言评论交流。

猜你喜欢

转载自blog.csdn.net/ZuoYueLiang/article/details/132451593