حفظ دولتی و ذخیره سازی مداوم

حفظ وضعیت و ذخیره‌سازی پایدار، جنبه‌های غیر بدیهی برنامه‌های inking، به ویژه در Compose هستند. اشیاء داده اصلی، مانند ویژگی‌های قلم‌مو و نقاطی که یک stroke را تشکیل می‌دهند، پیچیده هستند و به طور خودکار پایدار نمی‌مانند. این امر مستلزم یک استراتژی آگاهانه برای ذخیره وضعیت در سناریوهایی مانند تغییرات پیکربندی و ذخیره دائمی ترسیمات کاربر در یک پایگاه داده است.

حفظ ایالت

در Jetpack Compose، وضعیت رابط کاربری معمولاً با استفاده از remember و rememberSaveable مدیریت می‌شود. در حالی که rememberSaveable امکان حفظ خودکار وضعیت را در طول تغییرات پیکربندی فراهم می‌کند، قابلیت‌های داخلی آن به انواع داده‌های اولیه و اشیاء که Parcelable یا Serializable را پیاده‌سازی می‌کنند، محدود می‌شود.

برای اشیاء سفارشی که حاوی ویژگی‌های پیچیده‌ای هستند، مانند Brush ، باید مکانیسم‌های سریال‌سازی و حذف سریال‌سازی صریح را با استفاده از یک ذخیره‌کننده وضعیت سفارشی تعریف کنید. با تعریف یک Saver سفارشی برای شیء Brush ، می‌توانید ویژگی‌های اساسی قلم‌مو را هنگام تغییرات پیکربندی حفظ کنید، همانطور که در مثال 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) }

ذخیره‌سازی پایدار

برای فعال کردن ویژگی‌هایی مانند ذخیره، بارگذاری و همکاری بالقوه در لحظه سند، strokeها و داده‌های مرتبط را در قالب سریالی ذخیره کنید. برای Ink API، سریال‌سازی و deserialization دستی ضروری است.

برای بازیابی دقیق یک stroke، Brush و StrokeInputBatch آن را ذخیره کنید.

  • Brush ): شامل فیلدهای عددی (اندازه، اپسیلون)، رنگ و BrushFamily است.
  • StrokeInputBatch : فهرستی از نقاط ورودی با فیلدهای عددی.

ماژول Storage، سریال‌سازی فشرده پیچیده‌ترین بخش یعنی StrokeInputBatch ساده می‌کند.

برای نجات از سکته مغزی:

  • با استفاده از تابع encode ماژول ذخیره‌سازی، 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
  )
}

برای بارگذاری یک شیء stroke:

  • داده‌های دودویی ذخیره شده برای StrokeInputBatch را بازیابی کرده و با استفاده از تابع decode() ماژول ذخیره‌سازی، آن را deserialize کنید.
  • ویژگی‌های ذخیره‌شده‌ی Brush را بازیابی کنید و قلم‌مو را ایجاد کنید.
  • با استفاده از قلم‌مو بازسازی‌شده و StrokeInputBatch از حالت سریال خارج‌شده، stroke نهایی را ایجاد کنید.

    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 ارائه دهید. این به ترسیم خطوط جدید کمک می‌کند تا موقعیت و مقیاس خطوط موجود شما را مطابقت دهند.

شما این کار را با ارسال یک Matrix به پارامتر pointerEventToWorldTransform انجام می‌دهید. این ماتریس باید معکوس تبدیلی را که روی بوم stroke های نهایی خود اعمال می‌کنید، نشان دهد.

@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 موجود برای ترسیم هر stroke روی آن 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)
}

کمک‌کننده‌های شیء داده و مبدل

یک ساختار شیء سریال‌سازی تعریف کنید که اشیاء API مورد نیاز Ink را منعکس کند.

از ماژول ذخیره‌سازی Ink API برای رمزگذاری و رمزگشایی StrokeInputBatch استفاده کنید.

اشیاء انتقال داده
@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,
    )
  }
}