新的实验性查询方式: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直接支持,目的地可设置成列表或详情,自动根据窗口大小切换显示逻辑。详见
3.1 列表详情布局 NavigableListDetailPaneScaffold
会根据窗口大小自适应:大窗口并排显示列表页和详情页,小窗口只显示列表页点击后显示详情页。
|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 框架 | @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]) //还没有被点击就默认展示第一条数据的详情页
}
}
)