রাষ্ট্রীয় সংরক্ষণ এবং ক্রমাগত স্টোরেজ

ইঙ্কিং অ্যাপের ক্ষেত্রে, বিশেষ করে কম্পোজে, স্টেট প্রিজারভেশন এবং স্থায়ী স্টোরেজ হল তুচ্ছ বিষয় নয়। মূল ডেটা অবজেক্ট, যেমন ব্রাশ প্রোপার্টি এবং স্ট্রোক তৈরি করে এমন পয়েন্টগুলি জটিল এবং স্বয়ংক্রিয়ভাবে টিকে থাকে না। এর জন্য কনফিগারেশন পরিবর্তনের মতো পরিস্থিতিতে স্টেট সংরক্ষণ এবং ব্যবহারকারীর অঙ্কন স্থায়ীভাবে ডাটাবেসে সংরক্ষণ করার জন্য একটি সুচিন্তিত কৌশল প্রয়োজন।

রাষ্ট্রীয় সংরক্ষণ

জেটপ্যাক কম্পোজে, UI স্টেট সাধারণত 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) }

স্থায়ী সঞ্চয়স্থান

ডকুমেন্ট সংরক্ষণ, লোডিং এবং সম্ভাব্য রিয়েল-টাইম সহযোগিতার মতো বৈশিষ্ট্যগুলি সক্ষম করতে, স্ট্রোক এবং সংশ্লিষ্ট ডেটা একটি সিরিয়ালাইজড ফর্ম্যাটে সংরক্ষণ করুন। ইঙ্ক এপিআই-এর জন্য, ম্যানুয়াল সিরিয়ালাইজেশন এবং ডিসিরিয়ালাইজেশন প্রয়োজন।

স্ট্রোক সঠিকভাবে পুনরুদ্ধার করতে, এর Brush এবং StrokeInputBatch সংরক্ষণ করুন।

  • Brush : সংখ্যাসূচক ক্ষেত্র (আকার, অ্যাপসিলন), রঙ এবং BrushFamily অন্তর্ভুক্ত করে।
  • StrokeInputBatch : সংখ্যাসূচক ক্ষেত্র সহ ইনপুট পয়েন্টের একটি তালিকা।

স্টোরেজ মডিউলটি সবচেয়ে জটিল অংশ: StrokeInputBatch কম্প্যাক্টলি সিরিয়ালাইজ করা সহজ করে।

স্ট্রোক বাঁচাতে:

  • স্টোরেজ মডিউলের এনকোড ফাংশন ব্যবহার করে StrokeInputBatch সিরিয়ালাইজ করুন। ফলে প্রাপ্ত বাইনারি ডেটা সংরক্ষণ করুন।
  • স্ট্রোকের ব্রাশের প্রয়োজনীয় বৈশিষ্ট্যগুলি আলাদাভাবে সংরক্ষণ করুন:
    • ব্রাশ ফ্যামিলি &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 ব্যবহার করুন, যা আপনাকে দৃশ্যমান UI উপাদান ছাড়াই ক্যানভাসে অঙ্কন রেকর্ড করতে দেয়।

এই প্রক্রিয়ার মধ্যে রয়েছে একটি 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)
}

ডেটা অবজেক্ট এবং কনভার্টার সাহায্যকারী

একটি সিরিয়ালাইজেশন অবজেক্ট স্ট্রাকচার সংজ্ঞায়িত করুন যা প্রয়োজনীয় ইঙ্ক এপিআই অবজেক্টগুলিকে প্রতিফলিত করে।

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