Сохранение состояния и постоянное хранение данных — нетривиальные аспекты приложений для рисования от руки, особенно в Compose. Основные объекты данных, такие как свойства кисти и точки, формирующие обводку, сложны и не сохраняются автоматически. Это требует продуманной стратегии сохранения состояния в таких сценариях, как изменение конфигурации и постоянное сохранение рисунков пользователя в базе данных.
сохранение государства
В Jetpack Compose состояние пользовательского интерфейса обычно управляется с помощью remember и rememberSaveable . Хотя rememberSaveable обеспечивает автоматическое сохранение состояния при изменении конфигурации, его встроенные возможности ограничены примитивными типами данных и объектами, реализующими интерфейсы Parcelable или Serializable .
Для пользовательских объектов, содержащих сложные свойства, таких как Brush , необходимо определить явные механизмы сериализации и десериализации с помощью пользовательского механизма сохранения состояния. Определив пользовательский механизм Saver для объекта Brush , вы можете сохранить основные атрибуты кисти при изменении конфигурации, как показано в следующем примере brushStateSaver .
fun brushStateSaver(converters: Converters): Saver<MutableState<Brush>, SerializedBrush> = Saver(
save = { converters.serializeBrush(it.value) },
restore = { mutableStateOf(converters.deserializeBrush(it)) },
)
Затем вы можете использовать настраиваемую Saver , чтобы сохранить выбранное состояние кисти:
val currentBrush = rememberSaveable(saver = brushStateSaver(Converters())) { mutableStateOf(defaultBrush) }
Постоянное хранение
Для обеспечения таких функций, как сохранение и загрузка документов, а также возможность совместной работы в режиме реального времени, необходимо хранить штрихи и связанные с ними данные в сериализованном формате. Для API Ink требуется ручная сериализация и десериализация.
Для точного восстановления штриха сохраните его Brush и StrokeInputBatch .
-
Brush: включает числовые поля (размер, эпсилон), цвет иBrushFamily. -
StrokeInputBatch: Список входных точек с числовыми полями.
Модуль Storage упрощает компактную сериализацию наиболее сложной части: StrokeInputBatch .
Чтобы предотвратить инсульт:
- Сериализуйте объект
StrokeInputBatchиспользуя функцию кодирования модуля хранения. Сохраните полученные двоичные данные. - Сохраните отдельно основные свойства кисти для мазка:
- Перечисление, представляющее семейство кистей — Хотя экземпляр можно сериализовать, это неэффективно для приложений, использующих ограниченный набор семейств кистей.
-
colorLong -
size -
epsilon
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 . Это поможет новым штрихам соответствовать положению и масштабу уже существующих.
Для этого передайте Matrix в параметр pointerEventToWorldTransform . Матрица должна представлять собой обратное преобразование, которое вы применяете к готовому холсту с контурами.
@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
)
}
}
Экспорт ударов
You might need to export your stroke scene as a static image file. This is useful for sharing the drawing with other applications, generating thumbnails, or saving a final, uneditable version of the content.
To export a scene, you can render your strokes to an offscreen bitmap instead of
directly to the screen. Use
Android's Picture API, which lets you record drawings on a canvas without
needing a visible UI component.
The process involves creating a Picture instance, calling beginRecording()
to get a Canvas, and then using your existing CanvasStrokeRenderer to draw
each stroke onto that Canvas. After you record all the drawing commands, you
can use the Picture to create a Bitmap,
which you can then compress and save to a 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,
)
}
}