राज्य संरक्षण और स्थायी मेमोरी

स्टेट प्रिज़र्वेशन और परसिस्टेंट स्टोरेज, इंक वाले ऐप्लिकेशन के लिए ज़रूरी पहलू हैं. खास तौर पर, Compose में. ब्रश प्रॉपर्टी और स्ट्रोक बनाने वाले पॉइंट जैसे मुख्य डेटा ऑब्जेक्ट जटिल होते हैं और अपने-आप सेव नहीं होते. इसके लिए, कॉन्फ़िगरेशन में बदलाव और उपयोगकर्ता की ड्रॉइंग को डेटाबेस में हमेशा के लिए सेव करने जैसे मामलों में, स्थिति को सेव करने के लिए एक रणनीति की ज़रूरत होती है.

स्टेट को बनाए रखना

Jetpack Compose में, यूज़र इंटरफ़ेस (यूआई) की स्थिति को आम तौर पर remember और rememberSaveable का इस्तेमाल करके मैनेज किया जाता है. rememberSaveable कॉन्फ़िगरेशन में बदलाव होने पर, स्थिति को अपने-आप सेव करता है. हालांकि, इसमें पहले से मौजूद सुविधाएं सिर्फ़ प्रिमिटिव डेटा टाइप और Parcelable या Serializable को लागू करने वाले ऑब्जेक्ट के लिए उपलब्ध हैं.

Brush जैसी मुश्किल प्रॉपर्टी वाले कस्टम ऑब्जेक्ट के लिए, आपको साफ़ तौर पर सीरियलाइज़ेशन और डीसीरियलाइज़ेशन के तरीके तय करने होंगे. इसके लिए, कस्टम स्टेट सेवर का इस्तेमाल किया जा सकता है. Brush ऑब्जेक्ट के लिए कस्टम Saver तय करके, कॉन्फ़िगरेशन में बदलाव होने पर भी इसके ज़रूरी एट्रिब्यूट को सुरक्षित रखा जा सकता है. जैसा कि यहां दिए गए brushStateSaver उदाहरण में दिखाया गया है.

fun brushStateSaver(converters: Converters): Saver<MutableState<Brush>, String> = Saver(
    save = { state ->
        converters.brushToString(state.value)
    },
    restore = { jsonString ->
        val brush = converters.stringToBrush(jsonString)
        mutableStateOf(brush)
    }
)

इसके बाद, चुने गए ब्रश की स्थिति को बनाए रखने के लिए, कस्टम Saver का इस्तेमाल किया जा सकता है:

val converters = Converters()
val currentBrush = rememberSaveable(saver = brushStateSaver(converters)) { mutableStateOf(defaultBrush) }

परसिस्टेंट स्टोरेज

दस्तावेज़ सेव करने, लोड करने, और रीयल-टाइम में साथ मिलकर काम करने जैसी सुविधाएं चालू करने के लिए, स्ट्रोक और उनसे जुड़े डेटा को क्रम से व्यवस्थित किए गए फ़ॉर्मैट में सेव करें. Ink API के लिए, मैन्युअल तरीके से क्रमबद्ध और क्रम से हटाना ज़रूरी है.

स्ट्रोक को सटीक तरीके से वापस लाने के लिए, उसके Brush और StrokeInputBatch को सेव करें.

  • Brush: इसमें संख्या वाले फ़ील्ड (साइज़, ऐप्सीलोन), रंग, और BrushFamily शामिल हैं.
  • StrokeInputBatch: यह अंकों वाले फ़ील्ड के साथ इनपुट पॉइंट की सूची होती है.

स्टोरेज मॉड्यूल, सबसे मुश्किल हिस्से को आसानी से क्रम में लगाता है: StrokeInputBatch.

स्ट्रोक सेव करने के लिए:

  • स्टोरेज मॉड्यूल के एन्कोड फ़ंक्शन का इस्तेमाल करके, StrokeInputBatch को क्रम से लगाएं. इससे मिलने वाले बाइनरी डेटा को सेव करता है.
  • स्ट्रोक के ब्रश की ज़रूरी प्रॉपर्टी को अलग से सेव करें:
    • यह enum, ब्रश फ़ैमिली को दिखाता है &mdash हालांकि, इंस्टेंस को क्रम से लगाया जा सकता है, लेकिन यह उन ऐप्लिकेशन के लिए कारगर नहीं है जो ब्रश फ़ैमिली के सीमित विकल्पों का इस्तेमाल करते हैं
    • 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
  )
}

स्ट्रोक ऑब्जेक्ट लोड करने के लिए:

  • StrokeInputBatch के लिए सेव किया गया बाइनरी डेटा वापस पाएं और स्टोरेज मॉड्यूल के decode() फ़ंक्शन का इस्तेमाल करके, उसे डीसीरियलाइज़ करें.
  • सेव की गई Brush प्रॉपर्टी वापस पाएं और ब्रश बनाएं.
  • फिर से बनाए गए ब्रश और डिसिरियलाइज़ किए गए StrokeInputBatch का इस्तेमाल करके, फ़ाइनल स्ट्रोक बनाएं.

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

ज़ूम, पैन, और रोटेशन को मैनेज करना

अगर आपका ऐप्लिकेशन ज़ूम करने, पैन करने या घुमाने की सुविधा देता है, तो आपको InProgressStrokes को मौजूदा ट्रांसफ़ॉर्मेशन की जानकारी देनी होगी. इससे नए स्ट्रोक, मौजूदा स्ट्रोक की पोज़िशन और स्केल से मैच होते हैं.

इसके लिए, pointerEventToWorldTransform पैरामीटर को Matrix पास करें. मैट्रिक्स, आपके फ़िनिश किए गए स्ट्रोक कैनवस पर लागू किए गए ट्रांसफ़ॉर्मेशन का उलटा होना चाहिए.

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

स्ट्रोक एक्सपोर्ट करना

您可能需要将笔画场景导出为静态图片文件。这对于与其他应用分享绘画、生成缩略图或保存最终的不可编辑版本的内容非常有用。

如需导出场景,您可以将笔画渲染到屏幕外位图,而不是直接渲染到屏幕。使用 Android's Picture API,这样您就可以在画布上录制绘画,而无需使用可见的界面组件。

该过程包括创建 Picture 实例、调用 beginRecording() 以获取 Canvas,然后使用现有的 CanvasStrokeRenderer 将每笔画绘制到该 Canvas 上。记录所有绘制命令后,您可以使用 Picture 创建 Bitmap,然后将其压缩并保存到文件中。

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

डेटा ऑब्जेक्ट और कन्वर्टर हेल्पर

Ink API के ज़रूरी ऑब्जेक्ट के हिसाब से, एक सीरियलाइज़ेशन ऑब्जेक्ट स्ट्रक्चर तय करें.

StrokeInputBatch को कोड में बदलने और समझने के लिए, Ink API के स्टोरेज मॉड्यूल का इस्तेमाल करें.

डेटा ट्रांसफ़र ऑब्जेक्ट
@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,
}
ग्राहक में बदलने वाले लोगों की संख्या
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,
    )
  }
}