狀態保留與永久儲存空間

狀態保留和永久儲存空間是墨水筆跡應用程式的重要環節,尤其是在 Compose 中。筆刷屬性和構成筆劃的點等核心資料物件相當複雜,不會自動保留。這需要有策略地在設定變更等情況下儲存狀態,並將使用者的繪圖永久儲存至資料庫。

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

永久儲存空間

如要啟用儲存、載入文件和即時協作等功能,請以序列化格式儲存筆劃和相關資料。使用 Ink API 時,必須手動序列化和去序列化。

如要準確還原筆劃,請儲存筆劃的 BrushStrokeInputBatch

Storage 模組可簡化最複雜部分的緊湊序列化作業:StrokeInputBatch

如何儲存筆劃:

  • 使用儲存空間模組的編碼函式,將 StrokeInputBatch 序列化。 儲存產生的二進位資料。
  • 分別儲存筆劃 Brush 的必要屬性:
    • 代表筆刷系列的列舉 &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 參數。這個矩陣應代表您套用至完成筆劃畫布的轉換反向。

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

資料物件和轉換器輔助程式

定義與所需 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,
}
轉換人數
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,
    )
  }
}