Compose 适配 - 自适应布局(窗口大小类 WindowSizeClasses、列表详情、辅助窗格)

新的实验性查询方式:UiMediaScope,详见

一、概念

基于窗口可用空间而非设备类型来设计自适应布局,实现设备无关性和动态适配性,避免硬编码,以不同形态布局更好的展示内容。将可用的显示区域分别在宽高上划分为紧凑型(Compact)、中等型(Medium)和扩展型(Expanded),由于垂直滚动的普遍性,通常根据可用宽度进行适配。

|-----------------|----------------------|----------|----------------------------|
| 类型 | 宽度 | 展示内容的窗格数 | 常见设备 |
| Compat 紧凑型 | width < 600 | 1 | 手机(竖屏) 折叠(竖屏展开、竖屏半折叠) |
| Medium 中等型 | 600 ≤ width < 840 | 1(推荐)或2 | 平板(竖屏) 折叠(横屏展开,横屏半折叠) |
| Expanded 扩展型 | 840 ≤ width < 1200 | 1或2(推荐) | 手机(横屏) 平板(横屏) 折叠(横屏二分屏) 电脑 |
| Large 大型 | 1200 ≤ width < 1600 | 2或3(推荐) | 外接显示器 |
| Extra-large 超大型 | 1600 ≤ width | 3或4(推荐) | 外接显示器 |

二、手动实现(窗口大小类 WindowSizeClasses)

根据窗口大小,自动加载不同布局。如果仅仅是列表详情、辅助窗格的场景,使用下方开箱即用的组件会更方便。

2.1 添加依赖

最新版本

Kotlin 复制代码
implementation("androidx.compose.material3.adaptive:adaptive:1.2.0")

启动 Large 和 Extra-large 需在 Gradle 构建文件中声明使用新的断点即可选择启用。

Kotlin 复制代码
currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)

2.2 基本使用

2.2.1 方式一(已过时)

Kotlin 复制代码
@Composable
fun Demo(
    windowWidthSizeClass: WindowWidthSizeClass = currentWindowAdaptiveInfo().windowSizeClass
) {
    //根据判断结果,分别加载不同界面或对变量赋值
    when (windowWidthSizeClass) {
        WindowWidthSizeClass.COMPACT -> CompactScreen()
        WindowWidthSizeClass.MEDIUM -> MediumScreen()
        WindowWidthSizeClass.EXPANDED -> ExpandedScreen()
        else -> CompactScreen()
    }
}

2.2.2 方式二

判断顺序必须从大到小!

Kotlin 复制代码
@Composable
fun Demo(
    windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
) {
    //根据判断结果,分别加载不同界面或对变量赋值(≥返回true)
    if (windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_EXPANDED_LOWER_BOUND)) {
        ExpandedScreen()
    } else if (windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND)) {
        MediumScreen()
    } else {
        CompactScreen()
    }
}

2.3 优化

2.3.1 动画

添加动画 AnimatedContent 使界面切换更平滑。

Kotlin 复制代码
@Composable
fun Demo(
    windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
) {
    AnimatedContent(
        targetState = windowSizeClass
    ) { windowSizeClass ->
        if(windowSizeClass...) {...}
    }
}

2.3.2 针对APP使用了密度适配的情况

密度适配:详见

UI图给的宽高一般都是根据手机设计的,修改密度后,作用域内获取 WindowSizeClass 进行屏幕大小判断的结果总是 Compact,因此初始化时通过 CompositionLocal 来提供全局获取。

Kotlin 复制代码
//无法提供默认值,TopLevel没有Compose作用域
val LocalWindowSizeClass = compositionLocalOf<WindowSizeClass> { error("LocalWindowSizeClass没有默认值") }

@Composable
fun AppTheme() {
    val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
    CompositionLocalProvider(
        LocalWindowSizeClass provides windowSizeClass
    ) {
        content()
    }
}

封装进自定义主题写法

Kotlin 复制代码
//通过全局入口AppTheme获取更符合直觉,因此设为private
private val LocalWindowSizeClass = compositionLocalOf<WindowSizeClass> { error("LocalWindowSizeClass没有默认值") }

//声明一个单例,用作全局入口
object AppTheme {
    //用于获取窗口大小信息
    val windowSizeClass: WindowSizeClass
        @Composable get() = LocalWindowSizeClass.current
}

@Composable
fun AppTheme() {
    val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
    CompositionLocalProvider(
        LocalWindowSizeClass provides windowSizeClass
    ) {
        content()
    }
}

三、开箱即用(列表详情布局、辅助窗格布局)

Navigation3直接支持,目的地可设置成列表或详情,自动根据窗口大小切换显示逻辑。详见

官方页面

会根据窗口大小自适应:大窗口并排显示列表页和详情页,小窗口只显示列表页点击后显示详情页。

|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 框架 | @Composable fun <T> NavigableListDetailPaneScaffold( navigator: ThreePaneScaffoldNavigator<T>, //导航器 listPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit, //列表窗格 detailPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit, //详情窗格 modifier: Modifier = Modifier, extraPane: (@Composable ThreePaneScaffoldPaneScope.() -> Unit)? = null, //额外窗格(提供额外的背景信息) defaultBackBehavior: BackNavigationBehavior = BackNavigationBehavior.PopUntilScaffoldValueChange, paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? = null, paneExpansionState: PaneExpansionState? = null, ) |
| 导航器 | @Composable fun <T> rememberListDetailPaneScaffoldNavigator( scaffoldDirective: PaneScaffoldDirective = calculatePaneScaffoldDirective(currentWindowAdaptiveInfo()), adaptStrategies: ThreePaneScaffoldAdaptStrategies = ListDetailPaneScaffoldDefaults.adaptStrategies(), isDestinationHistoryAware: Boolean = true, initialDestinationHistory: List<ThreePaneScaffoldDestinationItem<T>> = DefaultListDetailPaneHistory, ): ThreePaneScaffoldNavigator<T> |
| 动画 | @Composable fun <S, T : PaneScaffoldValue<S>> ExtendedPaneScaffoldPaneScope<S, T>.AnimatedPane( modifier: Modifier = Modifier, enterTransition: EnterTransition = motionDataProvider.calculateDefaultEnterTransition(paneRole), exitTransition: ExitTransition = motionDataProvider.calculateDefaultExitTransition(paneRole), boundsAnimationSpec: FiniteAnimationSpec<IntRect> = PaneMotionDefaults.AnimationSpec, content: (@Composable AnimatedPaneScope.() -> Unit), ) |

Kotlin 复制代码
val coroutineScope = rememberCoroutineScope()
//数据类型实现Parcelable支持保存和恢复所选列表项
val navigator = rememberListDetailPaneScaffoldNavigator<MyData>()
NavigableListDetailPaneScaffold(
    modifier = Modifier.background(AppColors.green),    //列表和详情之间会有间隔显示这个颜色
    navigator = navigator,
    listPane = {
        AnimatedPane {  //可选,默认的窗格动画
            ListPage(
                list = dataList,
                onItemClick = {
                    coroutineScope.launch {
                        //导航到详情窗格
                        navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, it)
                    }
                }
            )
        }
    },
    detailPane = {
        AnimatedPane {
            //currentDestination是当前目的地,contentKey是携带的数据
            navigator.currentDestination?.contentKey?.let { it->
                DetailPage(it)
            } ?: DetailPage(dataList[0])  //还没有被点击就默认展示第一条数据的详情页
        }
    }
)

官方页面

相关推荐
YB13756 小时前
jetpack compose 副作用 SideEffect
android·kotlin
CHB8 小时前
uni-app x 蒸汽模式 性能测试基准报告 Benchmark【Android版】
android·ios·uni-app
带娃的IT创业者9 小时前
拆掉那堵墙:LibrePods 如何让 AirPods 在 Android 上重获新生
android·开源项目·airpods·librepods·生态解锁
_阿南_11 小时前
flutter 展示的字体突然奔放了
android·flutter·ios
文人sec14 小时前
MySQL事务与索引做了什么?
android·笔记·mysql
刘洋浪子15 小时前
AndroidStudio保存日志菜单
android
朱涛的自习室15 小时前
从 Prompt 到 Graph:AI 工程的进化史
android·前端·人工智能
2501_9327502615 小时前
Android 中 Serializable 与 Parcelable 的对比与选择
android
FungLeo16 小时前
Flutter 两个反直觉布局坑:ListTile 水波纹 / VerticalDivider 踩坑实录
android·flutter
1570925113417 小时前
ViewBinding与DataBinding区别与选型
android