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])  //还没有被点击就默认展示第一条数据的详情页
        }
    }
)

官方页面

相关推荐
plainGeekDev8 小时前
synchronized → Coroutines
android·java·kotlin
sun0077009 小时前
gtest 行覆盖率、函数覆盖率、分支覆盖率 . 一般这3个要达到多少?算达标
android
施棠海9 小时前
设计稿一键变成可运行的Android页面:Pixel2XML全流程UI开发框架实战(附完整源码)
android·ui·架构
Jomurphys9 小时前
Compose 适配 - 分辨率(密度)
android·compose
__Witheart__9 小时前
获取产品对应文件夹
android
__Witheart__9 小时前
版型关联的设备树查询方式
android·rockchip
浪客川9 小时前
Android的SystemUI的启动流程简析
android·开发语言
世界尽头与你10 小时前
Android Root 检测原理
android·安全·网络安全·渗透测试
jctech10 小时前
让「小程序」跑起真正的原生代码:ComboNative 多进程原生小程序运行时 · Alpha 首发
android·设计模式·架构