Mempertahankan status dan penyimpanan persisten adalah aspek penting dari aplikasi tinta digital. 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 aplikasi berbasis tampilan, status UI dikelola menggunakan kombinasi berikut:
- Objek
ViewModel - Status instance tersimpan menggunakan:
- Aktivitas
onSaveInstanceState() - SavedStateHandle ViewModel
- Penyimpanan lokal untuk mempertahankan status UI selama transisi aplikasi dan aktivitas
- Aktivitas
Lihat Menyimpan status UI.
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,
)
}
}