前言
公司的项目使用了MVI架构,V层使用compose进行UI绘制,本着探索与学习,闲暇时间写了个记事本的app,现需要一个写字板功能,遂记录,本文不涉及compose 重组机制介绍,和绘制原理,只记录功能。
功能拆分
-
写字板
根据手势检测移动路径,使用Canvas组件绘制路径 -
保存Compose中的UI成图片
暂时没有找到Compose 控件直接转换成的方法,转换思路
1、使用Modifier.onGloballyPositioned()监听组件在全局的位置
2、使用Bitmap Canvas PixelCopy 等类,将指定区域的View 保存成Bitmap,如果是传统的View 可以通过drawToBitmap() 扩展函数操作
3、使用contentResolver插入到相册
手势检测
Compose 中的Modifier提供pointerInput扩展方法用来监测手势输入
由于pointerInput传入的方法是挂起函数 并是PointerInputScope的扩展函数,所以pointerInput处理手势时,并不会阻塞当前线程
在PointerInputScope.awaitEachGesture()方法中 ,我们可以通过传入一个函数实时等待手指输入,获取当前位置和手指事件
绘制Path
和传统xml布局相似,可以通过Canvas 绘制一些点、线、路径等
差别是,XML是Canvas类,而Compose提供了一个方法(声明式UI特性)
Canvas(modifier = Modifier.fillMaxSize()) {
val path = Path()
path.moveTo(0f, 0f)
path.lineTo(size.width / 2f, size.height / 2f)
path.lineTo(size.width, 0f)
path.close()
drawPath(path, Color.Magenta, style = Stroke(width = 10f))
}
Compose组件转换成bitmap
DisposableEffect(Unit) {
screenshotState.callback = {
runCatching {
composeView?.let {
if (it.width == 0f || it.height == 0f){
return@let
}
view.screenshot(it){
screenshotState.imageState.value = it
if (it is ImageResult.Success){
println("screenshot imageState Success")
screenshotState.bitmapState.value = it.data
}
}
}
}.onFailure {
screenshotState.imageState.value = ImageResult.Error(Exception(it.message))
}
}
onDispose {
println("screenshot onDispose ")
val bmp = screenshotState.bitmapState.value
bmp?.apply {
if (!isRecycled) {
recycle()
}
}
screenshotState.bitmapState.value = null
screenshotState.callback = null
}
}
//根据区域保存bitmap
fun View.screenshot(
bounds: Rect,
bitmapCallback: (ImageResult) -> Unit
) {
try {
val bitmap = Bitmap.createBitmap(
bounds.width.toInt(),
bounds.height.toInt(),
Bitmap.Config.ARGB_8888,
)
PixelCopy.request(
(this.context as Activity).window,
bounds.toAndroidRect(),
bitmap,
{
when (it) {
PixelCopy.SUCCESS -> {
bitmapCallback.invoke(ImageResult.Success(bitmap))
}
PixelCopy.ERROR_DESTINATION_INVALID -> {
}
PixelCopy.ERROR_SOURCE_INVALID -> {
bitmapCallback.invoke(
)
}
PixelCopy.ERROR_TIMEOUT -> {
bitmapCallback.invoke(
ImageResult.Error(
Exception(
"A timeout occurred while trying to acquire a buffer " +
"from the source to copy from."
)
)
)
}
PixelCopy.ERROR_SOURCE_NO_DATA -> {
}
else -> {
bitmapCallback.invoke(
)
}
}
},
Handler(Looper.getMainLooper())
)
} catch (e: Exception) {
bitmapCallback.invoke(ImageResult.Error(e))
}
}
具体实现
@Composable
fun DrawTablet() {
// 记录每一次move的路线
var linepath by remember { mutableStateOf(Offset.Zero) }
// 记录path对象
val path by remember { mutableStateOf(Path()) }
val screenshotState = rememberScreenshotState()
val context = LocalContext.current
val scope = rememberCoroutineScope()
ScreenshotBox(screenshotState = screenshotState,
modifier = Modifier
.statusBarsPadding().
navigationBarsPadding()
.fillMaxSize()
.pointerInput(Unit) {
awaitEachGesture{
while (true) {
val event = awaitPointerEvent()
when (event.type) {
//按住时,更新起始点
Press -> {
path.moveTo(event.changes.first().position.x, event.changes.first().position.y)
}
//移动时,更新起始点 移动时,记录路径path
Move->{
linepath = event.changes.first().position
}
}
}
}
}){
Canvas(modifier = Modifier.fillMaxSize()) {
//重组新路线
path.lineTo(linepath.x, linepath.y)
drawPath( color = Color.Red,
path = path,
style = Stroke(width = 10F))
}
Row(modifier = Modifier.align(Alignment.BottomCenter)) {
Button(modifier = Modifier.weight(1f).padding(8.dp), onClick = {
linepath = Offset.Zero
path.reset()
}){
Text(text = "Clear")
}
Button(modifier = Modifier.weight(1f).padding(8.dp), onClick = {
scope.launch {
screenshotState.capture()
screenshotState.bitmap?.insertImageToImage(context)
}
}){
Text(text = "Screenshot")
}
}
}
}