官方页面一、概念1.1 生态存在的问题以前Android设备以手机为主有些APP锁死横竖屏screenOrientation在平板上运行也只能是竖屏界面以此共用同一套UI、有些APP锁死界面宽高比maxAspectRatio/minAspectRatio并拒绝 Activity 根据屏幕尺寸更改界面大小resizeableActivity导致即使在大屏上也只能显示手机版界面。现在传统根据屏幕大小和方向做适配的方式已不再适用现在APP的显示方式可能是整个设备屏幕或分屏、多窗口模式下的可调整大小的窗口动态宽高比意味着横竖屏判断无效、以及可折叠设备的半折叠和全展开状态下的形态。因此需要根据可用的窗口大小和姿态做适配。1.1.1 Android16 新规为提升大屏设备的体验以下 API屏幕方向、宽高比、大小调整在 targetSdk36 且宽度 600dp 会失效也就是仅对紧凑型仍然有效。Android16 可以在 AndroidManifest 中配置绕过但后续高版本不再豁免。 基于 android:appCategory 标志游戏将不受这些变更的影响。此外用户拥有控制权他们可以在宽高比设置中明确选择使用应用的默认行为。清单属性 / API忽略的值screenOrientationportrait, reversePortrait, sensorPortrait, userPortrait, landscape, reverseLandscape, sensorLandscape, userLandscapesetRequestedOrientation()portrait, reversePortrait, sensorPortrait, userPortrait, landscape, reverseLandscape, sensorLandscape, userLandscaperesizeableActivity所有minAspectRatio所有maxAspectRatio所有application ... property android:nameandroid.window.PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY android:valuetrue / /application1.2 窗口可用空间基于窗口可用空间而非设备类型来设计自适应布局实现设备无关性和动态适配性避免硬编码以不同形态布局更好的展示内容。将可用的显示区域分别在宽高上划分为紧凑型Compact、中等型Medium和扩展型Expanded由于垂直滚动的普遍性通常根据可用宽度进行适配。类型宽度展示内容的窗格数常见设备Compat紧凑型width 6001手机竖屏折叠竖屏展开、竖屏半折叠Medium中等型600 ≤ width 8401推荐或2平板竖屏折叠横屏展开横屏半折叠Expanded扩展型840 ≤ width 12001或2推荐手机横屏平板横屏折叠横屏二分屏电脑Large大型1200 ≤ width 16002或3推荐外接显示器Extra-large超大型1600 ≤ width3或4推荐外接显示器二、手动区分可用空间 WindowSizeClasses最新版本implementation(androidx.compose.material3.adaptive:adaptive:1.2.0)启动 Large 和 Extra-large 需在 Gradle 构建文件中声明使用新的断点即可选择启用。currentWindowAdaptiveInfo(supportLargeAndXLargeWidth true)1.1 基本使用已过时Composable fun Demo( windowWidthSizeClass: WindowWidthSizeClass currentWindowAdaptiveInfo().windowSizeClass ) { //根据判断结果分别加载不同界面或对变量赋值 when (windowWidthSizeClass) { WindowWidthSizeClass.COMPACT - CompactScreen() WindowWidthSizeClass.MEDIUM - MediumScreen() WindowWidthSizeClass.EXPANDED - ExpandedScreen() else - CompactScreen() } }推荐判断顺序必须从大到小Composable fun Demo( windowSizeClass: WindowSizeClass currentWindowAdaptiveInfo().windowSizeClass ) { //根据判断结果分别加载不同界面或对变量赋值 if (windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_EXPANDED_LOWER_BOUND)) { ExpandedScreen() } else if (windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND)) { MediumScreen() } else { CompactScreen() } }1.2 优化添加动画 AnimatedContent 使界面切换更平滑。换用 rememberSaveable 确保状态健壮免受Activity重建影响。AnimatedContent( targetState windowSizeClass ) { windowSizeClass - if(windowSizeClass...) {...} }1.3 针对APP使用了密度适配的情况详见UI给的宽高一般都是根据手机设计的修改密度后作用域内获取 WindowSizeClass 进行屏幕大小判断的结果总是 Compact因此初始化时通过 CompositionLocal 来提供全局获取。//无法提供默认值TopLevel没有Compose作用域 val LocalWindowSizeClass compositionLocalOfWindowSizeClass { error(LocalWindowSizeClass没有默认值) } Composable fun AppTheme() { val windowSizeClass currentWindowAdaptiveInfo().windowSizeClass CompositionLocalProvider( LocalWindowSizeClass provides windowSizeClass ) { content() } }封装进自定义主题写法//通过全局入口AppTheme获取更符合直觉因此设为private private val LocalWindowSizeClass compositionLocalOfWindowSizeClass { error(LocalWindowSizeClass没有默认值) } //声明一个单例用作全局入口 object AppTheme { //用于获取窗口大小信息 val windowSizeClass: WindowSizeClass Composable get() LocalWindowSizeClass.current } Composable fun AppTheme() { val windowSizeClass currentWindowAdaptiveInfo().windowSizeClass CompositionLocalProvider( LocalWindowSizeClass provides windowSizeClass ) { content() } }三、使用开箱即用的自适应布局官方页面3.1 自适应导航栏 NavigationSuiteScaffold导航在小窗口上例如手机全屏显示一般位于界面底部在展开窗口上例如平板全屏显示一般位于界面侧边。NavigationSuiteScaffold 会根据 WindowSizeClass 显示适当的导航方式包括在运行时窗口大小发生变化时动态更改界面。当宽或高较小、或处于桌上模式会显示在底部其他情况显示在侧边。有三种底部Bar、侧边Rail、抽屉Drawer就是更宽的侧边适合更大的屏幕。implementation(androidx.compose.material3:material3-adaptive-navigation-suite)Composablefun NavigationSuiteScaffold(navigationSuiteItems: NavigationSuiteScope.() - Unit, //条目modifier: Modifier Modifier,layoutType: NavigationSuiteType NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(WindowAdaptiveInfoDefault), //默认计算方式navigationSuiteColors: NavigationSuiteColors NavigationSuiteDefaults.colors(), //导航栏背景色containerColor: Color NavigationSuiteScaffoldDefaults.containerColor, //切换区域的背景色contentColor: Color NavigationSuiteScaffoldDefaults.contentColor, //切换区域的内容色Text文字会变色content: Composable () - Unit {}, //切换区域)参数layoutType默认是自动更改导航栏显示的方式可通过NavigationSuiteType指定为None不显示、NavigationBar底部、NavigationRail侧边、NavigationDrawer抽屉。3.1.1 定义节点信息enum class AppDestinations( val itemIcon: ImageVector, val itemName: String, ) { HOME(Icons.Default.Home, 首页), MALL(Icons.Default.ShoppingCart, 商城), MESSAGE(Icons.Default.Email, 消息), MINE(Icons.Default.Person, 个人) }3.1.2 定义条目颜色val navigationItemColors NavigationSuiteDefaults.itemColors( //底部导航栏 navigationBarItemColors NavigationBarItemColors( selectedIconColor AppColors.red, //选中后的图标颜色 selectedTextColor AppColors.red, //选中后的文字颜色 selectedIndicatorColor AppColors.blue, //选中的指示器颜色 unselectedIconColor AppColors.black, //未选中的图标颜色 unselectedTextColor AppColors.black, //未选中的文字颜色 disabledIconColor AppColors.purple, //被禁用的图标颜色 disabledTextColor AppColors.purple //被禁用的文字颜色 ), //侧边导航栏 navigationRailItemColors NavigationRailItemColors( selectedIconColor AppColors.red, //选中后的图标颜色 selectedTextColor AppColors.red, //选中后的文字颜色 selectedIndicatorColor AppColors.blue, //选中的指示器颜色 unselectedIconColor AppColors.black, //未选中的图标颜色 unselectedTextColor AppColors.black, //未选中的文字颜色 disabledIconColor AppColors.purple, //被禁用的图标颜色 disabledTextColor AppColors.purple //被禁用的文字颜色 ), //抽屉导航栏只能这样实现上面两个也能这样实现 navigationDrawerItemColors NavigationDrawerItemDefaults.colors( selectedIconColor AppColors.red, //选中后的图标颜色 selectedTextColor AppColors.red, //选中后的文字颜色 selectedContainerColor AppColors.blue, //选中的指示器颜色 selectedBadgeColor AppColors.orange, //选中后的小红点颜色 unselectedIconColor AppColors.black, //未选中的图标颜色 unselectedTextColor AppColors.black, //未选中的文字颜色 unselectedContainerColor AppColors.transparent, //未选中指示器颜色 unselectedBadgeColor AppColors.orange //未选中的小红点颜色 ) )3.1.3 配置条目和内容切换NavigationSuiteScaffold( navigationSuiteItems { AppDestinations.entries.forEach { item( icon { Icon(it.itemIcon, ) }, //条目图标 label { Text(it.itemName) }, //条目名称 selected it currentDestination, //是否选中 onClick { currentDestination it }, //点击回调 colors navigationItemColors //条目颜色 ) } }, navigationSuiteColors NavigationSuiteDefaults.colors( navigationBarContainerColor AppColors.green, //底部导航栏的背景色 navigationRailContainerColor AppColors.green, //侧边导航栏的背景色 navigationDrawerContainerColor AppColors.green //抽屉导航栏的背景色 ), containerColor AppColors.red, //切换区域的背景色 contentColor AppColors.blue //切换区域的内容颜色Text文字颜色会被改变 ) { when (currentDestination) { AppDestinations.HOME - HomeScreen() AppDestinations.MALL - MallScreen() AppDestinations.MESSAGE - MessageScreen() AppDestinations.MINE - MineScreen() } }3.2 列表详情布局 NavigableListDetailPaneScaffold会根据窗口大小自适应大窗口并排显示列表页和详情页小窗口只显示列表页点击后显示详情页。框架Composablefun T NavigableListDetailPaneScaffold(navigator: ThreePaneScaffoldNavigatorT, //导航器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,)导航器Composablefun T rememberListDetailPaneScaffoldNavigator(scaffoldDirective: PaneScaffoldDirective calculatePaneScaffoldDirective(currentWindowAdaptiveInfo()),adaptStrategies: ThreePaneScaffoldAdaptStrategies ListDetailPaneScaffoldDefaults.adaptStrategies(),isDestinationHistoryAware: Boolean true,initialDestinationHistory: ListThreePaneScaffoldDestinationItemT DefaultListDetailPaneHistory,): ThreePaneScaffoldNavigatorT动画Composablefun S, T : PaneScaffoldValueS ExtendedPaneScaffoldPaneScopeS, T.AnimatedPane(modifier: Modifier Modifier,enterTransition: EnterTransition motionDataProvider.calculateDefaultEnterTransition(paneRole),exitTransition: ExitTransition motionDataProvider.calculateDefaultExitTransition(paneRole),boundsAnimationSpec: FiniteAnimationSpecIntRect PaneMotionDefaults.AnimationSpec,content: (Composable AnimatedPaneScope.() - Unit),)val coroutineScope rememberCoroutineScope() //数据类型实现Parcelable支持保存和恢复所选列表项 val navigator rememberListDetailPaneScaffoldNavigatorMyData() 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]) //还没有被点击就默认展示第一条数据的详情页 } } )3.3 辅助窗格布局 NavigableSupportingPaneScaffold四、响应式排版参考文章3.1 流式布局 FlowRow流式布局能实时响应屏幕可用空间进行重排界面。Composable fun AdapterScreen( windowWidthSizeClass: WindowWidthSizeClass currentWindowAdaptiveInfo().windowSizeClass.windowWidthSizeClass ) { FlowRow( modifier Modifier.fillMaxSize(), horizontalArrangement Arrangement.Center, verticalArrangement Arrangement.Center, maxItemsInEachRow 3 ////可用空间再大一行也只有3个子元素 ) { //大型组件作为第一个直接放 Big() //对紧凑型来说小型组件横着放放不下会自动换行效果图横着显示2个 //对中等型和展开型来说用容器 FlowColumn 包裹小型组件使之成为一个整体 //充分利用纵向空间在容器中竖着放放不下会自动换列效果图竖着显示两个 if(windowWidthSizeClass WindowWidthSizeClass.COMPACT) { Small() Small() } else { FlowColumn { Small() Small() } } //中型组件同理小型组件 if(windowWidthSizeClass WindowWidthSizeClass.COMPACT) { Medium() Medium() } else { FlowColumn { Medium() Medium() } } } }3.2 保持子元素内部动画状态 movableContentOf()当流式布局进行重排的时候子元素的动画会被打断重新启动。借助可移动内容 Movable content可以在子元素被移动的时候不丢失动画状态。val smallContent remember { movableContentOf { Small() Small() } } FlowRow { if (windowSizeClass WindowWidthSizeClass.Compact) { smallContent() } else { FlowColumn { smallContent() } } }3.3 子元素位移动画 Modifier.animateBounds()当流式布局进行重排的时候子元素是瞬间移动的没有流畅自然的感觉。最外层使用 LookaheadScope 包裹能够让 Compose 在布局变化时执行中间测量过程并告知子元素这些中间状态。使用 Modifier.animateBounds() 构建一个自定义的 Modifier 传递给子元素构建时可以指定 boundsTransform 参数到自定义的 spring 规范从而定制动画的运行方式。最新版本implementation(androidx.compose.animation:animation:1.8.0-beta01)//自定义动画 val boundsTransform { _: Rect, _: Rect- spring( dampingRatio Spring.DampingRatioNoBouncy, stiffness Spring.StiffnessMedium, visibilityThreshold Rect.VisibilityThreshold ) } //最外层包裹一下 LookaheadScope { //单独创建 Modifier 方便多次传参给子元素 val MyModifier Modifier.animateBounds( lookaheadScope thisLookaheadScope, boundsTransform boundsTransform ) val smallContent remember { movableContentOf { small(modifier MyModifier) //将自定义的 Modifier 传递给子元素 small(modifier MyModifier) } } FlowRow { if (windowSizeClass WindowWidthSizeClass.Compact) { smallContent() } else { FlowColumn { smallContent() } } } }