状态保留和永久性存储是墨迹书写应用(尤其是 Compose 应用)的重要方面。核心数据对象(例如笔刷属性和构成笔画的点)非常复杂,不会自动持久保留。这需要一种周全的策略,以便在配置更改等场景中保存状态,并将用户的绘图永久保存到数据库中。
State preservation
In Jetpack Compose, UI state is typically managed using
remember
and
rememberSaveable.
While
rememberSaveable
offers automatic state preservation across configuration changes, its built-in
capabilities are limited to primitive data types and objects that implement
Parcelable or
Serializable.
For custom objects that contain complex properties, such as
Brush, you must define explicit
serialization and deserialization mechanisms using a custom state saver. By
defining a custom Saver
for the Brush object, you can preserve the brush's essential attributes when
configuration changes occur, as shown in the following brushStateSaver
example.
fun brushStateSaver(converters: Converters): Saver<MutableState<Brush>, SerializedBrush> = Saver(
save = { converters.serializeBrush(it.value) },
restore = { mutableStateOf(converters.deserializeBrush(it)) },
)
You can then use the custom Saver to
preserve the selected brush state:
val currentBrush = rememberSaveable(saver = brushStateSaver(Converters())) { mutableStateOf(defaultBrush) }
永久性存储
为了实现文档保存、加载和潜在的实时协作等功能,请以序列化格式存储笔画和关联数据。对于 Ink API,需要手动进行序列化和反序列化。
如需准确恢复笔画,请保存其 Brush 和 StrokeInputBatch。
Brush:包括数值字段(大小、epsilon)、颜色和BrushFamily。StrokeInputBatch:具有数值字段的输入点列表。
Storage 模块可简化最复杂部分(即 StrokeInputBatch)的紧凑序列化。
如需保存笔画,请执行以下操作:
- 使用存储模块的 encode 函数序列化
StrokeInputBatch。存储生成的二进制数据。 - 分别保存笔画的画笔的基本属性:
- 表示笔刷系列的枚举 &mdash 虽然可以序列化实例,但对于使用有限笔刷系列选择的应用来说,这并不高效
colorLongsizeepsilon
fun serializeStroke(stroke: Stroke): SerializedStroke {
val serializedBrush = serializeBrush(stroke.brush)
val encodedSerializedInputs = ByteArrayOutputStream().use
{
stroke.inputs.encode(it)
it.toByteArray()
}
return SerializedStroke(
inputs = encodedSerializedInputs,
brush = serializedBrush
)
}
如需加载笔画对象,请执行以下操作:
- 检索
StrokeInputBatch的已保存二进制数据,并使用存储模块的 decode() 函数对其进行反序列化。 - 检索已保存的
Brush属性并创建笔刷。 使用重新创建的笔刷和反序列化的
StrokeInputBatch创建最终笔画。fun deserializeStroke(serializedStroke: SerializedStroke): Stroke { val inputs = ByteArrayInputStream(serializedStroke.inputs).use { StrokeInputBatch.decode(it) } val brush = deserializeBrush(serializedStroke.brush) return Stroke(brush = brush, inputs = inputs) }
处理缩放、平移和旋转
如果您的应用支持缩放、平移或旋转,则必须向 InProgressStrokes 提供当前转换。这有助于使新绘制的笔画与现有笔画的位置和缩放比例保持一致。
为此,您需要向 pointerEventToWorldTransform 参数传递 Matrix。该矩阵应表示您应用于完成的笔画画布的转换的逆。
@Composable
fun ZoomableDrawingScreen(...) {
// 1. Manage your zoom/pan state (e.g., using detectTransformGestures).
var zoom by remember { mutableStateOf(1f) }
var pan by remember { mutableStateOf(Offset.Zero) }
// 2. Create the Matrix.
val pointerEventToWorldTransform = remember(zoom, pan) {
android.graphics.Matrix().apply {
// Apply the inverse of your rendering transforms
postTranslate(-pan.x, -pan.y)
postScale(1 / zoom, 1 / zoom)
}
}
Box(modifier = Modifier.fillMaxSize()) {
// ...Your finished strokes Canvas, with regular transform applied
// 3. Pass the matrix to InProgressStrokes.
InProgressStrokes(
modifier = Modifier.fillMaxSize(),
pointerEventToWorldTransform = pointerEventToWorldTransform,
defaultBrush = currentBrush,
nextBrush = onGetNextBrush,
onStrokesFinished = onStrokesFinished
)
}
}
导出笔画
您可能需要将笔画场景导出为静态图片文件。这对于与其他应用分享绘画、生成缩略图或保存最终的不可编辑版本的内容非常有用。
如需导出场景,您可以将笔画渲染到屏幕外位图,而不是直接渲染到屏幕。使用 Android's Picture API,这样您就可以在画布上录制绘画,而无需使用可见的界面组件。
该过程包括创建 Picture 实例、调用 beginRecording() 以获取 Canvas,然后使用现有的 CanvasStrokeRenderer 将每笔画绘制到该 Canvas 上。记录所有绘制命令后,您可以使用 Picture 创建 Bitmap,然后将其压缩并保存到文件中。
fun exportDocumentAsImage() {
val picture = Picture()
val canvas = picture.beginRecording(bitmapWidth, bitmapHeight)
// The following is similar logic that you'd use in your custom View.onDraw or Compose Canvas.
for (item in myDocument) {
when (item) {
is Stroke -> {
canvasStrokeRenderer.draw(canvas, stroke, worldToScreenTransform)
}
// Draw your other types of items to the canvas.
}
}
// Create a Bitmap from the Picture and write it to a file.
val bitmap = Bitmap.createBitmap(picture)
val outstream = FileOutputStream(filename)
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outstream)
}
数据对象和转换器辅助程序
定义一个与所需 Ink API 对象对应的序列化对象结构。
使用 Ink API 的存储模块对 StrokeInputBatch 进行编码和解码。
数据传输对象
@Parcelize
@Serializable
data class SerializedStroke(
val inputs: ByteArray,
val brush: SerializedBrush
) : Parcelable {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is SerializedStroke) return false
if (!inputs.contentEquals(other.inputs)) return false
if (brush != other.brush) return false
return true
}
override fun hashCode(): Int {
var result = inputs.contentHashCode()
result = 31 * result + brush.hashCode()
return result
}
}
@Parcelize
@Serializable
data class SerializedBrush(
val size: Float,
val color: Long,
val epsilon: Float,
val stockBrush: SerializedStockBrush,
val clientBrushFamilyId: String? = null
) : Parcelable
enum class SerializedStockBrush {
Marker,
PressurePen,
Highlighter,
DashedLine,
}
转化者数量
object Converters {
private val stockBrushToEnumValues = mapOf(
StockBrushes.marker() to SerializedStockBrush.Marker,
StockBrushes.pressurePen() to SerializedStockBrush.PressurePen,
StockBrushes.highlighter() to SerializedStockBrush.Highlighter,
StockBrushes.dashedLine() to SerializedStockBrush.DashedLine,
)
private val enumToStockBrush =
stockBrushToEnumValues.entries.associate { (key, value) -> value to key
}
private fun serializeBrush(brush: Brush): SerializedBrush {
return SerializedBrush(
size = brush.size,
color = brush.colorLong,
epsilon = brush.epsilon,
stockBrush = stockBrushToEnumValues[brush.family] ?: SerializedStockBrush.Marker,
)
}
fun serializeStroke(stroke: Stroke): SerializedStroke {
val serializedBrush = serializeBrush(stroke.brush)
val encodedSerializedInputs = ByteArrayOutputStream().use { outputStream ->
stroke.inputs.encode(outputStream)
outputStream.toByteArray()
}
return SerializedStroke(
inputs = encodedSerializedInputs,
brush = serializedBrush
)
}
private fun deserializeStroke(
serializedStroke: SerializedStroke,
): Stroke? {
val inputs = ByteArrayInputStream(serializedStroke.inputs).use { inputStream ->
StrokeInputBatch.decode(inputStream)
}
val brush = deserializeBrush(serializedStroke.brush, customBrushes)
return Stroke(brush = brush, inputs = inputs)
}
private fun deserializeBrush(
serializedBrush: SerializedBrush,
): Brush {
val stockBrushFamily = enumToStockBrush[serializedBrush.stockBrush]
val brushFamily = customBrush?.brushFamily ?: stockBrushFamily ?: StockBrushes.marker()
return Brush.createWithColorLong(
family = brushFamily,
colorLong = serializedBrush.color,
size = serializedBrush.size,
epsilon = serializedBrush.epsilon,
)
}
}