Jetpack Compose中的Accompanist

news2024/9/22 21:12:13

accompanist是Jetpack Compose官方提供的一个辅助工具库,以提供那些在Jetpack Compose sdk中目前还没有的功能API。

权限

依赖配置:

repositories {
    mavenCentral()
}

dependencies {
    implementation "com.google.accompanist:accompanist-permissions:0.28.0"
}

单个权限申请

例如,我们需要获取相机权限,可以通过rememberPermissionState(Manifest.permission.CAMERA)创建一个 PermissionState对象,然后通过PermissionState.status.isGranted判断权限是否已获取,并通过调用permissionState.launchPermissionRequest()来申请权限。

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 别忘了在清单文件中添加权限声明 -->
    <uses-permission android:name="android.permission.CAMERA"/>
    ....
</manifest>
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun PermissionExample() {
    // Camera permission state
    val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA)

    if (cameraPermissionState.status.isGranted) {
        Text("Camera permission Granted")
    } else {
        Column(horizontalAlignment = Alignment.CenterHorizontally) {
            val textToShow = if (cameraPermissionState.status.shouldShowRationale) {
                // 如果用户之前选择了拒绝该权限,应当向用户解释为什么应用程序需要这个权限
                "未获取相机授权将导致该功能无法正常使用。"
            } else {
                // 首次请求授权
                "该功能需要使用相机权限,请点击授权。"
            }
            Text(textToShow)
            Spacer(Modifier.height(8.dp))
            Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
                Text("请求权限")
            }
        }
    }
}

在这里插入图片描述

多个权限申请

类似的,通过rememberMultiplePermissionsState获取到 PermissionsState之后, 通过调用permissionsState.launchMultiplePermissionRequest()来请求权限。

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
	<!-- 别忘了在清单文件中添加权限声明 -->
    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
 	...
</manifest>

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun MultiplePermissionsExample() {
    val multiplePermissionsState = rememberMultiplePermissionsState(
        listOf(
            android.Manifest.permission.READ_EXTERNAL_STORAGE,
            android.Manifest.permission.CAMERA,
        )
    )
    if (multiplePermissionsState.allPermissionsGranted) {
        Text("相机和读写文件权限已授权!")
    } else {
        Column(modifier = Modifier.padding(10.dp)) {
            Text(
                getTextToShowGivenPermissions(
                    multiplePermissionsState.revokedPermissions, // 被拒绝/撤销的权限列表
                    multiplePermissionsState.shouldShowRationale
                ),
                fontSize = 16.sp
            )
            Spacer(Modifier.height(8.dp))
            Button(onClick = { multiplePermissionsState.launchMultiplePermissionRequest() }) {
                Text("请求权限")
            }
            multiplePermissionsState.permissions.forEach {
                Divider()
                Text(text = "权限名:${it.permission} \n " +
                        "授权状态:${it.status.isGranted} \n " +
                        "需要解释:${it.status.shouldShowRationale}", fontSize = 16.sp)
            }
            Divider()
        }
    }
}

@OptIn(ExperimentalPermissionsApi::class)
private fun getTextToShowGivenPermissions(
    permissions: List<PermissionState>,
    shouldShowRationale: Boolean
): String {
    val size = permissions.size
    if (size == 0) return ""
    val textToShow = StringBuilder().apply { append("以下权限:") }
    for (i in permissions.indices) {
        textToShow.append(permissions[i].permission).apply {
            if (i == size - 1) append(" ") else append(", ")
        }
    }
    textToShow.append(
        if (shouldShowRationale) {
            " 需要被授权,以保证应用功能正常使用."
        } else {
            " 被拒绝使用. 应用功能将不能正常使用."
        }
    )
    return textToShow.toString()
}

在这里插入图片描述

以上代码请求了两个权限,所以运行后系统会分别弹出两次授权弹窗。

定位权限申请:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
</manifest>
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun LocationPermissionsExample() {
    val locationPermissionsState = rememberMultiplePermissionsState(
        listOf(
            android.Manifest.permission.ACCESS_COARSE_LOCATION,
            android.Manifest.permission.ACCESS_FINE_LOCATION,
        )
    )
    if (locationPermissionsState.allPermissionsGranted) {
        Text("定位权限已授权")
    } else {
        Column(horizontalAlignment = Alignment.CenterHorizontally) {
            val textToShow = if (locationPermissionsState.shouldShowRationale) {
                // 两个权限都被拒绝
                "无法获取定位权限将导致应用功能无法正常使用"
            } else {
                // 首次授权
                "该功能需要定位授权"
            }
            Text(text = textToShow)
            Spacer(Modifier.height(8.dp))
            Button(onClick = { locationPermissionsState.launchMultiplePermissionRequest() }) {
                Text("请求授权")
            }
        }
    }
}

注意:定位权限在 Android 10 以后就被拆分为前台权限Manifest.permission.ACCESS_FINE_LOCATION和后台权限Manifest.permission.ACCESS_BACKGROUND_LOCATION,如果要申请后台权限,首先minSdk配置必须是29以上(也就是Android 10.0,不过这一点很多公司应该不会选择,因为兼容的手机版本高了)且在 Android 11 后两个权限不能同时申请,也就是说要先请求前台权限之后才能申请后台权限。

SystemUiController

该库可以设置应用顶部状态栏和底部导航栏的颜色。

dependencies {
    implementation "com.google.accompanist:accompanist-systemuicontroller:0.28.0"
}

例如,可以设置状态栏和导航栏的颜色随着手机系统设置的主题改变而变化

@Composable
fun MyComposeApplicationTheme(
    isDarkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        // Android 12以上支持动态主题颜色(可以跟随系统桌面壁纸的主色调自动获取主题颜色)
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (isDarkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }
        isDarkTheme -> DarkColorScheme
        else -> LightColorScheme
    }

    // 修改状态栏和导航栏颜色
    val systemUiController = rememberSystemUiController()
    SideEffect {
        // setStatusBarColor() and setNavigationBarColor() also exist
        systemUiController.setSystemBarsColor(
            color = if(isDarkTheme) Color.Black else Color.White,
        )
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

在这里插入图片描述
也可以设置icons的颜色

// Remember a SystemUiController
val systemUiController = rememberSystemUiController()
val useDarkIcons = !isSystemInDarkTheme() 
DisposableEffect(systemUiController, useDarkIcons) {
    // Update all of the system bar colors to be transparent, and use
    // dark icons if we're in light theme
    systemUiController.setSystemBarsColor(
        color = Color.Transparent,
        darkIcons = useDarkIcons
    )  
}

此外可以使用 systemUiController.setStatusBarColor()systemUiController.setNavigationBarColor() 分别设置状态栏和导航栏的颜色。

如果需要其他组件跟随系统主题颜色变化,最好使用MaterialTheme.colorScheme中的颜色属性。

Pager

对标传统View中的ViewPager组件。

dependencies {
    implementation "com.google.accompanist:accompanist-pager:0.28.0"
}

HorizontalPager

@OptIn(ExperimentalPagerApi::class)
@Composable
fun HorizontalPagerExample() {
    HorizontalPager(count = 10) { page ->
        Box(Modifier.background(colors[page % colors.size]).fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            Text(text = "Page: $page",color = Color.White,fontSize = 22.sp)
        }
    }
}

在这里插入图片描述
在模拟器中运行的时候,有时会出现卡住在中间的情况,不知道是不是模拟器的原因:
在这里插入图片描述
如果想跳转到指定页面,可以使用 pagerState.scrollToPage(index) 或者pagerState.animateScrollToPage(index) 这两个挂起方法:

@OptIn(ExperimentalPagerApi::class)
@Composable
fun HorizontalPagerExample2() {
    val scope = rememberCoroutineScope()
    val pagerState = rememberPagerState()

    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        HorizontalPager(
            count = 10,
            state = pagerState,
            modifier = Modifier.height(300.dp)
        ) { page ->
            Box(Modifier.background(colors[page % colors.size]).fillMaxSize(),
                contentAlignment = Alignment.Center
            ) {
                Text(text = "Page: $page", color = Color.White, fontSize = 22.sp)
            }
        }
        Button(onClick = { scope.launch { pagerState.animateScrollToPage(2) } }) {
            Text(text = "跳转到第3页")
        }
    }
}

VerticalPager

使用类似HorizontalPager

@OptIn(ExperimentalPagerApi::class)
@Composable
fun VerticalPagerExample() {
    VerticalPager(count = 10) { page ->
        Box(Modifier.background(colors[page % colors.size]).fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            Text(text = "Page: $page",color = Color.White,fontSize = 22.sp)
        }
    }
}

HorizontalPagerVerticalPager 背后是基于 LazyRowLazyColumn 实现的,不在当前屏幕显示的页面会从容器中移除。

contentPadding

HorizontalPagerVerticalPager 支持设置 contentPadding , 如果设置start padding,则当前页的开头会显示上一页的部分内容,如果设置horizontal padding,则当前页的开头和结尾会分别显示上一页和下一页的部分内容。

@OptIn(ExperimentalPagerApi::class)
@Composable
fun HorizontalPagerExample() {
    HorizontalPager(
        count = 10,
        contentPadding = PaddingValues(start = 64.dp),
    ) { page ->
        Box(Modifier.background(colors[page % colors.size]).fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            Text(text = "Page: $page", color = Color.White, fontSize = 22.sp)
        }
    }
}

在这里插入图片描述

@OptIn(ExperimentalPagerApi::class)
@Composable
fun HorizontalPagerExample() {
    HorizontalPager(
        count = 10,
        contentPadding = PaddingValues(horizontal = 64.dp),
    ) { page ->
        Box(Modifier.background(colors[page % colors.size]).fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            Text(text = "Page: $page", color = Color.White, fontSize = 22.sp)
        }
    }
}

在这里插入图片描述

item滚动效果

Pager的作用域内允许应用轻松引用currentPagecurrentPageOffset 这些值来计算动画效果。官方提供了一个calculateCurrentOffsetForPage()扩展函数来计算给定页面的偏移量:

@OptIn(ExperimentalPagerApi::class)
@Composable
fun ItemScrollEffect() {
    HorizontalPager(count = 10) { page ->
        Card(
            Modifier.graphicsLayer {
                    // 计算当前页面距离滚动位置的绝对偏移量,然后根据偏移量来计算效果
                    val pageOffset = calculateCurrentOffsetForPage(page).absoluteValue

                    // We animate the scaleX + scaleY, between 85% and 100%
                    lerp(
                        start = 0.85f,
                        stop = 1f,
                        fraction = 1f - pageOffset.coerceIn(0f, 1f)
                    ).also { scale ->
                        scaleX = scale
                        scaleY = scale
                    }

                    // We animate the alpha, between 50% and 100%
                    alpha = lerp(
                        start = 0.5f,
                        stop = 1f,
                        fraction = 1f - pageOffset.coerceIn(0f, 1f)
                    )
                }
        ) {
            Box(Modifier
                .background(colors[page % colors.size])
                .fillMaxWidth(0.85f).height(500.dp),
                contentAlignment = Alignment.Center
            ) {
                Text(text = "Page: $page", color = Color.White, fontSize = 22.sp)
            }
        }
    }
}

在这里插入图片描述

注:上面代码中使用到的函数lerp需要单独添加一个依赖库androidx.compose.ui:ui-util

监听页面切换

val pagerState = rememberPagerState()

LaunchedEffect(pagerState) {
    // Collect from the pager state a snapshotFlow reading the currentPage
    snapshotFlow { pagerState.currentPage }.collect { page ->
        // do something with page index
    }
}

VerticalPager(
    count = 10,
    state = pagerState,
) { page ->
    Text(text = "Page: $page")
}

PagerIndicator

Accompanist库提供了HorizontalPagerIndicatorVerticalPagerIndicator组件可以分别搭配HorizontalPagerVerticalPager 使用,需要单独导入依赖库accompanist-pager-indicators,当然,你也可以自己监听页面切换状态写一个。

@OptIn(ExperimentalPagerApi::class)
@Composable
fun HorizontalPagerIndicatorExample() {
    Scaffold(
        modifier = Modifier.fillMaxSize()
    ) { padding ->
        Column(Modifier.fillMaxSize().padding(padding)) {
            val pagerState = rememberPagerState()

            // Display 10 items
            HorizontalPager(
                count = 10,
                state = pagerState,
                // Add 32.dp horizontal padding to 'center' the pages
                contentPadding = PaddingValues(horizontal = 32.dp),
                modifier = Modifier
                    .weight(1f)
                    .fillMaxWidth(),
            ) { page ->
                Box(Modifier
                    .background(colors[page % colors.size])
                    .fillMaxWidth()
                    .height(500.dp),
                    contentAlignment = Alignment.Center
                ) {
                    Text(text = "Page: $page", color = Color.White, fontSize = 22.sp)
                }
            }

            HorizontalPagerIndicator(
                pagerState = pagerState,
                modifier = Modifier
                    .align(Alignment.CenterHorizontally)
                    .padding(16.dp),
            )
        }
    }
}

在这里插入图片描述

@OptIn(ExperimentalPagerApi::class)
@Composable
fun VerticalPagerIndicatorExample() {
    Scaffold(
        modifier = Modifier.fillMaxSize()
    ) { padding ->
        val pagerState = rememberPagerState()
        Row(verticalAlignment = Alignment.CenterVertically) {
            // Display 10 items
            VerticalPager(
                count = 10,
                state = pagerState,
                // Add 32.dp vertical padding to 'center' the pages
                contentPadding = PaddingValues(vertical = 32.dp),
                modifier = Modifier
                    .weight(1f)
                    .height(300.dp)
            ) { page ->
                Box(Modifier
                    .background(colors[page % colors.size])
                    .fillMaxSize(),
                    contentAlignment = Alignment.Center
                ) {
                    Text(text = "Page: $page", color = Color.White, fontSize = 22.sp)
                }
            }

            VerticalPagerIndicator(
                pagerState = pagerState,
                modifier = Modifier.padding(16.dp)
            )
        }
    }
}

在这里插入图片描述

Pager结合Tab使用

HorizontalPager 可以结合 TabRowScrollableTabRow 使用,但是好像没有找到垂直的TabRow可以结合VerticalPager使用的,需要的话只能自己写一个了。

@OptIn(ExperimentalPagerApi::class)
@Composable
fun PagerWithScrollableTabRow() {
    Column {
        val pagerState = rememberPagerState()
        val titles = ('A'..'Z').toList()
        ScrollableTabRow(
            // Our selected tab is our current page
            selectedTabIndex = pagerState.currentPage,
            // Override the indicator, using the provided pagerTabIndicatorOffset modifier
            indicator = { tabPositions ->
                TabRowDefaults.Indicator(
                    Modifier.pagerTabIndicatorOffset(pagerState, tabPositions)
                )
            }
        ) {
            val scope = rememberCoroutineScope()
            // Add tabs for all of our pages
            titles.forEachIndexed { index, title ->
                Tab(
                    text = { Text("$title") },
                    selected = pagerState.currentPage == index,
                    onClick = { scope.launch { pagerState.animateScrollToPage(index) } },
                )
            }
        }

        HorizontalPager(
            count = titles.size,
            state = pagerState,
            modifier = Modifier
                .weight(1f)
                .fillMaxWidth()
        ) { page ->
            Box(Modifier
                .background(colors[page % colors.size])
                .fillMaxSize(),
                contentAlignment = Alignment.Center
            ) {
                Text(text = "Page: ${titles[page]}", color = Color.White, fontSize = 22.sp)
            }
        }
    }
}

在这里插入图片描述

FlowLayout

dependencies {
    implementation "com.google.accompanist:accompanist-flowlayout:0.28.0"
}

可以自动换行的流式布局,使用也非常简单,分别提供了两种FlowRowFlowColumn布局,属性和Row跟Column组件的用法都差不多

@Composable
fun FlowRowExample() {
    FlowRow(
        Modifier.padding(15.dp),
        mainAxisSpacing = 8.dp,
        crossAxisSpacing = 8.dp
    ) {
        SampleContent()
    }
}

@Composable
fun FlowColumnExample() {
    FlowColumn(
        Modifier.padding(15.dp),
        mainAxisSpacing = 8.dp,
        crossAxisSpacing = 8.dp
    ) {
        SampleContent2()
    }
}

@Composable
internal fun SampleContent() {
    repeat(30) { it ->
        Box(
            modifier = Modifier
                .width(Random.nextInt(150).coerceAtLeast(50).dp)
                .clip(RoundedCornerShape(50))
                .background(Color.Green) ,
            contentAlignment = Alignment.Center,
        ) {
            Text(it.toString(), fontSize = 22.sp)
        }
    }
}

@Composable
internal fun SampleContent2() {
    repeat(30) { it ->
        Box(
            modifier = Modifier
            	.width(50.dp)
                .height(Random.nextInt(150).coerceAtLeast(50).dp)
                .clip(RoundedCornerShape(50))
                .background(Color.Green) ,
            contentAlignment = Alignment.Center,
        ) {
            Text(it.toString(), fontSize = 22.sp)
        }
    }
}

在这里插入图片描述
在这里插入图片描述

WebView

dependencies {
    implementation "com.google.accompanist:accompanist-webview:0.28.0"
}

使用超级简单:

@Composable
fun WebViewExample() {
    val state = rememberWebViewState("https://m.baidu.com")
    WebView(state)
}

开启JavaScript:

WebView(
    state = webViewState,
    onCreated = { it.settings.javaScriptEnabled = true }
)

设置是否捕获拦截返回按键:

 WebView(
     state,
     onCreated = { it.settings.javaScriptEnabled = true },
     captureBackPresses = true
 )

这个要夸一夸了,captureBackPresses这个值默认为true, 也就是默认会捕获拦截返回按键,相信做过原生与H5混合开发的都知道,传统的View体系中的WebView组件是不拦截返回键的,这就会导致你在网页里点击了好几层看的正高兴时,一不小心按了手机的back键就会直接关闭退出整个网页!简直受不了!解决这个尴尬的问题往往需要开发者在持有WebView的Activity中自己手动拦截KeyEvent事件进行处理,现在好了,这个问题终于不用我们去处理了。

还可以自定义WebView

WebView(
    ...
    factory = { context -> CustomWebView(context) }
)

navigation-animation

dependencies {
    implementation "com.google.accompanist:accompanist-navigation-animation:0.28.0"
}

navigation-animation 库提供了一种可为Navigation Compose添加自定义转场动画的方法。

	val navController = rememberAnimatedNavController()
    AnimatedNavHost(navController) {
        composable("routeName",
            enterTransition = {...},
            exitTransition = {...},
            popEnterTransition = {...},
            popExitTransition = {...}
        ) { 
        	SomeScreen(navController) 
        }
    }

AnimatedNavHost 中的每个composable中通过enterTransition可以设置当前页面的入场效果,通过exitTransition参数可以设置当前页面的离场效果。

其中,在每个Transition参数的AnimatedContentScope<NavBackStackEntry> 作用域中,可通过 initialStatetargetState 属性来精确的自定义要运行的Transition效果(可使用特定的转场Api 如slideIntoContainerslideOutOfContainer)。

这里initialStatetargetState 的含义:

  • initialState:转场动画的起始状态,从哪个页面开始的
  • targetState:转场动画的结束状态,结束后是跳转到哪个页面的

composable配置中四个Transition参数的含义:

  • enterTransition:控制当前页面作为 targetState 时的入场动画 (到达的屏幕)
  • exitTransition:控制当前页面作为 initialState 时的离场动画 (出发的屏幕)
  • popEnterTransition:控制当前页面作为targetState因弹栈而入场动画 (别的页面被弹出后,当前页面被显示),如果不设置,则默认值同enterTransition
  • popExitTransition:控制当前页面作为initialState弹栈时的离场动画 (当前页面被弹出),如果不设置,默认值同exitTransition
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun ExperimentalAnimationNav() {
    val navController = rememberAnimatedNavController()
    AnimatedNavHost(navController, startDestination = "Blue") {
        composable("Blue",
            // 从Red导航到Blue时,Blue的入场动画是slideInFromRight
            enterTransition = { initialState.isRoute("Red").transition(slideInFromRight) },
            // 从Blue导航到Red时,Blue的离场动画是slideOutToLeft
            exitTransition = { targetState.isRoute("Red").transition(slideOutToLeft) },
            // 从Red页面弹栈后显示Blue时,Blue的入场动画是slideInFromLeft
            popEnterTransition = { initialState.isRoute("Red" ).transition(slideInFromLeft) },
            // 从Blue页面弹栈后显示Red时,Blue的离场动画是slideOutToRight
            popExitTransition = { targetState.isRoute("Red").transition(slideOutToRight) }
        ) { BlueScreen(navController) }

        composable("Red",
            // 从Blue导航到Red时,Red的入场动画是slideInFromRight
            // 从Green导航到Red时,Red的入场动画是slideInFromBottom
            enterTransition ={initialState.by(mapOf("Blue" to slideInFromRight, "Green" to slideInFromBottom )) },
            // 从Red导航到Blue时,Red的离场动画是slideOutToLeft
            // 从Red导航到Green时,Red的离场动画是slideOutToTop
            exitTransition ={targetState.by(mapOf("Blue" to slideOutToLeft, "Green" to slideOutToTop)) },
            // 从Blue页面弹栈后显示Red时,Red的入场动画是slideInFromLeft
            // 从Green页面弹栈后显示Red时,Red的入场动画是slideInFromTop
            popEnterTransition ={initialState.by(mapOf("Blue" to slideInFromLeft, "Green" to slideInFromTop )) },
            // 从Red页面弹栈后显示Blue时,Red的离场动画是slideOutToRight
            // 从Red页面弹栈后显示Green时,Red的离场动画是slideOutToBottom
            popExitTransition ={targetState.by(mapOf("Blue" to slideOutToRight, "Green" to slideOutToBottom )) }
        ) { RedScreen(navController) }

        // 从Blue导航到Inner时,Inner入场方式:expandIn放大进入
        // 从Inner导航到Blue时,Inner离场方式:shrinkOut缩小退出
        // Inner第一个屏幕是Green
        navigation(
            startDestination = "Green",
            route = "Inner",
            enterTransition = { expandIn(animationSpec = tween700ms()) },
            exitTransition = { shrinkOut(animationSpec = tween700ms()) }
        ) {
            composable(
                "Green",
                // 从Red导航到Green时,Green的入场动画是slideInFromBottom
                enterTransition = { initialState.isRoute("Red").transition(slideInFromBottom) },
                // 从Green导航到Red时,Green的离场动画是slideOutToTop
                exitTransition = { targetState.isRoute("Red").transition(slideOutToTop) },
                // 从Red页面弹栈后显示Green时,Green的入场动画是slideInFromTop
                popEnterTransition = { initialState.isRoute("Red").transition(slideInFromTop) },
                // 从Green页面弹栈后显示Red时,Green的离场动画是slideOutToBottom
                popExitTransition = { targetState.isRoute( "Red").transition(slideOutToBottom) }
            ) { GreenScreen(navController) }
        }
    }
}

@ExperimentalAnimationApi
@Composable
fun AnimatedVisibilityScope.BlueScreen(navController: NavHostController) {
    Column(Modifier.fillMaxSize().background(Color.Blue)) {
        val buttonModifier = Modifier.wrapContentWidth().then(Modifier.align(Alignment.CenterHorizontally))
        Spacer(Modifier.height(25.dp))
        NavigateButton("Navigate Horizontal", buttonModifier) {
            navController.navigate("Red")
        }
        Spacer(Modifier.height(25.dp))
        NavigateButton("Navigate Expand",buttonModifier) {
            navController.navigate("Inner")
        }
        MyText("Blue", Modifier.weight(1f))
        PopBackButton(navController)
    }
}

@ExperimentalAnimationApi
@Composable
fun AnimatedVisibilityScope.RedScreen(navController: NavHostController) {
    Column(Modifier.fillMaxSize().background(Color.Red)) {
        val buttonModifier = Modifier.wrapContentWidth().then(Modifier.align(Alignment.CenterHorizontally))
        Spacer(Modifier.height(25.dp))
        NavigateButton("Navigate Horizontal", buttonModifier) {
            navController.navigate("Blue")
        }
        Spacer(Modifier.height(25.dp))
        NavigateButton("Navigate Vertical", buttonModifier) {
            navController.navigate("Green")
        }
        MyText("Red", Modifier.weight(1f))
        PopBackButton(navController)
    }
}

@ExperimentalAnimationApi
@Composable
fun AnimatedVisibilityScope.GreenScreen(navController: NavHostController) {
    Column(Modifier.fillMaxSize().background(Color.Green)) {
        val buttonModifier = Modifier.wrapContentWidth().then(Modifier.align(Alignment.CenterHorizontally))
        Spacer(Modifier.height(25.dp))
        NavigateButton("Navigate to Red", buttonModifier) {
            navController.navigate("Red")
        }
        MyText("Green", Modifier.weight(1f))
        PopBackButton(navController)
    }
}

@ExperimentalAnimationApi
@Composable
fun AnimatedVisibilityScope.MyText(text: String, modifier: Modifier = Modifier) {
    Text(text,
        modifier.fillMaxWidth().animateEnterExit(
                enter = fadeIn(animationSpec = tween(250, delayMillis = 450)),
                exit = ExitTransition.None
            ),
        color = Color.White, fontSize = 80.sp, textAlign = TextAlign.Center
    )
}

@Composable
fun NavigateButton(text: String, modifier: Modifier = Modifier, listener: () -> Unit = {}) {
    Button(
        onClick = listener,
        colors = ButtonDefaults.buttonColors(backgroundColor = Color.LightGray),
        modifier = modifier
    ) {
        Text(text = text)
    }
}

@Composable
fun PopBackButton(navController: NavController) {
    // Use LocalLifecycleOwner.current as a proxy for the NavBackStackEntry
    // associated with this Composable
    if (navController.currentBackStackEntry == LocalLifecycleOwner.current &&
        navController.previousBackStackEntry != null) {
        Button(
            onClick = { navController.popBackStack() },
            colors = ButtonDefaults.buttonColors(backgroundColor = Color.LightGray),
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(text = "Go to Previous screen")
        }
    }
}
@Stable
fun <T> tween700ms(): TweenSpec<T> = tween(700)

@OptIn(ExperimentalAnimationApi::class)
val AnimatedContentScope<NavBackStackEntry>.slideInFromBottom
    get() = slideIntoContainer(SlideDirection.Up, animationSpec = tween700ms())

@OptIn(ExperimentalAnimationApi::class)
val AnimatedContentScope<NavBackStackEntry>.slideInFromTop
    get() = slideIntoContainer(SlideDirection.Down, animationSpec = tween700ms())

@OptIn(ExperimentalAnimationApi::class)
val AnimatedContentScope<NavBackStackEntry>.slideInFromRight
    get() = slideIntoContainer(SlideDirection.Left, animationSpec = tween700ms())

@OptIn(ExperimentalAnimationApi::class)
val AnimatedContentScope<NavBackStackEntry>.slideInFromLeft
    get() = slideIntoContainer(SlideDirection.Right, animationSpec = tween700ms())

@OptIn(ExperimentalAnimationApi::class)
val AnimatedContentScope<NavBackStackEntry>.slideOutToTop
    get() = slideOutOfContainer(SlideDirection.Up, animationSpec = tween700ms())

@OptIn(ExperimentalAnimationApi::class)
val AnimatedContentScope<NavBackStackEntry>.slideOutToBottom
    get() = slideOutOfContainer(SlideDirection.Down, animationSpec = tween700ms())

@OptIn(ExperimentalAnimationApi::class)
val AnimatedContentScope<NavBackStackEntry>.slideOutToLeft
    get() = slideOutOfContainer(SlideDirection.Left, animationSpec = tween700ms())

@OptIn(ExperimentalAnimationApi::class)
val AnimatedContentScope<NavBackStackEntry>.slideOutToRight
    get() = slideOutOfContainer(SlideDirection.Right, animationSpec = tween700ms())

fun NavBackStackEntry.isRoute(target: String): Boolean  = this.destination.route == target

fun <T> Boolean.transition(transition: T) : T? = if(this) transition else null

fun <T> NavBackStackEntry.by(map: Map<String, T>) : T? = map[this.destination.route]

在这里插入图片描述

对于每个Transition参数,如果返回null,则将使用父导航元素的Transition,从而允许您在导航图级别设置一组全局转换,该转换将应用于该图中的每个composable。如果父导航也返回null,则会一直向上寻找直到根AnimatedNavHost,它控制所有目标和未指定目标的嵌套导航图的全局转换。

注意:这意味着如果想要在目标页面之间立即跳转,它应该返回EnterTransition.NoneExitTransition.None,表示不应该运行任何转场效果,而不是返回null

通过Modifier.zIndex可指定高度层级(值越大在屏幕上层级越高)转场动画时会根据层级进行覆盖(高层级的覆盖低层级的):

object Destinations {
    const val First = "first"
    const val Second = "second"
    const val Third = "third"
}

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun NavTestZIndexScreen() {
    val navController = rememberAnimatedNavController()
    AnimatedNavHost(navController, Destinations.First, Modifier.fillMaxSize()) {
        composable(
            Destinations.First,
            enterTransition = { NavigationTransition.slideInFromBottom }, // 当前页面入场动画
            exitTransition = { NavigationTransition.IdentityExit },       // 当前页面离场动画
            popEnterTransition = { NavigationTransition.IdentityEnter },  // 当前页面因弹栈而入场动画
            popExitTransition = { NavigationTransition.slideOutToBottom }, // 当前页面因弹栈离场动画
        ) {
            Button(onClick = { navController.navigate(Destinations.Second) }) {
                Text(text = "First", fontSize = 22.sp)
            }
        }
        composable(
            route = Destinations.Second,
            enterTransition = { NavigationTransition.slideInFromBottom }, // 当前页面入场动画
            exitTransition = { NavigationTransition.IdentityExit },       // 当前页面离场动画
            popEnterTransition = { NavigationTransition.IdentityEnter },  // 当前页面因弹栈而入场动画
            popExitTransition = { NavigationTransition.slideOutToBottom },  // 当前页面因弹栈离场动画
        ) {
            Button(onClick = { navController.navigate(Destinations.Third) },
                colors = ButtonDefaults.buttonColors(backgroundColor = Color.Yellow),
                modifier = Modifier.zIndex(100f)
            ) {
                Text(text = "Second", fontSize = 22.sp)
            }
        }
        composable(
            route = Destinations.Third,
            enterTransition = { NavigationTransition.slideInFromBottom }, // 当前页面入场动画
            exitTransition = { NavigationTransition.IdentityExit },       // 当前页面离场动画
            popEnterTransition = { NavigationTransition.IdentityEnter },  // 当前页面因弹栈而入场动画
            popExitTransition = { NavigationTransition.slideOutToBottom }, // 当前页面因弹栈离场动画
        ) {
            Button(onClick = { navController.popBackStack() },
                colors = ButtonDefaults.buttonColors(backgroundColor = Color.Blue),
                modifier = Modifier.zIndex(200f)
            ) {
                Text(text = "Third", fontSize = 22.sp)
            }
        }
    }
}

object NavigationTransition {
    private val animation: FiniteAnimationSpec<IntOffset> = tween(
        easing = LinearOutSlowInEasing,
        durationMillis = 1000,
    )

    val IdentityEnter = slideInVertically(
        initialOffsetY = {
            -1 // 保持不动 fix for https://github.com/google/accompanist/issues/1159
        },
        animationSpec = animation
    )

    val IdentityExit = slideOutVertically(
        targetOffsetY = {
            -1 // 保持不动 fix for https://github.com/google/accompanist/issues/1159
        },
        animationSpec = animation
    )

    var slideInFromBottom =
        slideInVertically(initialOffsetY = { fullHeight -> fullHeight }, animationSpec = animation)

    var slideOutToBottom =
        slideOutVertically(targetOffsetY = { fullHeight -> fullHeight }, animationSpec = animation)
}

在这里插入图片描述

Insets

dependencies {
    // accompanist-insets 已经废弃,相关功能已经集成到了 androidx.compose.foundation 包中,可以直接使用
    // implementation "com.google.accompanist:accompanist-insets:<version>"
    // accompanist-insets-ui 仍然可继续使用,它提供了一些支持设置 contentPadding 的 Material 组件
    implementation "com.google.accompanist:accompanist-insets-ui:0.28.0"
}

insets库主要用来调整系统状态栏、导航栏等的padding以更加友好的适配屏幕内的组件。本来accompanist提供了相应的支持库,但是现在已经标记为deprecated废弃了,因为其功能已经集成到了compose ui的sdk中了,不需要单独使用accompanist版本的insets库了。

相关设置:
1)在Activity中设置WindowCompat.setDecorFitsSystemWindows(window, false)禁止dector适应系统,交由我们自己去适配,同时需要使用 System UI Controller 库将系统栏的背景设为透明。

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 可以将内容延伸到系统状态栏的下面
        WindowCompat.setDecorFitsSystemWindows(window, false)
}

2)如果需要支持IME键盘相关的insets支持,需要在清单文件中添加如下配置:

<activity
      android:name=".MyActivity"
      android:windowSoftInputMode="adjustResize">
</activity>

3)修改Theme.kt文件

	// 修改状态栏和导航栏颜色为透明
    val systemUiController = rememberSystemUiController()
    SideEffect {
        systemUiController.setSystemBarsColor(Color.Transparent)
    }

此时可能会发现页面中的组件出现一些被遮挡的情况,如顶部的TopAppBar:
在这里插入图片描述
在添加accompanist-insets-ui依赖库之后,就可以使用其提供的com.google.accompanist.insets.ui.TopAppBar,它可以设置一个contentPadding,将contentPadding设置为WindowInsets.statusBars.asPaddingValues()即可

@Composable
fun ScaffoldExample() {
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("首页", color = MaterialTheme.colors.onPrimary) },
                contentPadding = WindowInsets.statusBars.asPaddingValues(),
            )
        },
        ...
	)
}

在这里插入图片描述
同样的可以修改bottomBarBottomNavigation替换成accompanist-insets-ui中的对应组件:

 Scaffold(
    topBar = {...},
  	bottomBar = {
       BottomNavigation(contentPadding = WindowInsets.navigationBars.asPaddingValues()) {...}
    }
    ...
}

在这里插入图片描述

常用的系统 inset bar:

  • WindowInsets.navigationBars
  • WindowInsets.statusBars
  • WindowInsets.ime
  • WindowInsets.systemBars
  • WindowInsets.displayCutout(挖孔)
  • WindowInsets.waterfall
  • WindowInsets.captionBar

如果根部局不是一个Scaffold而是其他Composable组件,例如:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        WindowCompat.setDecorFitsSystemWindows(window, false)
        setContent {
            MyComposeApplicationTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Box(Modifier.background(Color.Red)) {
                        Text("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", fontSize = 20.sp)
                    }
                }
                // ScaffoldExample()
            }
        }
 }

那么也可能会出现被状态栏遮挡内容的情况:
在这里插入图片描述
可以使用Modifier.windowInsetsPadding(WindowInsets.statusBars)来解决:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        WindowCompat.setDecorFitsSystemWindows(window, false)
        setContent {
            MyComposeApplicationTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Box(Modifier.background(Color.Red).windowInsetsPadding(WindowInsets.statusBars)) {
                        Text("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", fontSize = 20.sp)
                    }
                }
                // ScaffoldExample()
            }
        }
 }

在这里插入图片描述
或者直接设置Modifier.systemBarsPadding() 上下bar同时适配:

Box(Modifier.background(Color.Red).systemBarsPadding()) {
    Text("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", fontSize = 20.sp)
}

常用的系统 inset padding 修饰符:

  • Modifier.windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top))
  • Modifier.systemBarsPadding()
  • Modifier.statusBarsPadding()
  • Modifier.navigationBarsPadding()
  • Modifier.imePadding()
  • Modifier.displayCutoutPadding()
  • Modifier.navigationBarsPadding().imePadding()

由于状态栏被设置成了透明,所以另一种方案是可以放一个假的状态栏在我们自己的布局的顶部,例如:

Column {
   // 模拟一个状态栏
    Spacer(
        Modifier
            .background(Color.Red.copy(alpha = 0.7f))
            .windowInsetsTopHeight(WindowInsets.statusBars) // 匹配系统状态栏高度
            .fillMaxWidth())
    Box(Modifier.background(Color.Red).fillMaxSize()) {
        Text("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", fontSize = 20.sp)
    }
}

在这里插入图片描述
这样也可以达到类似的效果。

常用的系统 inset height 修饰符:

  • Modifier.windowInsetsTopHeight(WindowInsets.statusBars)
  • Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)
  • Modifier.windowInsetsStartWidth(WindowInsets.navigationBars) / Modifier.windowInsetsEndWidth(WindowInsets.navigationBars)
  • WindowInsets.statusBars.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top).asPaddingValues()
  • WindowInsets.ime.getBottom(LocalDensity.current)

IME相关适配:

来看以下代码:

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun IMEPaddingExample() {
    val listItems = remember { mutableStateListOf<String>().apply {
        addAll(('A'..'Z').toList().map { "$it".repeat(15) })
    } }
    Column(Modifier.systemBarsPadding()) {  
        Box(Modifier.weight(1f)) {
            LazyColumn(
                modifier = Modifier.fillMaxSize(),
                reverseLayout = true,
                verticalArrangement = Arrangement.spacedBy(10.dp)
            ) {
                items(listItems) { it ->
                    Card { Text(it, Modifier.fillMaxWidth().height(50.dp), fontSize = 22.sp) } }
            }
        }
        var value by remember { mutableStateOf("") }
        TextField(
            value = value,
            onValueChange = { value = it},
            modifier = Modifier.fillMaxWidth(),
            placeholder = { Text("IME is Visible: ${WindowInsets.isImeVisible}")},
            textStyle = TextStyle(fontSize = 20.sp)
        )
    }
}

运行以上代码会发现输入框被键盘遮挡:

在这里插入图片描述

注意:以上代码在清单文件中配置了Activity标签的android:windowSoftInputMode="adjustResize" 属性,如果windowSoftInputMode配置的属性值是adjustPan则以上代码输入框不会被键盘遮挡。

所以第一种解决方案就是设置android:windowSoftInputMode="adjustPan" 另外一种方法是我们可以通过Modifier.imePadding()或者Modifier.navigationBarsPadding().imePadding()修饰符来解决:

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun IMEPaddingExample() {
    val listItems = remember { mutableStateListOf<String>().apply {
        addAll(('A'..'Z').toList().map { "$it".repeat(15) })
    } }
    Column(Modifier.systemBarsPadding()) { // 关键点1
        Box(Modifier.weight(1f)) {
            LazyColumn(
                modifier = Modifier.fillMaxSize(),
                reverseLayout = true,
                verticalArrangement = Arrangement.spacedBy(10.dp)
            ) {
                items(listItems) { it ->
                    Card { Text(it, Modifier.fillMaxWidth().height(50.dp), fontSize = 22.sp) } }
            }
        }
        var value by remember { mutableStateOf("") }
        TextField(
            value = value,
            onValueChange = { value = it},
            modifier = Modifier.fillMaxWidth().imePadding(), // 关键点2
            placeholder = { Text("IME is Visible: ${WindowInsets.isImeVisible}")},
            textStyle = TextStyle(fontSize = 20.sp)
        )
    }
}

在这里插入图片描述
另外,在以上代码中使用了WindowInsets.isImeVisible这个api可以很方便的获取键盘显示隐藏的状态,这相比传统View体系真的要方便的多了,要知道在以前根本没有可靠的方法来判断键盘的显示和隐藏状态。

API 30+以上的设备上,支持使用Modifier.imeNestedScroll()来控制键盘的显示隐藏和LazyList列表进行联动效果的动画:

在这里插入图片描述

代码如下:

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun IMEScrollAnimationExample() {
    val listItems = remember { mutableStateListOf<String>().apply {
        addAll(('A'..'Z').toList().map { "$it".repeat(15) })
    } }
    Column(Modifier.statusBarsPadding().navigationBarsPadding()) { // 关键点1
        LazyColumn(
            modifier = Modifier.weight(1f).imeNestedScroll(), // 关键点2
            reverseLayout = true,
            verticalArrangement = Arrangement.spacedBy(10.dp)
        ) {
            items(listItems) { it ->
                Card { Text(it, Modifier.fillMaxWidth().height(50.dp), fontSize = 22.sp) } }
        }
        var value by remember { mutableStateOf("") }
        TextField(
            value = value,
            onValueChange = { value = it},
            modifier = Modifier.fillMaxWidth().imePadding(), // 关键点3
            placeholder = { Text("IME is Visible: ${WindowInsets.isImeVisible}")},
            textStyle = TextStyle(fontSize = 20.sp)
        )
    }
}

Safe Inset Api

什么是Safe Inset Api,比如屏幕顶部是显示相机挖孔屏的手机设备,我们的应用中的组件内容不应当绘制到该区域,因为该区域是看不到界面的。所以需要使用系统提供的一些api,在组件布局时绕开该区域的显示。

常用的Safe Insets:

  • WindowInsets.safeDrawing
  • WindowInsets.safeGestures
  • WindowInsets.safeContent

常用的Safe Insets Padding:

  • Modifier.safeDrawingPadding()
  • Modifier.safeContentPadding()
  • Modifier.safeGesturesPadding()

例如,以下代码中,第一层的Box组件排除了系统顶部状态栏区域的padding,第二层Box组件排除挖孔位置的padding区域,这样就使我们绕开了顶部状态栏和挖孔区
在这里插入图片描述

接下来就可以在第三层中“安全”地绘制我们真正的组件内容了:

在这里插入图片描述
compose.material3的许多组件中都内置了对inset的支持,所以未来最好还是使用Material3的组件而不是Material1的。

在这里插入图片描述
在这里插入图片描述

constraintlayout-compose

constraintlayout-compose是官方提供compose版本的Constraintlayout约束布局组件,但是这个库不在Accompanist组件包中,而是单独提供了一个依赖:

dependencies {
	implementation "androidx.constraintlayout:constraintlayout-compose:1.0.1"
}

ConstraintLayout官网介绍:https://developer.android.google.cn/jetpack/compose/layouts/constraintlayout

使用非常简单, 首先通过 createRefs()createRefFor() 创建引用,ConstraintLayout 中的每个元素都需要关联一个引用,然后使用 Modifier.constrainAs() 修饰符提供约束,将引用作为它的参数进行关联, 在 lambda 中指定其约束条件。约束条件是使用 linkTo() 或其他有用的方法指定的。

@Composable
fun ConstraintLayoutExp01() {
    ConstraintLayout {
        // 使用 createRefs() 或 createRefFor() 创建引用 
        val (button, text) = remember { createRefs() }

        Button(
            onClick = { /* Do something */ },
            // 使用 Modifier.constrainAs() 修饰符关联引用
            // 使用 linkTo() 方法指定的约束条件
            modifier = Modifier.constrainAs(button) {
                top.linkTo(parent.top, margin = 16.dp)  // 指定button顶部距离parent顶部为16dp
                centerHorizontallyTo(parent)
            }
        ) {
            Text("Button")
        }

        // 将引用 "text" 分配到 Text 组件,并指定其约束为顶部在button的底部,距离为16dp
        Text("Text", Modifier.constrainAs(text) {
            top.linkTo(button.bottom, margin = 16.dp)
            centerHorizontallyTo(parent) // 水平居中
        })
    }
}

在这里插入图片描述

@Composable
fun ConstraintLayoutExp02() {
    ConstraintLayout(
        Modifier
            .width(300.dp)
            .height(100.dp)
            .padding(10.dp)
    )  {
        val (headImg, nameText, descText) = remember { createRefs() }
        Image(
            painter = painterResource(id = R.drawable.ic_head),
            contentDescription = null,
            modifier = Modifier.constrainAs(headImg) {
                top.linkTo(parent.top)
                bottom.linkTo(parent.bottom)
                start.linkTo(parent.start)
            }
        )
        Text(
            text = "用户名",
            fontSize = 16.sp,
            maxLines = 1,
            modifier = Modifier.constrainAs(nameText) {
                top.linkTo(headImg.top)
                start.linkTo(headImg.end, 10.dp)
            }
        )
        Text(
            text = "个人描述个人描述个人描述个人描述个人描述个人描述aaa",
            fontSize = 14.sp,
            color = Color.Gray,
            fontWeight = FontWeight.Light,
            modifier = Modifier.constrainAs(descText) {
                top.linkTo(nameText.bottom, 5.dp)
                start.linkTo(headImg.end, 10.dp)
                end.linkTo(parent.end, 5.dp)
                width = Dimension.fillToConstraints
            }
        )
    }
}

在这里插入图片描述

Barrier分界线使用:

@Composable
fun BarrierDemo() {
    ConstraintLayout(
        modifier = Modifier
            .width(400.dp)
            .padding(10.dp)
    ) {
        val (usernameTextRef, passwordTextRef, usernameInputRef, passWordInputRef, dividerRef) = remember { createRefs() }
        val barrier = createEndBarrier(usernameTextRef, passwordTextRef)
        Text(
            text = "用户名",
            fontSize = 14.sp,
            textAlign = TextAlign.Left,
            modifier = Modifier
                .constrainAs(usernameTextRef) {
                    top.linkTo(parent.top)
                    start.linkTo(parent.start)
                }
        )
        Divider(
            Modifier
                .fillMaxWidth()
                .constrainAs(dividerRef) {
                    top.linkTo(usernameTextRef.bottom)
                    bottom.linkTo(passwordTextRef.top)
                })
        Text(
            text = "密码",
            fontSize = 14.sp,
            modifier = Modifier
                .constrainAs(passwordTextRef) {
                    top.linkTo(usernameTextRef.bottom, 19.dp)
                    start.linkTo(parent.start)
                }
        )
        OutlinedTextField(
            value = "",
            onValueChange = {},
            modifier = Modifier.constrainAs(usernameInputRef) {
                start.linkTo(barrier, 10.dp)
                top.linkTo(usernameTextRef.top)
                bottom.linkTo(usernameTextRef.bottom)
                height = Dimension.fillToConstraints
            }
        )
        OutlinedTextField(
            value = "",
            onValueChange = {},
            modifier = Modifier.constrainAs(passWordInputRef) {
                start.linkTo(barrier, 10.dp)
                top.linkTo(passwordTextRef.top)
                bottom.linkTo(passwordTextRef.bottom)
                height = Dimension.fillToConstraints
            }
        )
    }
}

在这里插入图片描述

Guideline引导线:

@Composable
fun GuidelineDemo() {
    ConstraintLayout(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.LightGray),
    ) {
        val (userPortraitBackgroundRef, userPortraitImgRef, welcomeRef) = remember { createRefs() }
        val guideLine = createGuidelineFromTop(0.2f)
        Box(modifier = Modifier
            .constrainAs(userPortraitBackgroundRef) {
                top.linkTo(parent.top)
                bottom.linkTo(guideLine)
                height = Dimension.fillToConstraints
                width = Dimension.matchParent
            }
            .background(Color(0xFF1E9FFF))
        )
        Image(painter = painterResource(id = R.drawable.ic_head),
            contentDescription = "portrait",
            modifier = Modifier
                .constrainAs(userPortraitImgRef) {
                    top.linkTo(guideLine)
                    bottom.linkTo(guideLine)
                    start.linkTo(parent.start)
                    end.linkTo(parent.end)
                }
                .size(100.dp)
                .clip(CircleShape)
                .border(width = 2.dp, color = Color(0xFF5FB878), shape = CircleShape))
        Text(
            text = "Compose 技术爱好者",
            color = Color.White,
            fontSize = 26.sp,
            modifier = Modifier.constrainAs(welcomeRef) {
                top.linkTo(userPortraitImgRef.bottom, 20.dp)
                start.linkTo(parent.start)
                end.linkTo(parent.end)
            }
        )
    }
}

在这里插入图片描述

Chain连接约束:

@Composable
fun ChainDemo(chainStyle: ChainStyle = ChainStyle.Spread) {
    ConstraintLayout(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.Gray)
    ) {
        val (quotesFirstLineRef, quotesSecondLineRef, quotesThirdLineRef, quotesForthLineRef) = remember { createRefs() }
        createVerticalChain(quotesFirstLineRef, quotesSecondLineRef, quotesThirdLineRef, quotesForthLineRef,
            chainStyle = chainStyle)
        Text(
            text = "寄蜉蝣于天地,",
            color = Color.White,
            fontSize = 30.sp,
            fontWeight = FontWeight.Bold,
            modifier = Modifier.constrainAs(quotesFirstLineRef) {
                start.linkTo(parent.start)
                end.linkTo(parent.end)
            }
        )
        Text(
            text = "渺沧海之一粟。",
            color = Color.White,
            fontSize = 30.sp,
            fontWeight = FontWeight.Bold,
            modifier = Modifier.constrainAs(quotesSecondLineRef) {
                start.linkTo(parent.start)
                end.linkTo(parent.end)
            }
        )

        Text(
            text = "哀吾生之须臾,",
            color = Color.White,
            fontSize = 30.sp,
            fontWeight = FontWeight.Bold,
            modifier = Modifier.constrainAs(quotesThirdLineRef) {
                start.linkTo(parent.start)
                end.linkTo(parent.end)
            }
        )
        Text(
            text = "羡长江之无穷。",
            color = Color.White,
            fontSize = 30.sp,
            fontWeight = FontWeight.Bold,
            modifier = Modifier.constrainAs(quotesForthLineRef) {
                start.linkTo(parent.start)
                end.linkTo(parent.end)
            }
        )
    }
}

@Preview(showBackground = true)
@Composable
fun ChainDemoPreview() {
    ChainDemo(ChainStyle.Spread)
}

@Preview(showBackground = true)
@Composable
fun ChainDemoPreview2() {
    ChainDemo(ChainStyle.SpreadInside)
}

@Preview(showBackground = true)
@Composable
fun ChainDemoPreview3() {
    ChainDemo(ChainStyle.Packed)
}

在这里插入图片描述

coil 图片加载

coil 图片加载库不属于Accompanist库的一部分,是一个三方库,但是由于图片加载比较常用,这里放一起记录一下

dependencies {
 	implementation "io.coil-kt:coil-compose:2.2.2"
    implementation "io.coil-kt:coil-svg:2.2.2"
    implementation "com.github.skydoves:landscapist-coil:2.0.3"
}

coil (Coroutine Image Loader)主要是基于kotlin协程框架的图片加载器,相比于传统的基于View体系的Glide等图片加载框架,它更加适合于Compose。

可以直接使用coil 提供的AsyncImageCompose组件加载网络图片,或通过rememberAsyncImagePainter结合Image组件使用:

@Composable
fun CoilImageLoaderExample() {
    Row {
        Image(
            painter = rememberAsyncImagePainter("https://picsum.photos/300/300"),
            contentDescription = null
        )
        AsyncImage(
            model = "https://picsum.photos/300/300",
            contentDescription = null
        )
        AsyncImage(
            model = ImageRequest.Builder(LocalContext.current)
                .data("https://picsum.photos/300/300")
                .crossfade(true)
                .build(),
            placeholder = painterResource(R.drawable.ic_launcher_background),
            contentDescription = null,
            contentScale = ContentScale.Crop,
            // error = painterResource(),
            onSuccess = { success ->  
                
            },
            onError = { error ->
                      
            },
            onLoading = { loading ->
                        
            },
            modifier = Modifier.clip(CircleShape)
        )
    }
}

使用SubcomposeAsyncImage进行图片加载,通过painter.state可以判断当前状态是loadingerror还是success

@Composable
fun CoilImageLoaderExample2() {
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        SubcomposeAsyncImage(
            model = "https://picsum.photos/350/350" ,
            loading = { CircularProgressIndicator() },
            contentDescription = null,
            modifier = Modifier.size(200.dp)
        )
        SubcomposeAsyncImage(
            model = "https://picsum.photos/400/400" ,
            contentDescription = null,
            modifier = Modifier.size(200.dp)
        ) {
            val state = painter.state
            when(state) {
                is AsyncImagePainter.State.Loading -> CircularProgressIndicator()
                is AsyncImagePainter.State.Error -> Text("${state.result.throwable}")
                is AsyncImagePainter.State.Success -> SubcomposeAsyncImageContent()
                is AsyncImagePainter.State.Empty -> Text("Empty")
            }
        } 
    } 
}

SubcomposeAsyncImage会根据组件的约束空间来确定图片的最终大小,这说明在图片装载前,需要预先获取SubcomposeAsyncImage的约束信息, 而Subcomposelayout可以在子组件合成前,获取到父组件的约束信息或其他组件的约束信息。SubcomposeAsyncImage就是依靠Subcomposelayout的能力来实现的,子组件就是我们传入的content内容,它会在SubcomposeAsyncImage组件测量时进行组合。

如果指定了图片加载到内存时的尺寸大小,那么在加载时就不会再获取组件的约束信息:

@Composable
fun CoilImageLoaderExample2() {
        SubcomposeAsyncImage(
            model = ImageRequest.Builder(LocalContext.current)
                .data("https://picsum.photos/800/600")
                .size(800, 600)
                .crossfade(true)
                .build(),
            contentDescription = null,
        ) {
            val state = painter.state
            when(state) {
                is AsyncImagePainter.State.Loading -> CircularProgressIndicator()
                is AsyncImagePainter.State.Error -> Text("${state.result.throwable}")
                is AsyncImagePainter.State.Success -> SubcomposeAsyncImageContent()
                is AsyncImagePainter.State.Empty -> Text("Empty")
            }
        }
    }

}

另外,Coil也支持矢量图SVG的加载:

@Composable
fun CoilSVGExample() {
    Row {
        // 加载网络svg
        val context = LocalContext.current
        val imageLoader = ImageLoader.Builder(context)
            .components { add(SvgDecoder.Factory()) }
            .build()
        Image(
            painter = rememberAsyncImagePainter (
                "https://coil-kt.github.io/coil/images/coil_logo_black.svg",
                imageLoader = imageLoader
            ),
            contentDescription = null,
            modifier = Modifier.size(100.dp)
        ) 
        var flag by remember { mutableStateOf(false) }
        val size by animateDpAsState(targetValue = if(flag) 300.dp else 100.dp)
        CoilImage(
            imageModel = { "https://coil-kt.github.io/coil/images/coil_logo_black.svg" },
            imageOptions = ImageOptions(
                contentScale = ContentScale.Crop,
                alignment = Alignment.Center
            ),
            modifier = Modifier
                .size(size)
                .clickable(
                    onClick = { flag = !flag },
                    indication = null,
                    interactionSource = MutableInteractionSource()
                ),
            imageLoader = { imageLoader }
        )
    }
}

svg放大和缩小使用Coil有问题,不是矢量图,可以使用Landscapist:https://github.com/skydoves/Landscapist

Cloudy Blur模糊效果

Cloudy 是一个专门处理Jectpack Compose中的Blur高斯模糊效果的支持库,它可以向后兼容低版本,由于官方的SDK中的Modifier.blur()修饰符只能支持运行在Android 12+的设备上才有效果,所以可以使用该库做兼容支持。

Cloudy 也不属于 Accompanist 库的一部分,是一个三方库,这里仅做记录。

首先看一下系统自带的Modifier.blur()修饰符的效果:

// Modifier.blur() only supported on Android 12+ (API 31)
@Preview(showBackground = true)
@Composable
fun ModifierBlurExample() {
    Column(
        Modifier.padding(15.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        var progress by remember{ mutableStateOf(0f)}
        val radius by animateDpAsState(targetValue = (progress * 10f).dp)
        Text(
            text = "高斯模糊效果".repeat(10),
            Modifier.blur(
                    radius = radius,
                    edgeTreatment = BlurredEdgeTreatment.Unbounded
            ),
            fontSize = 20.sp
        )
        Image(
            painter = painterResource(id = R.drawable.ic_sky),
            contentDescription = null,
            modifier = Modifier
                .height(200.dp)
                .fillMaxWidth()
                .blur(
                    radius = radius,
                    edgeTreatment = BlurredEdgeTreatment.Unbounded
                ),
        )
        Slider(
            value = progress,
            onValueChange = { progress = it },
        )
    }
}

在这里插入图片描述
效果还是很不错的,其中Modifier.blur()修饰符支持的模糊半径radius参数是一个dp值。

然后再看一下 Cloudy 库的使用:

dependencies {
	implementation "com.github.skydoves:cloudy:0.1.1"
}
@Composable
fun BlurByCloudyLibExample() {
    Column(
        Modifier.padding(15.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        var radius by remember{ mutableStateOf(0)}
        // radius支持范围是[0..25]
        Cloudy(radius = radius) {
            Column {
                AsyncImage(
                    model = "https://picsum.photos/300/200",
                    contentDescription = null,
                    onSuccess = { radius = 1 },
                    modifier = Modifier
                        .clip(RoundedCornerShape(15))
                        .fillMaxWidth()
                )
                Text(text = "高斯模糊效果".repeat(10), fontSize = 20.sp)
            }
        }
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceEvenly,
            verticalAlignment = Alignment.CenterVertically
        ) {
            Button(onClick = { if (radius > 0) radius-- }) { Text(text = "Minus") }
            Text(text = "radius: $radius", fontSize = 20.sp)
            Button(onClick = { if (radius < 25) radius++ }) { Text(text = "Add") }
        }
    }

}

在这里插入图片描述
Cloudy 库的使用也很简单,使用提供的Composable组件Cloudy将需要应用Blur效果的组件包起来即可,它也提供一个radius模糊半径参数,不过这个radius是一个Int值,且范围是[0, 25]


参考资料:

  • Accompanist官网:https://github.com/google/accompanist

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/133618.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

阳后买不到温度计 那么自己diy!(已开源)

这里写目录标题一 说明二 成品效果三 硬件材料四 硬件连接五 软件六 3D外盒模型一 说明 前段时间放开疫情后&#xff0c;身边人基本都阳了&#xff0c;自己也不出所料阳了&#xff0c;然后去药店买温度计&#xff0c;发现买不到&#xff0c;网上的买了也不发货&#xff0c;但是…

7.JS笔记-数组

1.数组的概念 使用数组Array可以把一组相关的数据存放在一起&#xff0c;并提供方便的获取方式。 数组是一组数据的集合&#xff0c;其中的每个数据被称作是元素&#xff0c;在数组中可以存放任意类型的元素。数组是一种将数据存储在单个变量名下的方式 2.创建数组 利用new关…

【Linux】Linux进程的理解 --- 进程状态、优先级、切换…

如果不改变自己&#xff0c;就别把跨年搞的和分水岭一样&#xff0c;记住你今年是什么吊样&#xff0c;明年就还会是什么吊样&#xff01;&#xff01;&#xff01; 文章目录一、冯诺依曼体系结构&#xff08;硬件&#xff09;二、操作系统&#xff08;软件&#xff09;1.操作…

git笔记2:Git基本理论,项目创建及克隆

目录 一、工作区域 二、工作流程 三、本地仓库搭建 1、创建全新的仓库 2、克隆远程仓库 一、工作区域 Git本地有三个工作区域&#xff1a; 工作目录&#xff08;Working Directory&#xff09;&#xff1a;平时存放代码的地方暂存区&#xff08;Stage/Index&#xff09;&a…

sec6-可派生类型和抽象类型

可派生类型 有两种类型&#xff0c;final类型和derivable类型。final类型没有任何子对象。derivable有子对象。 这两个对象之间的主要区别是它们的类。final类型对象没有自己的类区域。类的唯一成员是它的父类。 派生对象在类中有自己的区域。该类对其子类开放。 G_DECLARE…

【python系列】第三章 基本数据类型

*该系列内容来自于&#xff1a;中国大学MOOC&#xff08;幕客&#xff09;-python语言程序设计 ​​​​​​Python语言程序设计_北京理工大学_中国大学MOOC(慕课) 第三章 基本数据类型 方法论&#xff1a;Python语言数字及字符串类型 实践能力&#xff1a;初步学会编程进行…

236. 二叉树的最近公共祖先 - 力扣[LeetCode]

目录 如果二叉树是二叉搜索树&#xff1a; 如果是普通的二叉树 【方法一】子树判断法 【方法二】路径确定 【方法三】递归 面对此类型的公共祖先问题&#xff0c;可以分为以下几类情况讨论 如果二叉树是二叉搜索树&#xff1a; a. 如果树是空&#xff0c;直接返回nullpt…

分布式存储系统 Ceph 介绍与环境部署

文章目录一、概述二、Ceph 架构三、Ceph核心组件介绍四、Ceph 三种存储类型1&#xff09;块存储服务(RBD)2&#xff09;文件系统存储服务(CephFS)3&#xff09;对象存储服务(RGW)五、Ceph版本发行生命周期六、Ceph 集群部署1&#xff09;集群部署规划2&#xff09;前期准备1、关…

Python--让我们秀翻算法中的二进制

我相信大家在leetcode刷题或者更好的国外天梯刷题的时候应该经常能看到 **<<,>>,|,&**在我们的if里面构成了一个判断的条件. 然后在大家看不懂情况下就莫名其妙的把题目作对了!!! 所以我们准备持续的更新一下,二进制的用法. 大家要明白一个道理.一切的工具它…

Excel聚光灯--双箭头指示

实例需求&#xff1a;在工作表中核对数据时&#xff0c;虽然行列标题都会高亮显示&#xff0c;但是似乎并不明显&#xff0c;因此添加两个列标到活动单元格的箭头&#xff0c;以便于更好的定位。 选中单个单元格效果如下图所示。 选中多个单元格的效果如下图所示。 示例代码如…

51单片机——输出可调PWM

PWM控制在很多地方都会用到&#xff0c;比如使用PWM来控制电机的速度&#xff0c;使用PWM来生成想要的波形。 一、PWM PWM即脉冲宽度调制&#xff0c;在具有惯性的系统中&#xff0c;可以通过对一系列脉冲的宽度进行调制&#xff0c;来等效的获得所需要的模拟参量&#xff1b…

[JavaEE]线程的状态与安全

专栏简介: JavaEE从入门到进阶 题目来源: leetcode,牛客,剑指offer. 创作目标: 记录学习JavaEE学习历程 希望在提升自己的同时,帮助他人,,与大家一起共同进步,互相成长. 学历代表过去,能力代表现在,学习能力代表未来! 目录 1. 线程状态 1.1 观察线程的所有状态 1.2 线程的…

k-means算法进行数据分析应用

简介 kmeans算法又名k均值算法,K-means算法中的k表示的是聚类为k个簇&#xff0c;means代表取每一个聚类中数据值的均值作为该簇的中心&#xff0c;或者称为质心&#xff0c;即用每一个的类的质心对该簇进行描述。 其算法思想大致为&#xff1a;先从样本集中随机选取 k…

【王道操作系统】2.3.3 实现临界区进程互斥的硬件实现方法

实现临界区进程互斥的硬件实现方法 文章目录实现临界区进程互斥的硬件实现方法1.中断隐藏方法2.TestAndSet指令3.Swap指令1.中断隐藏方法 2.TestAndSet指令 执行TSL指令时&#xff0c;它的内部运转逻辑&#xff1a;假设lock现在为false&#xff0c;代表临界资源A空闲&#xff…

AssertionError: Torch not compiled with CUDA enabled解決方案

在執行pytorch代碼的時候&#xff0c;突然看到報錯 AssertionError: Torch not compiled with CUDA enabled 這說明了 1. 你pytoch确实安装了 2. 你安装的是cpu版本 作为验证&#xff0c;你可以在python编辑器输入下列代码 解决方案 首先&#xff0c;安装Nvidia toolkit su…

(深度学习快速入门)第二章:从线性神经网络入手深度学习(波士顿房价案例)

文章目录一&#xff1a;波士顿房价预测数据集说明二&#xff1a;Pytorch搭建模型&#xff08;1&#xff09;数据处理&#xff08;2&#xff09;网络结构&#xff08;3&#xff09;损失函数&#xff08;4&#xff09;优化方法&#xff08;5&#xff09;训练预测&#xff08;6&am…

pytorch应用(入门4)MLP实现MNIST手写数字分类

深层神经网络 前面一章我们简要介绍了神经网络的一些基本知识&#xff0c;同时也是示范了如何用神经网络构建一个复杂的非线性二分类器&#xff0c;更多的情况神经网络适合使用在更加复杂的情况&#xff0c;比如图像分类的问题&#xff0c;下面我们用深度学习的入门级数据集 M…

MyBatisPlus ---- 条件构造器和常用接口

MyBatisPlus ---- 条件构造器和常用接口1. wapper介绍2. QueryWrappera>例1&#xff1a;组装查询条件b>例2&#xff1a;组装排序条件c>例3&#xff1a;组装删除条件d>例4&#xff1a;条件的优先级e>例5&#xff1a;组装select子句f>例6&#xff1a;实现子查询…

dubbo源码实践-SPI扩展-自适应扩展机制

目录 1 前提必备知识 2 术语定义 3 自适应扩展机制的特点 4 扩展点实践 4.1 用户自定义自适应扩展 4.2 dubbo生成自适应扩展 4 自适应扩展类的用途 1 前提必备知识 具体的使用和原理就不说了&#xff0c;网上有人写的挺好的了。 可以参考&#xff1a; Dubbo SPI之自适…

【北京理工大学-Python 数据分析-1.1】

数据维度 维度&#xff1a;一组数据的组织形式 一维数据&#xff1a;由对等关系的有序或无序数据构成&#xff0c;采用线性组织形式。包括列表、集合和数组&#xff08;python中不常见&#xff0c;但在C和Java中比较常见&#xff09;类型。 列表&#xff1a;数据类型可以不同…