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

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

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

所以 2023 年开端,折叠屏适配也逐渐开端成为 Android 的干流 KPI 之一,那么不适配的话会怎么样?适配的话又是经过什么办法?本篇将带你深化了解这个论题。

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

Letterboxing 形式

首先,假如不适配的话,你的运用大概率(不用定)会是 Letterboxing 形式的显现办法,或许你会看到 App 以如下图所示的办法存在,也便是当运用的宽高比和屏幕份额不兼容时,App 或许会以 Letterbox 形式翻开

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

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

当然,是否进入 Letterboxing 形式和 TargetSDK 版别、 App 装备和屏幕分辨率都有联系,而且不同 OS 版别上 Letterboxing 形式的出现办法也或许有所不同,例如:

  • Android 12(API 31)开端引入了 Letterboxing 增强功用,可由手机厂家装备支撑:

    • 圆角: 窗口支撑圆角
    • 体系栏透明度:掩盖 App 的状况栏和导航栏支撑半透明
    • 可装备的宽高比:能够调整 App 的宽高比改善运用的外观
      2023 Android 折叠屏适配详解,是时候点亮新技能了
  • 12L(API 32)添加了:

    • 可装备方位:在大屏幕上,设备厂商能够将运用装备在显现屏的左边或右侧。
    • 重启按钮:设备厂商能够为尺度兼容形式的重启按钮赋予新的外观。(尺度兼容形式能够让 App 的宽或许高尽或许充满屏幕)

    当体系确认能够经过从头缩放运用以填充显现窗口来改善 Letterboxing 的显现时,Android 会将 App 置于尺度兼容形式,这时分体系显现一个重启控件,确认后会从头创立 App 进程、从头创立 Activity 偏重绘进行适配。

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

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

那么什么时分会进入 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 会不生效。

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

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

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

还有一点,在折叠屏翻开和闭合的时分,在屏幕发生了改变时,体系或许会毁掉并从头创立整个 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 折叠屏适配详解,是时候点亮新技能了

别的,在官方的不同屏幕尺度匹配里设定了窗户尺度等级标准,例如:

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

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

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

当然还有依据高度去判别的,可是大多数 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-adaptive 的 TwoPane 进行适配

TwoPane 供给了两个固定的槽位,两个槽位的默许方位由 TwoPaneStrategy 驱动,它能够决议将两个槽位水平或笔直摆放,并可装备它们之间的间隔。

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

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

不同场景 Compose 还能够运用 FlowLayout 适配折叠改变 ,FlowLayout 包括 FlowRowFlowColumn ,当一行(或一列)放不下里边的内容时,会主动换行,这在折叠屏翻开和缩短场景也非常有用。

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

关于 Compose 适配折叠屏 Demo 还能够参阅 : github.com/android/com…

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。

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

运用 Jetpack WindowManager 办理和装备 Activity Embedding 其实适当灵敏,能够预先装备 XML 规则,或许直接经过 API 进行办理装备,关于 XML 装备文件中定义的规则,设置以下特点:

  • splitRatio:设置容器份额。该值为开区间 (0.0, 1.0) 内的浮点数。
  • splitLayoutDirection:指定切割容器相关于互相的布局办法。值包括:
    • ltr: 左到右
    • rtl: 右到左
    • localeltr rtl 由语言环境设置决议

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

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

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

能够看到 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 折叠屏适配详解,是时候点亮新技能了

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 折叠屏适配详解,是时候点亮新技能了

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

自定义适配

除了官方的适配方案,或许咱们还需更灵敏的自定义适配方案,那么首先第一件事便是咱们需求知道怎么辨认折叠屏。

辨认折叠屏

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

仍是前面说到的 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 折叠屏适配详解,是时候点亮新技能了

  • 设备处于 Book 形式,屏幕半开而且铰链处于笔直方向

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

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

例如 Google Duo team 就经过 Jetpack WindowManager 辨认折叠屏状况,然后依据翻开状况在播映过程调整界面 UI。

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

简略介绍一下,便是在初始化时经过 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 折叠屏适配详解,是时候点亮新技能了

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

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

由于折叠屏和多屏幕下,你的 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 折叠屏适配详解,是时候点亮新技能了

多窗口和生命周期

已然折叠屏纯在多个区域,就或许存在多窗口,乃至不止两个窗口,这种状况下自然而然就存在生命周期适配的问题,例如多个 App 一起拜访 Camera 。

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

关于多窗口的进程,能够简略介绍下:

  • 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 折叠屏适配详解,是时候点亮新技能了

在多窗口形式下,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 目标会报告显现器的物理尺度、设备像素比和刷新率:


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

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