Penyimpanan persisten dan pelestarian status

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.

Modul Storage menyederhanakan serialisasi bagian yang paling kompleks secara ringkas: StrokeInputBatch.

Untuk menyimpan goresan:

  • Serialkan StrokeInputBatch menggunakan fungsi encode modul penyimpanan. Simpan data biner yang dihasilkan.
  • Simpan properti penting Brush goresan secara terpisah:
    • Enum yang merepresentasikan kelompok kuas &mdash; Meskipun instance dapat diserialisasi, hal ini tidak efisien untuk aplikasi yang menggunakan pilihan kelompok kuas yang terbatas
    • colorLong
    • size
    • epsilon
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 StrokeInputBatch dan deserialisasikan menggunakan fungsi decode() modul penyimpanan.
  • Ambil properti Brush yang disimpan dan buat kuas.
  • Buat goresan akhir menggunakan kuas yang dibuat ulang dan StrokeInputBatch yang 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,
    )
  }
}