实战项目
视频地址:https://www.bilibili.com/video/BV1aS4y1D7dv/?vd_source=3065178ab11a8e498478d23c269ac3a0
知识点
-
状态栏颜色配置:res --> values --> themes.xml
<style name="Theme.Worldlet" parent="android:Theme.Material.Light.NoActionBar"> <item name="android:statusBarColor">@color/purple_700</item> </style>
-
获取系统的AppBar高度可以通过查看
TopAppBar
源码,即56dp -
设置系统状态栏透明
-
方案一
-
引入依赖
// 可以设置System UI,例如系统状态栏的颜色和显示隐藏 dependencies { implementation "com.google.accompanist:accompanist-systemuicontroller:<version>" }
-
在Activity中
// 让内容显示在状态栏和系统导航栏后面,状态栏和导航栏会遮盖部分内容(在不同机型上) WindowCompat.setDecorFitsSystemWindows(window,false)
-
在themes.xml中
<style name="Theme.Worldlet" parent="android:Theme.Material.Light.NoActionBar"> <item name="android:statusBarColor">@color/purple_700</item> <item name="android:windowTranslucentStatus">true</item> </style>
-
-
方案二
// 处理不同机型,状态栏不透明问题 window.statusBarColor = Color.Transparent.value.toInt() // 处理不同机型,导航栏遮盖内容问题(已过时) // systemUiVisibility只能设置一个值,如果想要设置多个值,则需要这些值进行或运算(Java中是|,Kotlin中是or) window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
-
方案三
-
引入依赖
dependencies { implementation "com.google.accompanist:accompanist-systemuicontroller:<version>" }
-
设置状态栏透明
val systemUiController = rememberSystemUiController() LaunchedEffect(key1 = Unit) { systemUiController.setStatusBarColor(Color.Transparent) }
-
设置导航栏,防止遮挡:将用
Modifier.navigationBarsPadding()
修饰注意:
BottomNavigation
外围要有ProvideWindowInsets
包裹
-
-
-
获取系统状态栏高度
-
方案一
// 此方案只能在Activity中获取,如果其他组件需要,只能一步步向下传递 var statusHeight = 0 val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android") if (resourceId) { // 获取到的为Int,利用下一步转为dp statusHeight = resources.getDimensionPixelSize(resourceId) }
-
方案二
-
引入依赖
dependencies { implementation "com.google.accompanist:accompanist-insets:<version>" }
-
在需要的组件中
val statusBarHeightDp = with(LocalDensity.current) { LocalWindowInsets.current.statusBars.top.toDp() }
-
-
-
转换
px
为dp
val statusBarHeightDp = with(LocalDensity.current) { // statusBarHeight为Int,density:屏幕密度 statusBarHeight.toDp() }
-
Modifier.then()
函数可以将两个Modifier合并 -
依赖文件中,如果想使用定义的变量,则依赖项要用双引号包裹
dependencies { // 正确 implementation "com.google.accompanist:accompanist-insets:<version>" // 错误 implementation 'com.google.accompanist:accompanist-insets:<version>' }
-
获取屏幕宽度或高度
var otherSize: Int with(LocalConfiguration.current) { otherSize = screenWidthDp / 2 }
-
Modifier.offset()
可以为负值,用于在padding
不合适时进行负偏移(padding
不能为负值),但只能用在父组件上,用于控制子组件 -
在使用
Row
时,horizontalArrangement
生效的前提是要有宽度限定,同理,在使用Colum
时,verticalArrangement
要有高度限定 -
Modifier.weight(weight: Float, fill: Boolean)
如果
fill
为true,元素将占据分配的整个宽度,反之,包裹内容即可 -
dp
和px
的转换关系:dpValue = valueInPixel / density
-
在 Compose 中使用 View ,使用
AndroidView
可组合项,可以直接调用相应的View组件,也可以从XML加载// 直接调用 AndroidView(factory = { context -> TXCloudVideoView(context).apply { vodPlayer.setPlayerView(this) } }) // XML加载 AndroidView(factory = { context -> (LayoutInflater.from(context).inflate(R.layout.video, null, false) .findViewById(R.id.videoView) as TXCloudVideoView).apply { vodPlayer.setPlayerView(this) } })
-
Compose 中横竖屏切换
// 定义扩展函数 fun Context.findActivity(): Activity? = when (this) { is Activity -> this is ContextWrapper -> baseContext.findActivity() else -> null } // 控制横竖屏 val configuration = LocalConfiguration.current val context = LocalContext.current if (configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { context.findActivity()?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE } else { context.findActivity()?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT }
<!-- 在 AndroidManifest.xml 中配置启动时强制竖屏 --> <activity android:name=".MainActivity" android:exported="true" android:label="@string/app_name" android:screenOrientation="portrait" android:theme="@style/Theme.App"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>
-
rememberSaveable
会自动保存可保存在Bundle
中的任何值,对于其他值,可以将其传入自定义 Saver 对象,实现save
和restore
方法,对于save
方法的返回值类型,要可序列化。通过以下方式使类可序列化:// 在 build.gradle(Project:App) 中 plugins { id 'org.jetbrains.kotlin.plugin.parcelize' version '1.6.10' apply false } // 在 build.gradle(Project:App.app) 中 plugins { id 'org.jetbrains.kotlin.plugin.parcelize' }
import android.os.Parcelable import kotlinx.parcelize.Parcelize @Parcelize class MyClass : Parcelable { ... }
-
监听生命周期
val lifecycleOwner = LocalLifecycleOwner.current DisposableEffect(vodController) { val lifecycleEventObserver = LifecycleEventObserver { _, event -> when (event) { Lifecycle.Event.ON_RESUME -> vodController.resume() Lifecycle.Event.ON_PAUSE -> vodController.pause() else -> {} } } lifecycleOwner.lifecycle.addObserver(lifecycleEventObserver) onDispose { lifecycleOwner.lifecycle.removeObserver(lifecycleEventObserver) vodController.stopPlay() } }
-
监听返回键
BackHandler(enable = true) { // 所要进行的操作 }
注意:使用上述组件会拦截返回键,也就是说点击返回键只会执行上述操作,默认的操作将丢失,需要在上述操作中添加对默认操作的处理,也可以使用
enabled
参数使BackHandler
只在特定情况下启用 -
ViewModel
中调用挂起函数可以使用viewModelScope.launch { }
来提供协程作用域 -
将 Compose 的 State 作为
LaunchedEffect
的key
时,需要使用snapshotFlow
将 State 转换为 Flow ,否则监听不到变化参考文档:https://developer.android.google.cn/jetpack/compose/side-effects#snapshotFlow
Modifier
函数 | 描述 |
---|---|
clickable | 添加点击事件,默认会有水波纹效果,可以通过传递参数 interactionSource = remember { MutableInteractionSource() } , indication = null 取消 |
placeholder | 添加占位符,需要使用第三方库 Placeholder |
组件
名字 | 描述 | 使用 |
---|---|---|
BottomSheetScaffold | 脚手架 | 类似于 Scaffold ,不过自带一个BottomSheet,默认高度 56.dp ,可以设为 0.dp ,然后设置底部弹窗,需要设置 scaffoldState ( rememberBottomSheetScaffoldState() 获取),然后可以控制其状态设置弹窗是否显示 |
Canvas | 画布 | 画弧形(drawArc)等,Canvas 上单位为 px ,Int.dp.toPx() 可以转换单位(也可以画其他的比如折线图,但没有相应的方法,需要自己计算怎么画) |
CircularProgressIndicator | 环形加载指示器 | |
Divider | 分割线 | 在组件之间显示分割线 |
LazyColumn | 懒加载列 | 内部可以使用item 添加单个项,也可以使用items 循环添加 |
LazyHorizontalGrid | 懒加载网格 | 先排列列,使用 GridCells.Fixed 设置固定行数,使用 GridCells.Adaptive 设置动态行数 |
LazyVerticalGrid | 懒加载网格 | 先排列行,使用 GridCells.Fixed 设置固定列数,使用 GridCells.Adaptive 设置动态列数 |
LeadingIconTab | 带图标的单个分类 | TabRow 子项 |
Scaffold | 脚手架 | 页面框架,在设置bottomBar 等参数后,Scaffold 会算出一个剩余空间传入content |
Slider | 滑块 | 实现可拖动进度条、档次选择(例如字体小、大、超大)等功能,注意实现档次选择功能时其 steps 参数为中间的档次数,例如上例应为1 |
Spacer | 空白空间 | 可以用来控制组件间距 |
Tab | 单个分类 | TabRow 子项 |
TabRow | 分类标签 | 显示多个分类 |
TextField | 输入框 | visualTransformation 可以配置视觉效果,如密码显示为点keyboardOptions 可以配置键盘选项colors 可以配置输入框的各种颜色,包括聚焦状态、指示器 |
实战经验
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SeqPVdEr-1670167793715)(https://api.onedrive.com/v1.0/shares/s!AoF7mN_tKB-G0B5VxMtOoyIqXgEQ/root/content)]
需要引入依赖
dependencies { implementation "android.lifecycle:lifecycle-runtime-ktx:<version>" implementation "android.lifecycle:lifecycle-viewmodel-compose:<version>" }
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mEeeRaZk-1670167793716)(https://api.onedrive.com/v1.0/shares/s!AoF7mN_tKB-G0B0dhekJjjumYZjg/root/content)]
-
一般ViewModel和Entity是一对多的关系,一个页面可能会用到几个Entity,但一般只有一个ViewModel
-
图片的纵横比(aspectRatio)一般为7/3或16/9
-
设置轮播图无限滑动
@OptIn(ExperimentalPagerApi::class) @Composable fun SwiperContent(vm: MainViewModel) { //虚拟页数 val virtualCount = Int.MAX_VALUE //实际页数 val actualCount = vm.swiperData.size //初始图片下标 val initialIndex = virtualCount / 2 val pagerState = rememberPagerState(initialPage = initialIndex) val coroutineScope = rememberCoroutineScope() //自动滚动 DisposableEffect(Unit) { coroutineScope.launch { vm.swiperData() } val timer = Timer() timer.schedule(object : TimerTask() { override fun run() { if (vm.swiperLoaded) { coroutineScope.launch { pagerState.animateScrollToPage(pagerState.currentPage + 1) } } } }, 3000, 3000) onDispose { timer.cancel() } } HorizontalPager( count = virtualCount, state = pagerState, modifier = Modifier .padding(horizontal = 8.dp) .clip(RoundedCornerShape(8.dp)), userScrollEnabled = vm.swiperLoaded ) { index -> val actualIndex = (index - initialIndex).floorMod(actualCount) //index - (index.floorDiv(actualCount)) * actualCount AsyncImage( model = vm.swiperData[actualIndex].imageUrl, contentDescription = null, modifier = Modifier .fillMaxWidth() .aspectRatio(7 / 3f) .placeholder( visible = !vm.swiperLoaded, highlight = PlaceholderHighlight.shimmer() ), contentScale = ContentScale.Crop ) } } fun Int.floorMod(other: Int): Int = when (other) { 0 -> this //将虚拟数据按照实际数据总数分为 N 组 //当前虚拟下标是在这虚拟数据中的哪一组:虚拟下标floorDiv实际数据总数(虚拟下标/实际数据总数)。向下取整 //虚拟下标 - (虚拟下标/实际数据总数) * 实际数据总数 else -> this - floorDiv(other) * other }
-
WebView
-
Compose还没有
WebView
组件,只能通过xml布局的WebView
来实现
-
当以函数做参数且可以为空时,不可以直接调用(例如
content()
),这时可以使用content?.invoke()
调用,其中content()
和content.invoke()
效果一样 -
因为 Jetpack Compose 有很多实验性API,项目开始之前,先确定好使用的版本,否则将来开发过程中再改变版本,可能会出现实验性API被改变,导致不必要的重构。如果项目开发过程中确实要改变版本,且遇到了实验性API的改变,可以去官方文档查看实验性API的改变情况
-
视频播放器可以用
Box
包裹,因为视频播放器会有一些控制按钮(暂停、倍速等),而Box
可以将组件堆叠起来,也就是说一层视频播放层,一层视频控制层,视频控制层在视频播放层上面参考实现:https://gitee.com/RandyWei/jetpack-compose-case/blob/main/App/app/src/main/java/icu/bughub/app/app/ui/components/video/VideoPlayer.kt
-
复杂渐变实现
// 实现思路:在右上角向左下角渐变,从黄色到透明,再从左下角向右上角渐变,从蓝色到透明,即堆叠两个 // 也可以将最外层的 BoxWithConstraints 换成 Box ,但要手动获取屏幕的宽高,并转换为 px BoxWithConstraints(modifier = Modifier.fillMaxSize()) { //背景图层 Image( painter = painterResource(id = R.drawable.bg), contentDescription = null, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop ) //右上往左下渐变层 Box( modifier = Modifier .fillMaxSize() .background( // 可以实现从指定位置到指定位置的渐变 Brush.linearGradient( listOf(Color(0xffbb8378), Color.Transparent), // BoxWithConstraints 中可以直接获取屏幕的宽高( BoxWithConstraints 铺满屏幕的前提下) start = Offset(x = constraints.maxWidth.toFloat(), y = 0f), end = Offset(x = 0f, y = constraints.maxHeight.toFloat()) ) ) ) //左下往右上渐变层 Box( modifier = Modifier .fillMaxSize() .background( Brush.linearGradient( listOf(Color(0xFF149EE7), Color.Transparent), start = Offset(x = 0f, y = constraints.maxHeight.toFloat()), end = Offset(x = constraints.maxWidth.toFloat(), y = 0f) ) ) ) }
-
到达底部后加载更多
参考实现:https://www.bilibili.com/video/BV1aS4y1D7dv/?p=47&vd_source=3065178ab11a8e498478d23c269ac3a0
第三方库
-
Accompanist:Jetpack Compose 的工具包
https://google.github.io/accompanist/
- Navigation Animation 可以设置动画的
Navigation
- Placeholder 加载时的各种占位符,例如骨架屏
- Swipe Refresh 下拉刷新
- Navigation Animation 可以设置动画的
-
异步加载图片
Coil https://coil-kt.github.io/coil/
-
视频SDK
腾讯视频SDK集成文档 https://cloud.tencent.com/document/product/881
-
网络请求框架
Retrofit https://square.github.io/retrofit/
-
JSON解析
GSON https://github.com/google/gson
Moshi https://github.com/square/moshi/
vigation-animation/) 可以设置动画的Navigation
- Placeholder 加载时的各种占位符,例如骨架屏
- Swipe Refresh 下拉刷新
-
异步加载图片
Coil https://coil-kt.github.io/coil/
-
视频SDK
腾讯视频SDK集成文档 https://cloud.tencent.com/document/product/881
-
网络请求框架
Retrofit https://square.github.io/retrofit/
-
JSON解析
GSON https://github.com/google/gson
Moshi https://github.com/square/moshi/