使用 Jetpack Compose 让 SurfaceView 可任意滚动

什么是 Jetpack Compose?

Jetpack Compose 是用于构建原生 Android 界面的新工具包。
它可简化并加快 Android 上的界面开发,使用更少的代码、强大的工具和直观的 Kotlin API,快速让应用生动而精彩。

SurfaceView 使用之前滑动方法的缺陷

‘解决 SurfaceView 的滑动问题(1)上下滑动’ 一文中我们讲过如何解决 SurfaceView 的上下滑动
但还是有很多问题,例如重复绘制内容,计算滑动等等是很耗时、耗性能的,如此便有了本文

SurfaceView 滑动问题的解决

请确保有 Jetpack Compose & Kotlin 协程 & ViewModel ~ LiveData 的依赖
1.继承 SurfaceView ,名称改为 CoroutineSurfaceInternal 并改为以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//内部原生实现
class CoroutineSurfaceInternal @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : SurfaceView(context, attrs) {

//上一次的 Job 对象,这是为了和 Lifecycle 里面的一个概念一样,就是如果旧数据要刷新时出现新数据,那么老数据就不会刷新
//反而是新数据
private var lastJob: Job? = null

//Canvas 背景颜色
private var background = 0

init {
//设置始终位于顶端
setZOrderOnTop(true)
setZOrderMediaOverlay(true)
//设置默认画布背景为透明
holder.setFormat(PixelFormat.TRANSPARENT)
}

//设置画布背景颜色
fun setSurfaceBackground(background: Int) {
this.background = background
}

//和 draw 差不多,但这是 SurfaceView , 我们开线程才有意义
@Suppress("ControlFlowWithEmptyBody")
fun onSurfaceDraw(drawScope: Canvas.(CoroutineScope) -> Unit) {
//如果上一次的 job 不为空,则取消它
lastJob?.cancel()
//赋值为新的 Task
lastJob = CoroutineScope(Dispatchers.IO).launch {
//如果 Canvas 没有准备好则一直堵塞协程
while (!holder.surface.isValid) {
//如果该协程死了则直接返回
if (!isActive) {
return@launch
}
}
//注意线程安全,给 holder 加个锁,因为如果同时有多个协程执行到这实际上就得访问 holder.lockCanvas
synchronized(holder) {
val canvas = holder.lockCanvas()
//清空画布
canvas.drawColor(background)
//调用绘制
drawScope.invoke(canvas, this)
//解锁并提交内容
holder.unlockCanvasAndPost(canvas)
}
}
}

}

CoroutineSurfaceInternalSurfaceView 使用协程的内部原生类 ,后面会在 Compose 中进行包装
接下来用 Jetpack ComposeCoroutineSurfaceInternal 进行包装 ,创建 CoroutineSurface.kt 并改为以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* SurfaceView 的 Jetpack Compose 封装
* 添加了刷新函数,以后不用写 while(true) 绘制了!
* */
@Suppress("UNUSED_EXPRESSION")
@Composable
fun CoroutineSurface(modifier: Modifier = Modifier, drawScope: Canvas.(CoroutineScope) -> Unit) {
//获取对应的 ViewModel
val viewModel: CoroutineSurfaceViewModel = viewModel()
//小语法糖,挺甜的
val observableObject by viewModel.observableObject.observeAsState(0)
//AndroidView,Jetpack Compose 里面对原生 View 的 嫁接
AndroidView(factory = {
//这里创建原生 View 实例
CoroutineSurfaceInternal(it)
}, modifier = modifier) {
//这里是 update ,里面就是当 Component 重组时 执行的
//这一句必须有,否则不能触发重组
Log.e("StateChanged", observableObject.toString())
//设置画布背景颜色
it.setSurfaceBackground(if (isDarkTheme.value) Color.BLACK else Color.WHITE)
//开始画画
it.onSurfaceDraw(drawScope)
}
}

//主动让 SurfaceView 刷新
@Suppress("unused")
fun ComponentActivity.invalidateCoroutineSurface() {
//获取对应的 ViewModel
val viewModel = ViewModelProvider(this).get(CoroutineSurfaceViewModel::class.java)
//只有当 value 与原值不一样的时候才能让 Component 重组
viewModel.onObservableObjectChanged()
}

最后我们只需要指定让 Column 可滑动即可

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
让 Column 可滑动
*/
Column(
Modifier
.verticalScroll(rememberScrollState())
.horizontalScroll(rememberScrollState())
) {
//设置 CoroutineSurface 的大小,这是必须的
CoroutineSurface(modifier = Modifier.size(2000.dp)) {
//在这里进行你的绘制逻辑,this 就是 Canvas 实例
}
}

最后在你自己的 Jetpack Compose Activity 中使用这个 Column
需要刷新界面调用 invalidateCoroutineSurface 主动刷新即可