likes
comments
collection
share

2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

作者站长头像
站长
· 阅读数 15

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

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

2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

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

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

Letterboxing 模式

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

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

2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

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

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

    • 圆角: 窗口支持圆角
    • 系统栏透明度:覆盖 App 的状态栏和导航栏支持半透明
    • 可配置的宽高比:可以调整 App 的宽高比改善应用的外观 2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy
  • 12L(API 32)添加了:

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

    当系统确定可以通过重新缩放应用以填充显示窗口来改进 Letterboxing 的显示时,Android 会将 App 置于尺寸兼容模式,这时候系统显示一个重启控件,确定后会重新创建 App 进程、重新创建 Activity 并重绘进行适配。 2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

  • Android 13(API 33)添加了一个用户引导的提示对话框 : 2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

那么什么时候会进入 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

2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

那是不是我不使用高版本的 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 会不生效。

2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

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

2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

还有一点,在折叠屏展开和闭合的时候,在屏幕发生了变化时,系统可能会销毁并重新创建整个 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

2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

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

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

2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

当然还有基于高度去判断的,但是大多数 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 驱动,它可以决定将两个槽位水平或垂直排列,并可配置它们之间的间隔。

2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

更多可见:github.com/google/acco…

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

2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

关于 Compose 适配折叠屏 Demo 还可以参考 : github.com/android/com…

Activity Embedding

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

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

2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

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。

2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

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

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

2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

可以看到 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>

更多可见:developer.android.com/guide/topic…

SlidingPaneLayout

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

2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

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 还引入了锁定模式,支持在窗格重叠时控制滑动行为,例如:

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

2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

更多可见: developer.android.com/guide/topic…

自定义适配

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

识别折叠屏

2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

还是前面提到的 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
    

    2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

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

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

    2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

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

2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

简单介绍一下,就是在初始化时通过 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方法。

2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

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

2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

因为折叠屏和多屏幕下,你的 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 的等目录来提供自适应的布局
  • ·····

2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

多窗口和生命周期

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

2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

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

  • 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.

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

2023 Android 折叠屏适配详解,是时候点亮新技能了自 2019 年三星发布了第一台(柔宇不算) Galaxy

在多窗口模式下,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


  @override
  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 没有计划对折叠屏做适配,毕竟「又不是不能用」,但是了解完本篇,至少可以给你提供一些底气,至少看起来如果真要适配,也不是什么做不到的事情。

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

转载自:https://juejin.cn/post/7270303042079195193
评论
请登录