Jetpack Compose中的Accompanist

Posted 川峰

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Jetpack Compose中的Accompanist相关的知识,希望对你有一定的参考价值。

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 以上是关于Jetpack Compose中的Accompanist的主要内容,如果未能解决你的问题,请参考以下文章

JetPack Compose 基础(3)Compose 中的主题

Jetpack Compose 中的作用域状态

Jetpack Compose中的手势操作

Jetpack Compose中的Accompanist

Jetpack Compose中的Canvas

没有从 jetpack compose 中的 rememberLauncherForActivityResult() 获取结果