Durum koruma ve kalıcı depolama

Durum koruma ve kalıcı depolama, özellikle Compose'da mürekkep uygulamalarının önemli yönleridir. Fırça özellikleri ve bir vuruşu oluşturan noktalar gibi temel veri nesneleri karmaşıktır ve otomatik olarak kalıcı hale gelmez. Bu, yapılandırma değişiklikleri gibi senaryolarda durumu kaydetmek ve kullanıcının çizimlerini kalıcı olarak bir veritabanına kaydetmek için kasıtlı bir strateji gerektirir.

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

Kalıcı Depolama

Belge kaydetme, yükleme ve olası gerçek zamanlı ortak çalışma gibi özellikleri etkinleştirmek için vuruşları ve ilişkili verileri seri hale getirilmiş bir biçimde saklarız. Ink API için manuel serileştirme ve seri durumdan çıkarma gereklidir.

Bir vuruşu doğru şekilde geri yüklemek için Brush ve StrokeInputBatch değerlerini kaydedin.

Depolama modülü, en karmaşık bölüm olan StrokeInputBatch'ı kompakt bir şekilde serileştirmeyi kolaylaştırır.

Bir vuruşu kaydetmek için:

  • Depolama modülünün kodlama işlevini kullanarak StrokeInputBatch öğesini serileştirin. Elde edilen ikili verileri saklayın.
  • Fırça darbesinin temel özelliklerini ayrı ayrı kaydedin:
    • Fırça ailesini temsil eden enum. Örnek seri hale getirilebilse de bu, sınırlı sayıda fırça ailesi kullanan uygulamalar için verimli değildir.
    • 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
  )
}

Kontur nesnesi yüklemek için:

  • StrokeInputBatch için kaydedilen ikili verileri alın ve depolama modülünün decode() işlevini kullanarak seri durumdan çıkarın.
  • Kaydedilen Brush özelliklerini alın ve fırçayı oluşturun.
  • Yeniden oluşturulan fırçayı ve seri durumdan çıkarılan StrokeInputBatch kullanarak son fırça darbesini oluşturun.

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

Yakınlaştırma, kaydırma ve döndürme işlemlerini yönetme

Uygulamanızda yakınlaştırma, kaydırma veya döndürme destekleniyorsa mevcut dönüşümü InProgressStrokes iletmeniz gerekir. Bu sayede yeni çizilen konturlar, mevcut konturlarınızın konumuna ve ölçeğine uygun hale gelir.

Bunu, Matrix parametresine pointerEventToWorldTransform ileterek yapabilirsiniz. Matris, bitmiş vuruşlar tuvalinize uyguladığınız dönüşümün tersini temsil etmelidir.

@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
        )
    }
}

Vuruşları dışa aktarma

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

Veri nesnesi ve dönüştürücü yardımcıları

Gerekli Ink API nesnelerini yansıtan bir serileştirme nesne yapısı tanımlayın.

StrokeInputBatch kodlamak ve kodunu çözmek için Ink API'nin depolama modülünü kullanın.

Veri aktarım nesneleri
@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,
}
Dönüşüm sağlayan kullanıcı sayısı
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,
    )
  }
}