الحفاظ على الحالة ومساحة التخزين الدائمة

يُعدّ الحفاظ على الحالة والتخزين الدائم من الجوانب المهمة في تطبيقات الكتابة بالحبر الرقمي، خاصةً في Compose. إنّ عناصر البيانات الأساسية، مثل خصائص الفرشاة والنقاط التي تشكّل ضربة فرشاة، معقّدة ولا يتم الاحتفاظ بها تلقائيًا. ويتطلّب ذلك استراتيجية مدروسة لحفظ الحالة أثناء سيناريوهات مثل تغييرات الإعدادات وحفظ رسومات المستخدم بشكل دائم في قاعدة بيانات.

الاحتفاظ بالحالة

في Jetpack Compose، تتم إدارة حالة واجهة المستخدم عادةً باستخدام remember و rememberSaveable. على الرغم من أنّ rememberSaveable توفّر ميزة الحفاظ التلقائي على الحالة عند إجراء تغييرات في الإعدادات، تقتصر إمكاناتها المضمّنة على أنواع البيانات الأساسية والعناصر التي تنفّذ Parcelable أو Serializable.

بالنسبة إلى العناصر المخصّصة التي تحتوي على سمات معقّدة، مثل Brush، عليك تحديد آليات تسلسلية وتفكيك تسلسلي صريحة. ويكون من المفيد استخدام أداة مخصّصة لحفظ الحالة. من خلال تحديد Saver مخصّص للكائن Brush، يمكنك الحفاظ على سماته الأساسية عند حدوث تغييرات في الإعدادات، كما هو موضّح في المثال 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: قائمة بنقاط الإدخال التي تحتوي على حقول رقمية.

التسلسل الأساسي

حدِّد بنية عنصر تسلسلية تتطابق مع عناصر مكتبة Ink.

ترميز البيانات المتسلسلة باستخدام إطار العمل المفضّل لديك، مثل Gson وMoshi وProtobuf وغيرها، واستخدام الضغط لتحسين الأداء

data class SerializedStroke(
  val inputs: SerializedStrokeInputBatch,
  val brush: SerializedBrush
)

data class SerializedBrush(
  val size: Float,
  val color: Long,
  val epsilon: Float,
  val stockBrush: SerializedStockBrush
)

enum class SerializedStockBrush {
  MARKER_V1,
  PRESSURE_PEN_V1,
  HIGHLIGHTER_V1
}

data class SerializedStrokeInputBatch(
  val toolType: SerializedToolType,
  val strokeUnitLengthCm: Float,
  val inputs: List<SerializedStrokeInput>
)

data class SerializedStrokeInput(
  val x: Float,
  val y: Float,
  val timeMillis: Float,
  val pressure: Float,
  val tiltRadians: Float,
  val orientationRadians: Float,
  val strokeUnitLengthCm: Float
)

enum class SerializedToolType {
  STYLUS,
  TOUCH,
  MOUSE,
  UNKNOWN
}
class Converters {

  private val gson: Gson = GsonBuilder().create()

  companion object {
    private val stockBrushToEnumValues =
      mapOf(
        StockBrushes.markerV1 to SerializedStockBrush.MARKER_V1,
        StockBrushes.pressurePenV1 to SerializedStockBrush.PRESSURE_PEN_V1,
        StockBrushes.highlighterV1 to SerializedStockBrush.HIGHLIGHTER_V1,
      )

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

  private fun serializeStrokeInputBatch(inputs: StrokeInputBatch): SerializedStrokeInputBatch {
    val serializedInputs = mutableListOf<SerializedStrokeInput>()
    val scratchInput = StrokeInput()

    for (i in 0 until inputs.size) {
      inputs.populate(i, scratchInput)
      serializedInputs.add(
        SerializedStrokeInput(
          x = scratchInput.x,
          y = scratchInput.y,
          timeMillis = scratchInput.elapsedTimeMillis.toFloat(),
          pressure = scratchInput.pressure,
          tiltRadians = scratchInput.tiltRadians,
          orientationRadians = scratchInput.orientationRadians,
          strokeUnitLengthCm = scratchInput.strokeUnitLengthCm,
        )
      )
    }

    val toolType =
      when (inputs.getToolType()) {
        InputToolType.STYLUS -> SerializedToolType.STYLUS
        InputToolType.TOUCH -> SerializedToolType.TOUCH
        InputToolType.MOUSE -> SerializedToolType.MOUSE
        else -> SerializedToolType.UNKNOWN
      }

    return SerializedStrokeInputBatch(
      toolType = toolType,
      strokeUnitLengthCm = inputs.getStrokeUnitLengthCm(),
      inputs = serializedInputs,
    )
  }

  private fun deserializeStroke(serializedStroke: SerializedStroke): Stroke? {
    val inputs = deserializeStrokeInputBatch(serializedStroke.inputs) ?: return null
    val brush = deserializeBrush(serializedStroke.brush) ?: return null
    return Stroke(brush = brush, inputs = inputs)
  }

  private fun deserializeBrush(serializedBrush: SerializedBrush): Brush {
    val stockBrushFamily = enumToStockBrush[serializedBrush.stockBrush] ?: StockBrushes.markerV1

    return Brush.createWithColorLong(
      family = stockBrushFamily,
      colorLong = serializedBrush.color,
      size = serializedBrush.size,
      epsilon = serializedBrush.epsilon,
    )
  }

  private fun deserializeStrokeInputBatch(
    serializedBatch: SerializedStrokeInputBatch
  ): StrokeInputBatch {
    val toolType =
      when (serializedBatch.toolType) {
        SerializedToolType.STYLUS -> InputToolType.STYLUS
        SerializedToolType.TOUCH -> InputToolType.TOUCH
        SerializedToolType.MOUSE -> InputToolType.MOUSE
        else -> InputToolType.UNKNOWN
      }

    val batch = MutableStrokeInputBatch()

    serializedBatch.inputs.forEach { input ->
      batch.addOrThrow(
        type = toolType,
        x = input.x,
        y = input.y,
        elapsedTimeMillis = input.timeMillis.toLong(),
        pressure = input.pressure,
        tiltRadians = input.tiltRadians,
        orientationRadians = input.orientationRadians,
      )
    }

    return batch
  }

  fun serializeStrokeToEntity(stroke: Stroke): StrokeEntity {
    val serializedBrush = serializeBrush(stroke.brush)
    val serializedInputs = serializeStrokeInputBatch(stroke.inputs)
    return StrokeEntity(
      brushSize = serializedBrush.size,
      brushColor = serializedBrush.color,
      brushEpsilon = serializedBrush.epsilon,
      stockBrush = serializedBrush.stockBrush,
      strokeInputs = gson.toJson(serializedInputs),
    )
  }

  fun deserializeEntityToStroke(entity: StrokeEntity): Stroke {
    val serializedBrush =
      SerializedBrush(
        size = entity.brushSize,
        color = entity.brushColor,
        epsilon = entity.brushEpsilon,
        stockBrush = entity.stockBrush,
      )

    val serializedInputs =
      gson.fromJson(entity.strokeInputs, SerializedStrokeInputBatch::class.java)

    val brush = deserializeBrush(serializedBrush)
    val inputs = deserializeStrokeInputBatch(serializedInputs)

    return Stroke(brush = brush, inputs = inputs)
  }

  fun brushToString(brush: Brush): String {
        val serializedBrush = serializeBrush(brush)
        return gson.toJson(serializedBrush)
    }

  fun stringToBrush(jsonString: String): Brush {
        val serializedBrush = gson.fromJson(jsonString, SerializedBrush::class.java)
        return deserializeBrush(serializedBrush)
    }

}