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.
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) }
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,
)
}
}