一码Duo端-Kuikly-iOS折叠屏新UI适配实践

Kuikly 是腾讯开源的高性能跨端框架,基于 Kotlin Multiplatform 技术,覆盖 Android、iOS、HarmonyOS、H5、微信小程序、Mac 六大平台,支撑业务日活用户超5亿。折叠屏产品发展迅速,苹果新推出的 iPhone Duo 带来了不同以往的布局和交互方式,对于跨端页面来说需要处理更多平台差异,带来了更多适配工作。Kuikly 采用原生 UI 渲染,可以很好适配系统的新特性,本文结合具体示例,介绍如何借助 Kuikly 复用跨端代码,做好折叠屏的适配工作。

背景

过往折叠屏适配的核心,主要围绕处理开合、旋转与分屏场景引发的视图尺寸变更做处理。对于跨端页面,基于响应式布局并结合四边安全区域适配,基本可以覆盖绝大多数的场景。而 iPhone Duo 引入全新的设备特征:屏幕存在多处需要避让的局部区域,同时导航栏、工具栏由系统跟随设备形态动态调整。这一变化对跨端页面适配构成新的约束:若维持跨端层自绘导航栏、工具栏的方案,不仅避让区域计算逻辑复杂度高,且无法对齐原生的交互行为。为了实现最佳效果,似乎只能由各平台差异化实现,牺牲跨端方案的效率优势。

针对这一问题,Kuikly 依托原生 UI 渲染的优势,在原有响应式布局和安全区域能力基础上,进一步对齐系统能力,提供保留区域信息与系统导航栏工具栏的原生桥接,侧边栏位的布局与行为交由系统原生组件处理,而页面结构、按钮配置与交互逻辑仍在跨端代码里统一维护,兼顾了原生体验与跨端效率。

折叠屏基本特点

屏幕更大。 展开后的内屏接近一台小平板,一屏能显示的内容比普通手机多不少。很多原来要翻几页、点几次才能看到的信息,现在可以放在同一屏里。

屏幕尺寸会变。用户随时可能把手机合上或打开,App 会在外屏和内屏之间切换,拿到的宽高也跟着变。打开后再分屏或旋转,可用空间还会继续变化。按开合程度,划分三种形态:

  • 折叠态:合上后使用较小的外屏,适合单栏聚焦。
  • 展开态:完全展开,更大的屏幕,适合充分展示内容。
  • 悬停态:半折状态,适合分开呈现内容和操作。

折叠屏的特点给页面设计和开发带来了新的要求:布局要随屏幕尺寸调整,也要用好更大的空间,让内容展示和操作更高效。

iPhone Duo 新变化

iPhone Duo 区别于以往的折叠屏适配,还要关注额外的变化:

Vertical Controls(垂直控制栏)

iPhone Duo 最容易被用户感知到的变化,是系统控制区的位置。过去所有折叠屏设备在系统控制区位置都是沿用直板手机形态,顶部导航,底部 Tab Bar,中间内容。但苹果在折叠屏上把导航栏、工具栏和 Tab 控制放到屏幕侧边,为内容留出更多纵向空间。而且在分屏情况下,状态栏会分别出现在左右两侧,系统响应式容器会有相对应自适应能力。

Reserved Regions(保留区域)

过去布局主要通过安全区避开系统 UI。在 iPhone Duo 上,还要关注屏幕内部的摄像头和折叠位置。内外屏摄像头可能占用局部空间,设备部分展开时,中间的折叠区域也会影响内容排列。苹果通过预留区域描述这些位置及其生效状态,App 可以据此避开遮挡,或把内容安排到折叠区域两侧。

苹果官方给出的设备指南是:「可调整大小是你的 App 在 iPhone Duo 上的关键功能。如果你的 App 已在 iPad 和 Mac 上运行,或者你已在 iPhone Mirroring 中准备好让 App 调整大小,那么你已经为支持 iPhone Duo 打下了坚实基础。如果没有,请采用标准布局控件和容器,并使用大小类和场景几何来改进 App 的大小调整功能。然后,处理保留区域(例如折叠区域)来调整 App 大小并处理折叠布局。评估排列视图是否可以协助 App 布局来处理折叠区域。

在某些姿态下,iPhone Duo 会将导览栏、工具栏和标签栏一起垂直显示在显示屏侧边。请检查并整理 App 中的栏,以确保常用控制始终可见,且在空间有限时,较少使用的控制会出现在溢出菜单中。」

跨端适配痛点

对跨端页面来说,难点在于复用同一套代码的同时,还要适应各端不同的系统行为。布局可以随尺寸调整,但安全区、折叠区域、系统栏的位置和行为需要分别接入;单栏切换到多栏时,还要保持页面跳转、返回和操作状态的一致。

这些差异都交给使用方处理,页面中就会增加大量平台判断,未来还可能出现更多设备形态,逐端、逐页适配的成本会持续增加。因此,跨端框架需要把通用布局和交互留给业务复用,将设备差异封装成可用的能力,既减轻适配负担,也让页面能够利用各端的设备特性,这也是 Kuikly 的适配思路。

总体可以把适配划分成两部分:通用的尺寸和布局变化,以及预留区域、系统侧栏等设备特有的能力。Kuikly 采用原生 UI 渲染,前者可以沿用已有的响应式布局,后者通过预留区域、系统导航栏和工具栏等实现原生能力的接入,业务页面仍然使用 Kuikly 配置内容和交互。下面结合具体示例,看看现有页面如何适配折叠屏,并利用 Duo 的设备特性。

Kuikly 适配指南

1. 布局随可用空间变化

Kuikly 从一开始就支持响应式刷新和 Flex 布局。折叠、展开或分屏带来的尺寸变化,可以沿用这套机制处理。

在 attr 中使用 pageData.pageViewWidth 和 pageData.pageViewHeight 计算尺寸,宿主上报尺寸变化后,相关布局属性会自动更新。结合 flex(1f) 分配剩余空间,可以减少固定尺寸和手动高度计算。

展开后不必一味拉伸。阅读、表单等内容可限制最大宽度并居中,多出的空间用于留白、增加列表列数或展示详情。

kotlin 复制代码
// KuiklyDSL
View {
    attr {
        flex(1f)
    }
    View {
        attr {  
            width(ctx.pageData.pageViewWidth * 0.8f)
            maxWidth(600f)
        }    
     }
}
kotlin 复制代码
// Kuikly ComposeDSL
val pageWidth = LocalConfiguration.current.pageViewWidth
Box(Modifier.width(minOf(pageWidth * 0.8f, 600f).dp)) { 
    /* ... */ 
}

2. 安全区域和避让

安全区域仍然是多设备适配的关键。不同设备的刘海、系统栏和手势区域不同,文字和操作控件需要按当前安全区域避让。旋转、分屏或开合时,安全距离也可能变化,布局应随之更新,而不是预留固定的顶部、底部间距。

Kuikly 通过 pageData.safeAreaInsets 提供上、下、左、右的安全距离。把安全区域绑定到内容边距,就能随安全区变化调整布局:

kotlin 复制代码
// KuiklyDSL
View {
    attr {
        flex(1f)
        paddingTop(ctx.pageData.safeAreaInsets.top)
        paddingLeft(ctx.pageData.safeAreaInsets.left)
        paddingBottom(ctx.pageData.safeAreaInsets.bottom)
        paddingRight(ctx.pageData.safeAreaInsets.right)
    }
    // 需要安全区保护的前景内容
}
kotlin 复制代码
// Kuikly ComposeDSL
val configuration = LocalConfiguration.current
configuration.pageViewWidth                                // 订阅尺寸变化
val safe = configuration.pageData.safeAreaInsets

Box(Modifier.fillMaxSize().background(bgColor)) {          // 背景铺满
    Column(
        Modifier.fillMaxSize().absolutePadding(
            left = safe.left.dp, top = safe.top.dp,
            right = safe.right.dp, bottom = safe.bottom.dp,
        )
    ) {
        // 需要安全区保护的前景内容
    }
}

过去 App 做适配页面主要用 Top 和 Bottom 处理避让导航栏和底部状态栏。而 Duo 的导航栏和工具栏可能移到侧边,因此也要关注 left、right,并结合系统栏的实际占位处理左右空间,不能只留上下边距。

在 iPhone Duo 上,除了安全区域外,还要关注预留区域,处理摄像头、状态栏遮挡和折叠带来的布局限制。

  • 遮挡区域:避免文字、按钮等关键内容与它重叠。
  • 分割区域:需要分区的页面,可以把内容安排到折叠区域两侧。

Kuikly 通过 pageData.reservedRegions 提供区域的位置、大小、类型和当前是否生效,区域使用 x、y、width、height 描述,坐标相对 Kuikly 根视图,计算规避后的坐标。

如果希望背景、图片等内容延伸到顶部,可以不对整页统一添加 paddingTop,通过 pageData.reservedRegions 对当前生效的遮挡区域做局部避让。

kotlin 复制代码
// KuiklyDSL
View {
    attr {
        val pageWidth = ctx.pageData.pageViewWidth
        val occlusion = ctx.pageData.reservedRegions.firstOrNull {
            it.active && it.kind == ReservedRegionKind.OCCLUSION && it.x >= pageWidth / 2f
        }
        // 伪代码:有右侧遮挡时,避让右边位置 right 
        val paddingRight = if (occlusion != null) pageWidth - occlusion.x + 12f else 12f
        absolutePosition(right = paddingRight)
    }
}
kotlin 复制代码
// Kuikly ComposeDSL
val configuration = LocalConfiguration.current
val pageWidth = configuration.pageViewWidth
val occlusion = configuration.pageData.reservedRegions.firstOrNull {
    it.active && it.kind == ReservedRegionKind.OCCLUSION && it.x >= pageWidth / 2f
}
val badgeRight = if (occlusion != null) pageWidth - occlusion.x + 12f else 12f

Box(Modifier.fillMaxSize().background(bgColor)) {           // 背景延伸到顶部
    Text(
        "角标",
        Modifier.align(Alignment.TopEnd)
            .absolutePadding(top = 12.dp, right = badgeRight.dp),
    )
}

3. 空间变大后,怎么展示内容

展开后不一定要把所有元素放大。常见做法有三种:拉伸、调整布局和多栏展示,具体选哪种取决于页面内容。

拉伸:让适合扩展的内容占用更多空间

文本、画布等内容,可以扩展显示区域。普通按钮、图标和字号通常不用跟着同比例放大。

阅读页则要限制正文宽度,避免展开后每行文字过长。多出来的空间可以留白,不必强行填满。

调整布局:重排页面,或者增加列数

如:图文卡片可以由上下排改为左右排,信息流可以增加列数。Kuikly 的 WaterfallList(KuiklyDSL)、LazyVerticalStaggeredGrid(ComposeDSL) 支持设置列表宽度和列数:

kotlin 复制代码
// KuiklyDSL
WaterfallList {
    attr {
        flex(1f)
        listWidth(ctx.pagerData.pageViewWidth)
        columnCount(if (ctx.pagerData.pageViewWidth >= 600f) 4 else 2)
        itemSpacing(10f)
        lineSpacing(10f)
    }
    // 普通卡片不写死宽度,由列表按列宽分配
}
kotlin 复制代码
// Kuikly ComposeDSL
val pageWidth = LocalConfiguration.current.pageViewWidth
LazyVerticalStaggeredGrid(
    columns = StaggeredGridCells.Fixed(if (pageWidth >= 600f) 4 else 2),
    modifier = Modifier.fillMaxSize(),
    verticalItemSpacing = 10.dp,                          // 对应 lineSpacing
    horizontalArrangement = Arrangement.spacedBy(10.dp),  // 对应 itemSpacing
) {
    items(feed, key = { it.id }) { item ->
        FeedCard(item, Modifier.fillMaxWidth())   // 卡片不写死宽度,由网格按列宽分配
    }
}
双栏:同时展示有关联的内容

列表与详情、目录与正文等关联内容,可以在宽屏下并排展示,窄屏下仍按单栏逐级访问。外屏和内屏应保持一致的内容和操作逻辑,展开后只是把原来分步查看的信息同时呈现,让用户感觉仍在使用同一个 App。

详细可以参考:iPhone Duo来了,腾讯 Kuikly 助 App 从容适配

4. 导航栏和工具栏

在 iPhone Duo 上,导航栏和工具栏可以随设备姿态和容器布局调整到侧边,为内容留出更多纵向空间。页面除了适应尺寸变化,也要跟着系统栏的位置调整内容区域。

Kuikly DSL:通过 UINavigationBar 和 UIToolbar 接入宿主的原生系统栏。业务侧配置标题、图标、按钮和点击事件,系统根据容器和姿态处理栏的位置与排列。例如,添加一个返回按钮:

kotlin 复制代码
UINavigationBar {
    attr {
        tintColor(Color.BLACK)
    }
    items {
        navbarItems {
            navbarItem {
                attr {
                    placement(UIBarButtonItemPlacement.Leading)
                    symbol("chevron.backward")
                    title("返回")
                    index(0)
                    axis(UIBarAxis.VerticalPreferred)
                }
                event {
                    click {
                        ctx.acquireModule<RouterModule>(RouterModule.MODULE_NAME)
                            .closePage()
                    }
                }
            }
        }
    }
}

Kuikly ComposeDSL:可以用 MakeKuiklyComposeNode 挂一个 DSL 容器,在容器里声明它:

kotlin 复制代码
@Composable
fun SystemNavigationBar(onBack: () -> Unit) {
    val currentOnBack by rememberUpdatedState(onBack)
    MakeKuiklyComposeNode<DivView>(
        factory = { DivView() },
        modifier = Modifier.size(0.dp),      // 系统栏只配置宿主 navigationItem,不参与布局
        viewInit = {
            UINavigationBar {
                attr { tintColor(KuiklyColor.BLACK) }
                items {
                    navbarItems {
                        navbarItem {
                            attr {
                                placement(UIBarButtonItemPlacement.Leading)
                                symbol("chevron.backward")
                                title("返回")
                                index(0)
                                axis(UIBarAxis.VerticalPreferred)
                            }
                            event { click { currentOnBack() } }
                        }
                    }
                }
            }
        },
    )
}
封装通用导航栏,统一各端的导航和避让

整体来看,跨端差异主要在系统导航栏、工具栏的使用方式和空间占位。为方便适配,业务可以封装一个通用导航栏,统一导航、返回按钮和工具栏配置,把各端的导航与避让集中由组件处理。

iPhone 端接入系统导航栏和工具栏。在 Duo 的侧置布局下,根据栏的实际位置、安全区和生效的预留区域调整导航与正文空间;其他端或未接入系统栏的页面,沿用常规导航布局,按 safeAreaInsets 处理顶部、底部及所在边的安全距离。

正文使用容器分配的剩余空间,不再重复添加安全区边距。这样,导航样式和避让规则可以集中维护,适配新设备时也不必逐页修改。

这样既能利用 Duo 的系统侧栏等设备特性,也能让业务页面保持统一的配置方式。后续适配新设备时,主要在组件内部补充对应实现和避让规则,不必逐页修改。

总结

新设备形态的出现,也带来了更多布局调整和平台适配工作。Kuikly 致力于通过跨端代码复用和原生能力封装,减轻业务的重复适配负担,让页面在保持一致的内容和操作逻辑同时,也能利用各端的设备特性。未来面对更多设备形态,我们也希望把通用适配能力沉淀到框架中,让业务更专注于内容和体验。

参考引用

相关推荐
T量子永生1 小时前
Android SISO 分布式编译集群配置方案
android·分布式·云计算·源代码管理
2501_915918411 小时前
Windows 上开发 React Native iOS,真机调试、Fast Refresh 与打包 ipa
windows·vscode·react native·ios·个人开发·swift·敏捷流程
梦曦i1 小时前
uni-app x 新组件库v0.1.0发布,多端统一开发
前端·ui·uni-app x
街头便衣1 小时前
OpenRouter周榜复算:164万亿token,输出2.55%,DeepSeek账单占1.4%
android·java·开发语言·人工智能·语言模型·开源
OH_TPC2 小时前
HarmonyOS APP开发---“云诊室“在线问诊App,需要用到这个库
华为·harmonyos·鸿蒙·1024程序员节
警醒与鞭策13 小时前
Codex桌面版与Claude桌面版使用设置
android·unity·ai·性能优化
aiopencode14 小时前
局域网设备抓包怎么做?用 TraceEagle 抓电视、游戏机和 IoT 流量
后端·ios
奇牙coding15 小时前
GPT-5.5 报 401 invalid_api_key 怎么办?换密钥前按 4 层排查请求头、项目权限和端点
数据库·gpt·ios·ai
传奇开心果16 小时前
【Compose Multiplatform跨端声明式UI实用技术】第19课:多平台部署与发布
android·kotlin