La conservazione dello stato e l'archiviazione permanente sono aspetti non banali delle app di inchiostro digitale, soprattutto in Compose. Gli oggetti dati principali, come le proprietà del pennello e i punti che formano un tratto, sono complessi e non vengono salvati automaticamente. Ciò richiede una strategia deliberata per il salvataggio dello stato durante scenari come modifiche alla configurazione e salvataggio permanente dei disegni di un utente in un database.
Conservazione dello stato
In Jetpack Compose, lo stato dell'UI viene in genere gestito utilizzando
remember
e
rememberSaveable.
Sebbene
rememberSaveable
offra la conservazione automatica dello stato in caso di modifiche alla configurazione, le sue funzionalità integrate
sono limitate a tipi di dati primitivi e oggetti che implementano
Parcelable o
Serializable.
Per gli oggetti personalizzati che contengono proprietà complesse, ad esempio
Brush, devi definire meccanismi espliciti
di serializzazione e deserializzazione utilizzando un salvataggio dello stato personalizzato. Definendo un Saver personalizzato per l'oggetto Brush, puoi conservare gli attributi essenziali del pennello quando si verificano modifiche alla configurazione, come mostrato nell'esempio brushStateSaver seguente.
fun brushStateSaver(converters: Converters): Saver<MutableState<Brush>, SerializedBrush> = Saver(
save = { converters.serializeBrush(it.value) },
restore = { mutableStateOf(converters.deserializeBrush(it)) },
)
Puoi quindi utilizzare il Saver personalizzato per
mantenere lo stato del pennello selezionato:
val currentBrush = rememberSaveable(saver = brushStateSaver(Converters())) { mutableStateOf(defaultBrush) }
Archiviazione permanente
为了实现文档保存、加载和潜在的实时协作等功能,请以序列化格式存储笔画和关联数据。对于 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
)
}
}
导出笔画
Potresti dover esportare la scena del tratto come file immagine statica. Questa funzionalità è utile per condividere il disegno con altre applicazioni, generare miniature o salvare una versione finale e non modificabile dei contenuti.
Per esportare una scena, puoi eseguire il rendering dei tratti in una bitmap off-screen anziché
direttamente sullo schermo. Utilizza
Android's Picture API, che ti consente di registrare disegni su una tela senza
bisogno di un componente UI visibile.
La procedura prevede la creazione di un'istanza Picture, la chiamata di beginRecording()
per ottenere un Canvas e l'utilizzo del CanvasStrokeRenderer esistente per disegnare
ogni tratto su quel Canvas. Dopo aver registrato tutti i comandi di disegno, puoi utilizzare Picture per creare un Bitmap, che puoi comprimere e salvare in un file.
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,
)
}
}