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

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

一、概念

基于窗口可用空间而非设备类型来设计自适应布局,实现设备无关性和动态适配性,避免硬编码,以不同形态布局更好的展示内容。将可用的显示区域分别在宽高上划分为紧凑型(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)

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

2.1 添加依赖

最新版本

implementation("androidx.compose.material3.adaptive:adaptive:1.2.0")

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

currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)

2.2 基本使用

2.2.1 方式一(已过时)

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

2.2.2 方式二

判断顺序必须从大到小!

@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 使界面切换更平滑。 

@Composable
fun Demo(
    windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
) {
    AnimatedContent(
        targetState = windowSizeClass
    ) { windowSizeClass ->
        if(windowSizeClass...) {...}
    }
}

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

密度适配:详见

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

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

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

封装进自定义主题写法

//通过全局入口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),
)

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

3.2 辅助窗格布局 NavigableSupportingPaneScaffold

官方页面

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值