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

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

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

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

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

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

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

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 इंस्टेंस बनाना, Canvas पाने के लिए beginRecording() को कॉल करना, और फिर अपने मौजूदा 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,
    )
  }
}