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)
}
}
}
HorizontalPager
和 VerticalPager
背后是基于 LazyRow
和 LazyColumn
实现的,不在当前屏幕显示的页面会从容器中移除。
contentPadding
HorizontalPager
和 VerticalPager
支持设置 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的作用域内允许应用轻松引用currentPage
和currentPageOffset
这些值来计算动画效果。官方提供了一个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