Bảo tồn trạng thái và lưu trữ lâu dài

Việc lưu giữ trạng thái và bộ nhớ liên tục là những khía cạnh quan trọng của các ứng dụng viết mực, đặc biệt là trong Compose. Các đối tượng dữ liệu cốt lõi (chẳng hạn như thuộc tính cọ và các điểm tạo thành nét vẽ) rất phức tạp và không tự động duy trì. Điều này đòi hỏi một chiến lược có chủ ý để lưu trạng thái trong các trường hợp như thay đổi cấu hình và lưu vĩnh viễn bản vẽ của người dùng vào cơ sở dữ liệu.

State preservation

In Jetpack Compose, UI state is typically managed using remember and rememberSaveable. While rememberSaveable offers automatic state preservation across configuration changes, its built-in capabilities are limited to primitive data types and objects that implement Parcelable or Serializable.

For custom objects that contain complex properties, such as Brush, you must define explicit serialization and deserialization mechanisms using a custom state saver. By defining a custom Saver for the Brush object, you can preserve the brush's essential attributes when configuration changes occur, as shown in the following brushStateSaver example.

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

You can then use the custom Saver to preserve the selected brush state:

val currentBrush = rememberSaveable(saver = brushStateSaver(Converters())) { mutableStateOf(defaultBrush) }

Bộ nhớ liên tục

Để bật các tính năng như lưu, tải tài liệu và cộng tác theo thời gian thực (nếu có), hãy lưu trữ nét vẽ và dữ liệu liên quan ở định dạng được chuyển đổi tuần tự. Đối với Ink API, bạn cần phải chuyển đổi tuần tự và huỷ chuyển đổi tuần tự theo cách thủ công.

Để khôi phục chính xác một nét vẽ, hãy lưu BrushStrokeInputBatch của nét vẽ đó.

  • Brush: Bao gồm các trường số (kích thước, epsilon), màu sắc và BrushFamily.
  • StrokeInputBatch: Một danh sách các điểm đầu vào có trường số.

Mô-đun Storage (Bộ nhớ) giúp đơn giản hoá việc tuần tự hoá một cách gọn gàng phần phức tạp nhất: StrokeInputBatch.

Cách lưu nét vẽ:

  • Nối tiếp StrokeInputBatch bằng hàm mã hoá của mô-đun lưu trữ. Lưu trữ dữ liệu nhị phân thu được.
  • Lưu riêng các thuộc tính thiết yếu của Brush (Cọ) trong nét vẽ:
    • Enum đại diện cho họ cọ &mdash Mặc dù có thể chuyển đổi thực thể này thành chuỗi, nhưng việc này không hiệu quả đối với những ứng dụng sử dụng một số ít họ cọ
    • 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
  )
}

Cách tải một đối tượng nét vẽ:

  • Truy xuất dữ liệu nhị phân đã lưu cho StrokeInputBatch và giải tuần tự hoá dữ liệu đó bằng hàm decode() của mô-đun lưu trữ.
  • Truy xuất các thuộc tính Brush đã lưu và tạo cọ vẽ.
  • Tạo nét vẽ cuối cùng bằng cọ vẽ được tạo lại và StrokeInputBatch đã chuyển đổi tuần tự.

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

Xử lý thao tác thu phóng, dịch chuyển và xoay

Nếu ứng dụng của bạn hỗ trợ thao tác thu phóng, xoay hoặc di chuyển, bạn phải cung cấp thông tin biến đổi hiện tại cho InProgressStrokes. Điều này giúp các nét vẽ mới vẽ khớp với vị trí và tỷ lệ của các nét vẽ hiện có.

Bạn thực hiện việc này bằng cách truyền một Matrix đến tham số pointerEventToWorldTransform. Ma trận này phải biểu thị nghịch đảo của phép biến đổi mà bạn áp dụng cho canvas nét vẽ đã hoàn thành.

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

Xuất nét vẽ

You might need to export your stroke scene as a static image file. This is useful for sharing the drawing with other applications, generating thumbnails, or saving a final, uneditable version of the content.

To export a scene, you can render your strokes to an offscreen bitmap instead of directly to the screen. Use Android's Picture API, which lets you record drawings on a canvas without needing a visible UI component.

The process involves creating a Picture instance, calling beginRecording() to get a Canvas, and then using your existing CanvasStrokeRenderer to draw each stroke onto that Canvas. After you record all the drawing commands, you can use the Picture to create a Bitmap, which you can then compress and save to a file.

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

Đối tượng dữ liệu và các trình trợ giúp trình chuyển đổi

Xác định cấu trúc đối tượng tuần tự hoá phản ánh các đối tượng Ink API cần thiết.

Sử dụng mô-đun lưu trữ của Ink API để mã hoá và giải mã StrokeInputBatch.

Đối tượng chuyển dữ liệu
@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,
}
Người chuyển đổi
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,
    )
  }
}