自 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-dataandroid:name="android.supports_size_changes" android:value="true" />
所以这里简单做个总结就是:
-
当应用的宽高比与其屏幕比例不兼容,App 锁死旋转方向和大小时会进入 Letterboxing 模式
-
resizeableActivity
的效果主要看 TargetSDK 版本, Android 12(API 31)及更高版本上可能还是会进去分屏模式 -
maxAspectRatio
的作用主要看resizeableActivity
-
配置
android:configChanges
和supports_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.calculateWindowSizeClassclass 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-adaptive
的 TwoPane 进行适配。
TwoPane
提供了两个固定的槽位,两个槽位的默认位置由 TwoPaneStrategy
驱动,它可以决定将两个槽位水平或垂直排列,并可配置它们之间的间隔。
更多可见:https://github.com/google/accompanist/tree/3810fe1182cf52c6660787ae3226dfb7f5ad372a/sample/src/main/java/com/google/accompanist/sample/adaptive
不同场景 Compose 还可以使用 FlowLayout 适配折叠变化 ,FlowLayout 包含 FlowRow
和 FlowColumn
,当一行(或一列)放不下里边的内容时,会自动换行,这在折叠屏展开和收缩场景也非常实用。
关于 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
: 右到左locale
:ltr
或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><propertyandroid:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"android:value="true" /></application>
</manifest>
之后就可以通过 xml 创建各种 Split Rule 或者 WindowManager API 创建 Split Rule 然后调用。
<!-- main_split_config.xml --><resourcesxmlns:window="http://schemas.android.com/apk/res-auto"><!-- Define a split for the named activities. --><SplitPairRulewindow:splitRatio="0.33"window:splitLayoutDirection="locale"window:splitMinWidthDp="840"window:splitMaxAspectRatioInPortrait="alwaysAllow"window:finishPrimaryWithSecondary="never"window:finishSecondaryWithPrimary="always"window:clearTop="false"><SplitPairFilterwindow:primaryActivityName=".ListActivity"window:secondaryActivityName=".DetailActivity"/></SplitPairRule><!-- Specify a placeholder for the secondary container when content isnot available. --><SplitPlaceholderRulewindow:placeholderActivityName=".PlaceholderActivity"window:splitRatio="0.33"window:splitLayoutDirection="locale"window:splitMinWidthDp="840"window:splitMaxAspectRatioInPortrait="alwaysAllow"window:stickyPlaceholder="false"><ActivityFilterwindow:activityName=".ListActivity"/></SplitPlaceholderRule><!-- Define activities that should never be part of a split. Note: Takesprecedence over other split rules for the activity named in therule. --><ActivityRulewindow:alwaysExpand="true"><ActivityFilterwindow: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.SlidingPaneLayoutxmlns: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 combineddesired width (expressed using android:layout_width) wouldnot fit on-screen at once, the right pane is permitted tooverlap the left. --><androidx.recyclerview.widget.RecyclerViewandroid: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 thisexample, android:layout_weight is used to expand this detail paneto consume leftover available space when thethe entire window is wide enough to fit both the left and right pane.--><androidx.fragment.app.FragmentContainerViewandroid: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 支持以及新的 SensorEvent
,SensorEvent
可以监控合页角度,并提供设备的两部分之间的角度测量值:
sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManagerhingeAngleSensor = 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)弃用了 Display
的 getRealSize()
和 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_content
、match_parent
避免硬编码 - 使用
ConstraintLayout
做根布局,方便屏幕尺寸变化,视图自动移动和拉伸 - 在 App 的 AndroidManifest 里将
application
或activity
的android: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-dataandroid: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 没有计划对折叠屏做适配,毕竟「又不是不能用」,但是了解完本篇,至少可以给你提供一些底气,至少看起来如果真要适配,也不是什么做不到的事情。
如果你还有什么想说的,欢迎留言评论交流。