Conservazione dello stato e archiviazione permanente

La conservazione dello stato e l'archiviazione permanente sono aspetti non banali delle app di inchiostro digitale, soprattutto in Compose. Gli oggetti dati principali, come le proprietà del pennello e i punti che formano un tratto, sono complessi e non vengono salvati automaticamente. Ciò richiede una strategia deliberata per il salvataggio dello stato durante scenari come modifiche alla configurazione e salvataggio permanente dei disegni di un utente in un database.

Conservazione dello stato

In Jetpack Compose, lo stato dell'UI viene in genere gestito utilizzando remember e rememberSaveable. Sebbene rememberSaveable offra la conservazione automatica dello stato in caso di modifiche alla configurazione, le sue funzionalità integrate sono limitate a tipi di dati primitivi e oggetti che implementano Parcelable o Serializable.

Per gli oggetti personalizzati che contengono proprietà complesse, ad esempio Brush, devi definire meccanismi espliciti di serializzazione e deserializzazione utilizzando un salvataggio dello stato personalizzato. Definendo un Saver personalizzato per l'oggetto Brush, puoi conservare gli attributi essenziali del pennello quando si verificano modifiche alla configurazione, come mostrato nell'esempio brushStateSaver seguente.

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

Puoi quindi utilizzare il Saver personalizzato per mantenere lo stato del pennello selezionato:

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

Archiviazione permanente

为了实现文档保存、加载和潜在的实时协作等功能,请以序列化格式存储笔画和关联数据。对于 Ink API,需要手动进行序列化和反序列化。

如需准确恢复笔画,请保存其 BrushStrokeInputBatch

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

如需加载笔画对象,请执行以下操作:

  • 检索 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 提供当前转换。这有助于使新绘制的笔画与现有笔画的位置和缩放比例保持一致。

为此,您需要向 pointerEventToWorldTransform 参数传递 Matrix。该矩阵应表示您应用于完成的笔画画布的转换的逆。

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

导出笔画

Potresti dover esportare la scena del tratto come file immagine statica. Questa funzionalità è utile per condividere il disegno con altre applicazioni, generare miniature o salvare una versione finale e non modificabile dei contenuti.

Per esportare una scena, puoi eseguire il rendering dei tratti in una bitmap off-screen anziché direttamente sullo schermo. Utilizza Android's Picture API, che ti consente di registrare disegni su una tela senza bisogno di un componente UI visibile.

La procedura prevede la creazione di un'istanza Picture, la chiamata di beginRecording() per ottenere un Canvas e l'utilizzo del CanvasStrokeRenderer esistente per disegnare ogni tratto su quel Canvas. Dopo aver registrato tutti i comandi di disegno, puoi utilizzare Picture per creare un Bitmap, che puoi comprimere e salvare in un 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)
}

数据对象和转换器辅助程序

定义一个与所需 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,
    )
  }
}