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 致力于通过跨端代码复用和原生能力封装,减轻业务的重复适配负担,让页面在保持一致的内容和操作逻辑同时,也能利用各端的设备特性。未来面对更多设备形态,我们也希望把通用适配能力沉淀到框架中,让业务更专注于内容和体验。
参考引用
- 文中 Sample code:github.com/Tencent-TDS/KuiklyUI/tree/main/demo/src/commonMain/kotlin/com/tencent/kuikly/demo/pages/fold
- iPhone Duo来了,腾讯 Kuikly 助 App 从容适配:https://mp.csdn.net/mp_blog/creation/editor/164879503