การเก็บรักษาสถานะและพื้นที่เก็บข้อมูลถาวร

การรักษาสถานะและการจัดเก็บข้อมูลแบบถาวรเป็นแง่มุมที่สำคัญของแอปการเขียนด้วยลายมือ โดยเฉพาะใน Compose ออบเจ็กต์ข้อมูลหลัก เช่น พร็อพเพอร์ตี้ของแปรงและจุดที่สร้างสโตรก มีความซับซ้อนและไม่ได้คงอยู่โดยอัตโนมัติ ซึ่งต้องมีกลยุทธ์ที่รอบคอบในการบันทึกสถานะในระหว่างสถานการณ์ต่างๆ เช่น การเปลี่ยนแปลงการกำหนดค่าและการบันทึกภาพวาดของผู้ใช้ลงในฐานข้อมูลอย่างถาวร

การเก็บรักษาสถานะ

ใน Jetpack Compose โดยทั่วไปแล้วจะมีการจัดการสถานะ UI โดยใช้ remember และ rememberSaveable แม้ว่า rememberSaveable จะมีการรักษาสถานะอัตโนมัติเมื่อมีการเปลี่ยนแปลงการกำหนดค่า แต่ความสามารถในตัวของ Parcelableหรือ Serializableจะจำกัดไว้ที่ประเภทข้อมูลดั้งเดิมและออบเจ็กต์ที่ใช้

สําหรับออบเจ็กต์ที่กําหนดเองซึ่งมีพร็อพเพอร์ตี้ที่ซับซ้อน เช่น Brush คุณต้องกําหนดกลไกการทำ Serialization และ Deserialization อย่างชัดเจน โดยใช้เครื่องมือบันทึกสถานะที่กำหนดเอง การกำหนด 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) }

พื้นที่เก็บข้อมูลถาวร

หากต้องการเปิดใช้ฟีเจอร์ต่างๆ เช่น การบันทึก การโหลดเอกสาร และการทำงานร่วมกันแบบเรียลไทม์ ที่อาจเกิดขึ้น ให้จัดเก็บลายเส้นและข้อมูลที่เกี่ยวข้องในรูปแบบที่เรียงลำดับ สำหรับ Ink API คุณจะต้องทำ Serialization และ Deserialization ด้วยตนเอง

หากต้องการกู้คืนลายเส้นอย่างถูกต้อง ให้บันทึก 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 ซึ่งจะช่วยให้เส้นที่วาดใหม่ตรงกับ ตำแหน่งและขนาดของเส้นที่มีอยู่

โดยทำได้ด้วยการส่ง Matrix ไปยังพารามิเตอร์ pointerEventToWorldTransform เมทริกซ์ควรแสดงค่าผกผันของการเปลี่ยนรูปที่คุณ ใช้กับ Canvas ของเส้นที่วาดเสร็จแล้ว

@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 ที่จำเป็น

ใช้โมดูลพื้นที่เก็บข้อมูลของ 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,
}
ผู้ทำ Conversion
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,
    )
  }
}