22、多屏幕适配测试:不同尺寸测试、横竖屏测试、折叠屏测试
说实话,做Compose UI测试这么多年,我最怕听到的一句话就是:「在我手机上没问题啊」。嗯,这句话背后往往藏着一个血泪史——你的应用在测试机上跑得飞起,结果用户拿个折叠屏一展开,布局直接崩了。
多屏幕适配测试,说白了就是让你的UI在各种奇形怪状的屏幕上都能正常显示。我见过太多团队只盯着主流尺寸测,结果上线后被用户骂成筛子。今天我们就来聊聊怎么用Compose的测试框架,系统性地搞定这件事。
22.1 为什么多屏幕测试这么重要?
你想想看,现在的Android设备有多离谱?从2.8英寸的智能手表到12.4英寸的平板,中间还夹着各种折叠屏、卷轴屏。我去年接手过一个项目,用户反馈在三星Fold系列上,底部按钮被折叠铰链区域挡住了——这就是典型的没做折叠屏适配测试的后果。
多屏幕测试要覆盖的核心维度有三个:
- 屏幕尺寸:小屏手机、大屏手机、平板
- 屏幕方向:竖屏、横屏
- 折叠状态:折叠态、展开态、桌面模式
每个维度组合起来,测试用例数量会爆炸。所以我们需要一套可复用的测试方案,而不是手动一个个测。
22.2 不同尺寸测试:从360dp到800dp+
Compose测试中,我们可以通过setContent时传入自定义的Configuration来模拟不同屏幕尺寸。我个人习惯把常用的尺寸定义成常量,方便复用。
// 定义常用屏幕尺寸常量
object ScreenSizes {
val SMALL_PHONE = Configuration().apply {
screenWidthDp = 360
screenHeightDp = 640
smallestScreenWidthDp = 360
}
val MEDIUM_PHONE = Configuration().apply {
screenWidthDp = 411
screenHeightDp = 731
smallestScreenWidthDp = 411
}
val LARGE_PHONE = Configuration().apply {
screenWidthDp = 480
screenHeightDp = 853
smallestScreenWidthDp = 480
}
val TABLET = Configuration().apply {
screenWidthDp = 800
screenHeightDp = 1280
smallestScreenWidthDp = 800
}
}
有了这些配置,写测试就简单了。我通常会写一个辅助函数,把屏幕尺寸作为参数传进去:
@OptIn(ExperimentalTestApi::class)
fun <T> runScreenSizeTest(
configuration: Configuration,
testBlock: ComposeContentTestRule.() -> T
): T {
return composeTestRule.run {
// 设置自定义配置
mainClock.autoAdvance = false
setContent {
CompositionLocalProvider(
LocalConfiguration provides configuration
) {
// 这里放你的Composable
MyScreen()
}
}
testBlock()
}
}
// 使用示例
@Test
fun testLoginScreen_onSmallPhone() {
runScreenSizeTest(ScreenSizes.SMALL_PHONE) {
onNodeWithTag("login_button").assertIsDisplayed()
onNodeWithTag("logo").assertIsDisplayed()
}
}
@Test
fun testLoginScreen_onTablet() {
runScreenSizeTest(ScreenSizes.TABLET) {
// 平板模式下,布局应该是两列
onNodeWithTag("side_panel").assertIsDisplayed()
onNodeWithTag("main_content").assertIsDisplayed()
}
}
22.3 横竖屏测试:方向变了,布局不能崩
横竖屏切换是Android的老大难问题。我记得有一次,一个列表页面在竖屏时显示正常,一切到横屏,列表项之间的间距全乱了——因为横屏下屏幕宽度变大,但代码里写死了Modifier.width(100.dp)。
在Compose测试中,模拟横竖屏其实很简单,只需要修改Configuration的orientation字段:
object ScreenOrientations {
val PORTRAIT = Configuration().apply {
orientation = Configuration.ORIENTATION_PORTRAIT
screenWidthDp = 411
screenHeightDp = 731
}
val LANDSCAPE = Configuration().apply {
orientation = Configuration.ORIENTATION_LANDSCAPE
screenWidthDp = 731 // 横屏时宽高互换
screenHeightDp = 411
}
}
@Test
fun testDashboard_inLandscape() {
runScreenSizeTest(ScreenOrientations.LANDSCAPE) {
// 横屏下,导航栏应该在左侧
onNodeWithTag("navigation_rail").assertIsDisplayed()
// 内容区域应该更宽
onNodeWithTag("content_area").assertWidthIsAtLeast(500.dp)
}
}
这里有个坑要注意:横屏测试时,screenWidthDp和screenHeightDp的值要互换。我曾经犯过这个错,测试了半天发现横屏和竖屏的布局一模一样——原来是我忘了改宽高。
LaunchedEffect或remember,要确保它们在配置变更后能正确重置。我建议在横竖屏测试中都加上「状态保持」的断言。
22.4 折叠屏测试:新形态,新挑战
折叠屏是这两年最让人头疼的。我去年给一个电商App做适配,在三星Galaxy Z Fold上测试时发现:折叠态下商品卡片显示正常,但展开后卡片之间的间距变得特别大,看起来稀稀拉拉的。
折叠屏测试需要关注几个特殊场景:
- 折叠态:类似普通手机,屏幕宽度约360-400dp
- 半折叠态:屏幕呈一定角度,常用于桌面模式
- 展开态:屏幕完全展开,宽度约600-800dp
- 铰链区域:屏幕中间的物理折痕,要避免关键内容被遮挡
在Compose测试中,我们可以通过WindowLayoutInfo来模拟折叠状态。不过说实话,这个API在测试中不太好用。我个人的做法是直接模拟屏幕尺寸变化,因为折叠屏展开后本质上就是屏幕变宽了。
object FoldableStates {
val FOLDED = Configuration().apply {
screenWidthDp = 360
screenHeightDp = 780
smallestScreenWidthDp = 360
}
val HALF_FOLDED = Configuration().apply {
screenWidthDp = 600
screenHeightDp = 780
smallestScreenWidthDp = 600
}
val UNFOLDED = Configuration().apply {
screenWidthDp = 800
screenHeightDp = 780
smallestScreenWidthDp = 800
}
}
@Test
fun testProductGrid_onUnfoldedScreen() {
runScreenSizeTest(FoldableStates.UNFOLDED) {
// 展开态下,应该显示4列商品
onNodeWithTag("product_grid").assertExists()
onAllNodesWithTag("product_card").assertCountEquals(8) // 2行4列
}
}
@Test
fun testProductGrid_onFoldedScreen() {
runScreenSizeTest(FoldableStates.FOLDED) {
// 折叠态下,应该显示2列商品
onNodeWithTag("product_grid").assertExists()
onAllNodesWithTag("product_card").assertCountEquals(4) // 2行2列
}
}
BoxWithConstraints或WindowSizeClass来动态调整布局,而不是写死列数或间距。测试要验证的就是这些动态调整是否正确。
22.5 实战:写一个通用的多屏幕测试工具
说了这么多,我们来写一个真正能用的测试工具类。我一般在项目里会这样封装:
// 多屏幕测试工具类
class MultiScreenTestHelper(
private val composeTestRule: ComposeContentTestRule
) {
// 所有要测试的屏幕配置
private val allConfigs = listOf(
ScreenSizes.SMALL_PHONE,
ScreenSizes.MEDIUM_PHONE,
ScreenSizes.LARGE_PHONE,
ScreenSizes.TABLET,
ScreenOrientations.PORTRAIT,
ScreenOrientations.LANDSCAPE,
FoldableStates.FOLDED,
FoldableStates.UNFOLDED
)
// 批量运行测试
fun runOnAllScreens(
testName: String,
testBlock: () -> Unit
) {
allConfigs.forEach { config ->
val screenLabel = getScreenLabel(config)
println("Running '$testName' on $screenLabel")
composeTestRule.run {
setContent {
CompositionLocalProvider(
LocalConfiguration provides config
) {
// 这里由调用方提供Composable
}
}
testBlock()
}
}
}
private fun getScreenLabel(config: Configuration): String {
return when {
config.screenWidthDp <= 360 -> "小屏手机"
config.screenWidthDp in 361..480 -> "中屏手机"
config.screenWidthDp in 481..600 -> "大屏手机"
config.screenWidthDp > 600 -> "平板/折叠屏展开"
else -> "未知尺寸"
}
}
}
// 使用示例
@Test
fun testHomeScreen_onAllScreens() {
val helper = MultiScreenTestHelper(composeTestRule)
helper.runOnAllScreens("首页布局测试") {
onNodeWithTag("home_title").assertIsDisplayed()
onNodeWithTag("nav_bar").assertIsDisplayed()
}
}
allConfigs列表里加一项,所有测试用例自动覆盖。不过要注意,测试执行时间会线性增长,建议在CI上只跑关键用例的完整覆盖,日常开发可以只跑主流尺寸。
22.6 快照测试:一图胜千言
除了功能测试,我还强烈推荐做快照测试(Snapshot Testing)。说白了就是把UI渲染成图片,然后和基准图片对比。这样布局有没有偏移、间距对不对,一眼就能看出来。
Compose官方提供了captureToImage方法,配合Roborazzi或Paparazzi等库,可以很方便地做快照测试:
@Test
fun testProfileScreen_snapshot() {
composeTestRule.setContent {
ProfileScreen()
}
// 捕获当前UI的图片
composeTestRule.onRoot().captureToImage().let { bitmap ->
// 和基准图片对比
assertSnapshot(
bitmap = bitmap,
name = "profile_screen_default"
)
}
}
快照测试特别适合多屏幕场景。你可以为每个屏幕尺寸生成一张基准图,然后每次代码变更后自动对比。我见过一个团队用这种方式,把UI回归测试的时间从2小时缩短到了10分钟。
22.7 总结:多屏幕测试的黄金法则
做了这么多年多屏幕测试,我总结了几条黄金法则,分享给你:
- 不要等上线再测:从第一个Composable开始,就带上多屏幕测试。越早发现问题,修复成本越低。
- 参数化是王道:用参数化测试覆盖所有屏幕配置,别手动一个个写测试方法。
- 快照测试做补充:功能测试保证「能用」,快照测试保证「好看」。
- 关注边界值:小屏的最小宽度、折叠屏的铰链区域、平板的横竖屏切换——这些边界最容易出问题。
- 自动化是唯一出路:手动测10个屏幕尺寸?别傻了,写个脚本让机器去跑。
嗯,多屏幕适配测试其实没那么可怕。只要你把测试框架搭好了,后面就是水到渠成的事。下次再有人说「在我手机上没问题」,你就可以理直气壮地说:「来,看看我们的自动化测试报告,所有屏幕都过了」。
公众号:蓝海资料掘金营,微信deep3321