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.

Durumu koruma

Jetpack Compose'da kullanıcı arayüzü durumu genellikle remember ve rememberSaveable kullanılarak yönetilir. rememberSaveable yapılandırma değişiklikleri genelinde otomatik durum koruma özelliği sunsa da yerleşik özellikleri, temel veri türleri ve Parcelable ya da Serializable uygulayan nesnelerle sınırlıdır.

Brush gibi karmaşık özellikler içeren özel nesneler için özel bir durum kaydedici kullanarak açıkça tanımlanmış serileştirme ve seri durumdan çıkarma mekanizmaları tanımlamanız gerekir. Brush nesnesi için özel bir Saver tanımlayarak, yapılandırma değişiklikleri meydana geldiğinde fırçanın temel özelliklerini koruyabilirsiniz. Bu durum, aşağıdaki brushStateSaver örneğinde gösterilmiştir.

fun brushStateSaver(converters: Converters): Saver<MutableState<Brush>, SerializedBrush> = Saver(
    save = { converters.serializeBrush(it.value) },
    restore = { mutableStateOf(converters.deserializeBrush(it)) },
)

Ardından, seçilen fırça durumunu korumak için özel Saver simgesini kullanabilirsiniz:

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

Fırça darbesi sahnenizi statik bir resim dosyası olarak dışa aktarmanız gerekebilir. Bu, çizimi diğer uygulamalarla paylaşmak, küçük resimler oluşturmak veya içeriğin son, düzenlenemeyen bir sürümünü kaydetmek için kullanışlıdır.

Bir sahneyi dışa aktarmak için vuruşlarınızı doğrudan ekrana değil, ekran dışı bir bit eşleme olarak oluşturabilirsiniz. Görünür bir kullanıcı arayüzü bileşenine ihtiyaç duymadan tuval üzerinde çizimler kaydetmenize olanak tanıyan Android's Picture API öğesini kullanın.

Bu işlemde Picture örneği oluşturulur, beginRecording() çağrılarak Canvas alınır ve ardından mevcut CanvasStrokeRenderer kullanılarak her bir vuruş bu Canvas üzerine çizilir. Tüm çizim komutlarını kaydettikten sonra Picture simgesini kullanarak Bitmap oluşturabilir, ardından bunu sıkıştırıp dosyaya kaydedebilirsiniz.

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