상태 보존 및 영구 스토리지

상태 보존 및 영구 스토리지는 잉크 앱의 중요한 측면입니다. 이를 위해서는 구성 변경과 같은 시나리오에서 상태를 저장하고 사용자의 그림을 데이터베이스에 영구적으로 저장하기 위한 신중한 전략이 필요합니다.

상태 보존

뷰 기반 앱에서 UI 상태는 다음의 조합을 사용하여 관리됩니다.

UI 상태 저장을 참고하세요.

영구 스토리지

为了实现文档保存、加载和潜在的实时协作等功能,请以序列化格式存储笔画和关联数据。对于 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
        )
    }
}

导出笔画

획 장면을 정적 이미지 파일로 내보내야 할 수도 있습니다. 이는 그림을 다른 애플리케이션과 공유하거나, 썸네일을 생성하거나, 수정할 수 없는 최종 버전의 콘텐츠를 저장하는 데 유용합니다.

장면을 내보내려면 스트로크를 화면에 직접 렌더링하는 대신 오프스크린 비트맵으로 렌더링하면 됩니다. 표시되는 UI 구성요소 없이 캔버스에 그림을 기록할 수 있는 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,
}
转化者数量
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,
    )
  }
}