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.
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) }
Pamięć trwała
Aby włączyć funkcje takie jak zapisywanie i wczytywanie dokumentów oraz potencjalną współpracę w czasie rzeczywistym, przechowuj pociągnięcia i powiązane z nimi dane w formacie serializowanym. W przypadku interfejsu Ink API konieczne jest ręczne serializowanie i deserializowanie.
Aby dokładnie przywrócić pociągnięcie, zapisz jego Brush i StrokeInputBatch.
Brush: zawiera pola numeryczne (rozmiar, epsilon), kolor iBrushFamily.StrokeInputBatch: lista punktów wejściowych z polami liczbowymi.
Moduł Storage upraszcza kompaktową serializację najbardziej złożonej części: StrokeInputBatch.
Aby zapisać pociągnięcie:
- Zserializuj
StrokeInputBatchza pomocą funkcji kodowania modułu pamięci. Przechowuj wynikowe dane binarne. - Zapisz osobno najważniejsze właściwości pędzla:
- Wyliczenie reprezentujące rodzinę pędzli – chociaż instancję można serializować, nie jest to wydajne w przypadku aplikacji, które używają ograniczonego wyboru rodzin pędzli.
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
)
}
Aby wczytać obiekt pociągnięcia:
- Pobierz zapisane dane binarne dla
StrokeInputBatchi zdeserializuj je za pomocą funkcji decode() modułu pamięci. - Pobierz zapisane właściwości
Brushi utwórz pędzel. Utwórz ostatnie pociągnięcie za pomocą odtworzonego pędzla i zdeserializowanego obiektu
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) }
Obsługa powiększania, przesuwania i obracania
Jeśli Twoja aplikacja obsługuje powiększanie, przesuwanie lub obracanie, musisz podać bieżącą transformację do InProgressStrokes. Dzięki temu nowo narysowane pociągnięcia będą pasować do pozycji i skali istniejących pociągnięć.
W tym celu przekaż wartość Matrix do parametru pointerEventToWorldTransform. Macierz powinna reprezentować odwrotność przekształcenia, które stosujesz do gotowego obszaru roboczego z pociągnięciami.
@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
)
}
}
Eksportowanie pociągnięć
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)
}
Obiekty danych i pomocnicze funkcje konwertera
Określ strukturę obiektu serializacji, która odzwierciedla potrzebne obiekty interfejsu Ink API.
Użyj modułu pamięci interfejsu Ink API, aby kodować i dekodować StrokeInputBatch.
Obiekty przenoszenia danych
@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,
}
Użytkownicy dokonujący konwersji
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,
)
}
}