Mempertahankan status dan penyimpanan persisten adalah aspek yang tidak sepele dalam aplikasi tinta, terutama di Compose. Objek data inti, seperti properti kuas dan titik yang membentuk goresan, bersifat kompleks dan tidak otomatis dipertahankan. Hal ini memerlukan strategi yang disengaja untuk menyimpan status selama skenario seperti perubahan konfigurasi dan menyimpan gambar pengguna secara permanen ke database.
Mempertahankan status
Di Jetpack Compose, status UI biasanya dikelola menggunakan
remember
dan
rememberSaveable.
Meskipun
rememberSaveable
menawarkan pelestarian status otomatis di seluruh perubahan konfigurasi, kemampuan bawaannya
terbatas pada jenis data primitif dan objek yang menerapkan
Parcelable atau
Serializable.
Untuk objek kustom yang berisi properti kompleks, seperti
Brush, Anda harus menentukan mekanisme
serialisasi dan deserialisasi eksplisit menggunakan penyimpan status kustom. Dengan
menentukan Saver kustom
untuk objek Brush, Anda dapat mempertahankan atribut penting kuas saat
perubahan konfigurasi terjadi, seperti yang ditunjukkan dalam contoh brushStateSaver
berikut.
fun brushStateSaver(converters: Converters): Saver<MutableState<Brush>, SerializedBrush> = Saver(
save = { converters.serializeBrush(it.value) },
restore = { mutableStateOf(converters.deserializeBrush(it)) },
)
Kemudian, Anda dapat menggunakan Saver kustom untuk
mempertahankan status kuas yang dipilih:
val currentBrush = rememberSaveable(saver = brushStateSaver(Converters())) { mutableStateOf(defaultBrush) }
Penyimpanan Persisten
Untuk mengaktifkan fitur seperti penyimpanan, pemuatan, dan potensi kolaborasi real-time dokumen, simpan goresan dan data terkait dalam format berserial. Untuk Ink API, serialisasi dan deserialisasi manual diperlukan.
Untuk memulihkan goresan secara akurat, simpan Brush dan StrokeInputBatch-nya.
Brush: Mencakup kolom numerik (ukuran, epsilon), warna, danBrushFamily.StrokeInputBatch: Daftar titik input dengan kolom numerik.
Modul Storage menyederhanakan serialisasi bagian yang paling kompleks secara ringkas: StrokeInputBatch.
Untuk menyimpan goresan:
- Serialkan
StrokeInputBatchmenggunakan fungsi encode modul penyimpanan. Simpan data biner yang dihasilkan. - Simpan properti penting Brush goresan secara terpisah:
- Enum yang merepresentasikan kelompok kuas — Meskipun instance dapat diserialisasi, hal ini tidak efisien untuk aplikasi yang menggunakan pilihan kelompok kuas yang terbatas
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
)
}
Untuk memuat objek stroke:
- Ambil data biner tersimpan untuk
StrokeInputBatchdan deserialisasikan menggunakan fungsi decode() modul penyimpanan. - Ambil properti
Brushyang disimpan dan buat kuas. Buat goresan akhir menggunakan kuas yang dibuat ulang dan
StrokeInputBatchyang dideserialisasi.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) }
Menangani zoom, geser, dan rotasi
Jika aplikasi Anda mendukung zoom, menggeser, atau rotasi, Anda harus memberikan transformasi saat ini ke InProgressStrokes. Hal ini membantu goresan yang baru digambar cocok dengan posisi dan skala goresan yang sudah ada.
Anda melakukannya dengan meneruskan Matrix ke parameter pointerEventToWorldTransform. Matriks harus merepresentasikan kebalikan dari transformasi yang Anda
terapkan ke kanvas goresan akhir.
@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
)
}
}
Mengekspor goresan
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)
}
Objek data dan helper konverter
Tentukan struktur objek serialisasi yang mencerminkan objek Ink API yang diperlukan.
Gunakan modul penyimpanan Ink API untuk mengenkode dan mendekode StrokeInputBatch.
Objek transfer data
@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,
}
Converter
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,
)
}
}