Zachowywanie stanu i trwałe przechowywanie to niebanalne aspekty aplikacji do pisania odręcznego, zwłaszcza w Compose. Podstawowe obiekty danych, takie jak właściwości pędzla i punkty tworzące pociągnięcie, są złożone i nie są automatycznie zapisywane. Wymaga to przemyślanej strategii zapisywania stanu w sytuacjach takich jak zmiany konfiguracji i trwałe zapisywanie rysunków użytkownika w bazie danych.
Zachowywanie stanu
W Jetpack Compose stan interfejsu jest zwykle zarządzany za pomocą funkcji remember i rememberSaveable.
Chociaż rememberSaveable
oferuje automatyczne zachowywanie stanu podczas zmian konfiguracji, jego wbudowane możliwości są ograniczone do prostych typów danych i obiektów, które implementują Parcelable lub Serializable.
W przypadku obiektów niestandardowych zawierających złożone właściwości, takie jak Brush, musisz zdefiniować mechanizmy serializacji i deserializacji za pomocą niestandardowego narzędzia do zapisywania stanu. Definiując niestandardowy Saver
dla obiektu Brush, możesz zachować podstawowe atrybuty pędzla, gdy nastąpią zmiany konfiguracji, jak pokazano w tym brushStateSaver
przykładzie.
fun brushStateSaver(converters: Converters): Saver<MutableState<Brush>, SerializedBrush> = Saver(
save = { converters.serializeBrush(it.value) },
restore = { mutableStateOf(converters.deserializeBrush(it)) },
)
Następnie możesz użyć niestandardowego Saver, aby zachować wybrany stan pędzla:
val currentBrush = rememberSaveable(saver = brushStateSaver(Converters())) { mutableStateOf(defaultBrush) }
Pamięć trwała
为了实现文档保存、加载和潜在的实时协作等功能,请以序列化格式存储笔画和关联数据。对于 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
)
}
}
导出笔画
Może być konieczne wyeksportowanie sceny z pociągnięciami pędzla jako statycznego pliku obrazu. Jest to przydatne do udostępniania rysunku innym aplikacjom, generowania miniatur lub zapisywania ostatecznej, nieedytowalnej wersji treści.
Aby wyeksportować scenę, możesz wyrenderować pociągnięcia pędzla do bitmapy poza ekranem zamiast bezpośrednio na ekranie. Użyj funkcji Android's Picture API, która umożliwia nagrywanie rysunków na obszarze roboczym bez konieczności korzystania z widocznego komponentu interfejsu.
Proces ten obejmuje utworzenie instancji Picture, wywołanie beginRecording() w celu uzyskania Canvas, a następnie użycie istniejącego CanvasStrokeRenderer do narysowania każdego pociągnięcia na tym Canvas. Po zarejestrowaniu wszystkich poleceń rysowania możesz użyć ikony Picture, aby utworzyć Bitmap, który możesz następnie skompresować i zapisać w pliku.
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,
)
}
}