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-dataandroid: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.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 包含 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><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 支持以及新的 SensorEventSensorEvent 可以监控合页角度,并提供设备的两部分之间的角度测量值:

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/49190.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

pytorch 入门1-tensor 广播 view reshape

tensor 的四则运算broadcast import torch import numpy as np # 张量tensor 随机初始化 x torch.rand(4,3) print(x) y torch.randn(4,3) print(y)# 初始化全零 张量 a torch.zeros((4,4),dtypetorch.long) print(a) #初始化全一 张量 b torch.ones(4,4) print(b) c tor…

Spring相关知识

0、Spring的核心就是AOP和IOC IOC&#xff1a; AOP&#xff1a;AOP&#xff08;Aspect Oriented Programming&#xff09;是面向切面编程&#xff0c;它是一种编程思想&#xff0c;是面向对象编程&#xff08;OOP&#xff09;的一种补充。面向对象编程将程序抽象成各个层次的…

5、css学习5(链接、列表)

1、css可以设置链接的四种状态样式。 a:link - 正常&#xff0c;未访问过的链接a:visited - 用户已访问过的链接a:hover - 当用户鼠标放在链接上时a:active - 链接被点击的那一刻 2、 a:hover 必须在 a:link 和 a:visited 之后&#xff0c; a:active 必须在 a:hover 之后&…

【广州华锐互动】VR工厂消防安全演习提供了一种全新、生动的安全教育方式

在工业生产环境中&#xff0c;安全永远是首要的考虑因素。近年来&#xff0c;随着科技的发展&#xff0c;虚拟现实(VR)技术在各种领域的应用越来越广泛&#xff0c;包括教育和培训。其中&#xff0c;VR工厂消防安全演习就是一个典型的例子&#xff0c;它为员工提供了一种全新的…

关于目标检测鼻祖R-CNN论文

R-CNN系列论文是使用深度学习进行物体检测的鼻祖论文&#xff0c;其中fast-RCNN 以及faster-RCNN都是沿袭R-CNN的思路。R-CNN全称region with CNN features&#xff0c;其实它的名字就是一个很好的解释。用CNN提取出Region Proposals中的featues&#xff0c;然后进行SVM分类与b…

手搭手入门MyBatis-Plus

MyBatis-Plus Mybatis-Plus介绍 为简化开发而生 MyBatis-Plus(opens new window)&#xff08;简称 MP&#xff09;是一个 MyBatis(opens new window) 的增强工具&#xff0c;在 MyBatis 的基础上只做增强不做改变&#xff0c;为简化开发、提高效率而生。 特性 无侵入&#…

安全学习DAY16_信息打点-CDN绕过

信息打点-CDN绕过 文章目录 信息打点-CDN绕过本节思维导图相关链接&工具站&项目工具前置知识&#xff1a;CDN配置&#xff1a;配置1&#xff1a;加速域名-需要启用加速的域名配置2&#xff1a;加速区域-需要启用加速的地区配置3&#xff1a;加速类型-需要启用加速的资源…

信创、工业软件国产化:全面解析三大实时操作系统

信创与国产工业操作系统可以擦出什么火花。 信创技术的快速发展&#xff0c;为国产工业操作系统的研发和应用提供了广阔的空间。 工业操作系统作为工业制造的大脑和神经&#xff0c;工业软件已渗透和应用到工业领域几乎所有核心环节。工业操作系统是智能制造的核心&#xff0c;…

2023年菏泽市中职学校技能大赛“网络安全”赛项规程

2023年菏泽市中职学校技能大赛 “网络安全”赛项规程 一、赛项名称 赛项名称&#xff1a;网络安全 赛项所属专业大类&#xff1a;信息技术类 二、竞赛目的 通过竞赛&#xff0c;检验参赛选手对网络、服务器系统等网络空间中各个信息系统的安全防护能力&#xff0c;以及分析…

RabbitMq的使用

最近处理访客记录所以&#xff0c;来学习下rabbitMQ。之前同事已经写好了&#xff0c;这里只需要进行消费&#xff0c;后续会逐渐完善。 0.介绍 0.1交换机&#xff08;Exchanges&#xff09; rabbitmq中生产者发送的消息都是发送到交换机&#xff0c;再由交换机推入队列。所…

TouchGFX之存储器映射闪存

对于大多数项目&#xff0c;建议使用外部闪存&#xff0c;因为这允许应用程序使用多个大型图像。 即便最普通的应用程序&#xff0c;内部闪存也可能会很快被占用完。 1.配置QSPI&#xff08;嵌入式基础知识&#xff0c;此处不做分析&#xff09; 2.编写W25Q256配置代码&#xf…

Unity 图片资源的适配

前言 最近小编做Unity项目时&#xff0c;发现在资源处理这方面和Android有所不同&#xff1b;例如&#xff1a;Android的资源文件夹res下会有着mipmap-mdpi&#xff0c;mipmap-hdpi&#xff0c;mipmap-xhdpi&#xff0c;mipmap-xxhdpi&#xff0c;mipmap-xxxhdpi这五个文件夹&a…

睡眠模式下如何快速唤醒电脑,看这里!

这篇文章解释了如何唤醒正在睡觉的电脑,以及如果正常方法不起作用该怎么办。 一、如何从睡眠中唤醒电脑 不管你使用的是什么操作系统,关闭睡眠模式就像唤醒电脑一样简单,你可以通过某种方式与电脑交互来完成: 移动鼠标 滑动触摸板 按键盘上的任意键 有些设备有点不同,只…

前端需要理解的HTML知识

HTML&#xff08;超文本标记语言&#xff0c;HyperText Markup Language&#xff09;不是编程语言&#xff0c;而是定义了网页内容的含义和结构的标记语言。。“超文本”&#xff08;hypertext&#xff09;是指连接单个网站内或多个网站间的网页的链接。HTML 使用“标记”&…

C++信息学奥赛1119:矩阵交换行

解题思路&#xff1a;当输出时换行 解题程序&#xff1a; #include<iostream> using namespace std; int main() {int arr[5][5];// 输入矩阵元素for(int i0;i<5;i){for(int j0;j<5;j){cin>>arr[i][j];}} int n,m;cin>>n>>m;// 根据条件进行矩…

java八股文面试[数据结构]——Map有哪些子类

知识来源&#xff1a; 【23版面试突击】 用过哪些Map类&#xff0c;都有什么区别&#xff0c;HashMap是线程安全的吗&#xff1f;_哔哩哔哩_bilibili https://www.cnblogs.com/bubbleboom/p/12694013.html

OpenCV中QR二维码的生成与识别(CIS摄像头解析)

1、QR概述 QR(Quick Response)属于二维条码的一种&#xff0c;意思是快速响应的意思。QR码不仅信息容量大、可靠性高、成本低&#xff0c;还可表示汉字及图像等多种文字信息、其保密防伪性强而且使用非常方便。更重要的是QR码这项技术是开源的&#xff0c;在移动支付、电影票、…

Php“牵手”淘宝商品快递费用数据采集方法,淘宝API接口申请指南

淘宝天猫商品快递费用接口 API 是开放平台提供的一种 API 接口&#xff0c;它可以帮助开发者获取商品的详细信息&#xff0c;包括商品的标题、描述、图片&#xff0c;发货地址&#xff0c;快递费用&#xff0c;区域ID&#xff0c;等信息。在电商平台的开发中&#xff0c;快递费…

使用ctcloss训练矩阵生成目标字符串

首先我们需要明确 c t c l o s s ctcloss ctcloss是用来做什么的。比如说我们要生成的目标字符串长度为 l l l&#xff0c;而这个字符串包含 s s s个字符&#xff0c;字符串允许的最大长度为 L L L&#xff0c;这里我们认为一个位置是一个时间步&#xff0c;就是一拍&#xff0…

2023网络建设与运维模块三:服务搭建与运维

任务描述: 随着信息技术的快速发展,集团计划2023年把部分业务由原有的X86架构服务器上迁移到ARM架构服务器上,同时根据目前的部分业务需求进行了部分调整和优化。 一、X86架构计算机操作系统安装与管理 1.PC1系统为ubuntu-desktop-amd64系统(已安装,语言为英文),登录用户…