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
Anda mungkin perlu mengekspor adegan goresan sebagai file gambar statis. Hal ini berguna untuk membagikan gambar dengan aplikasi lain, membuat thumbnail, atau menyimpan versi akhir konten yang tidak dapat diedit.
Untuk mengekspor adegan, Anda dapat merender goresan ke bitmap di luar layar, bukan
langsung ke layar. Gunakan
Android's Picture API, yang memungkinkan Anda merekam gambar di kanvas tanpa
memerlukan komponen UI yang terlihat.
Proses ini melibatkan pembuatan instance Picture, pemanggilan beginRecording()
untuk mendapatkan Canvas, lalu penggunaan CanvasStrokeRenderer yang ada untuk menggambar
setiap goresan ke Canvas tersebut. Setelah merekam semua perintah menggambar, Anda
dapat menggunakan Picture untuk membuat Bitmap,
yang kemudian dapat Anda kompresi dan simpan ke 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,
)
}
}