Conservation de l'état et stockage persistant

La conservation de l'état et le stockage persistant sont des aspects non triviaux des applications d'encrage, en particulier dans Compose. Les objets de données principaux, tels que les propriétés du pinceau et les points qui forment un trait, sont complexes et ne sont pas conservés automatiquement. Cela nécessite une stratégie délibérée pour enregistrer l'état dans des scénarios tels que les changements de configuration et l'enregistrement permanent des dessins d'un utilisateur dans une base de données.

状态保留

在 Jetpack Compose 中,界面状态通常使用 rememberrememberSaveable 进行管理。虽然 rememberSaveable 可在配置更改期间自动保留状态,但其内置功能仅限于实现 ParcelableSerializable 的基本数据类型和对象。

对于包含复杂属性(例如 Brush)的自定义对象,您必须使用自定义状态保存器定义显式序列化和反序列化机制。通过为 Brush 对象定义自定义 Saver,您可以在配置发生更改时保留画笔的基本属性,如以下 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) }

Stockage persistant

Pour activer des fonctionnalités telles que l'enregistrement et le chargement de documents, ainsi que la collaboration potentielle en temps réel, stockez les traits et les données associées dans un format sérialisé. Pour l'API Ink, la sérialisation et la désérialisation manuelles sont nécessaires.

Pour restaurer un trait avec précision, enregistrez ses Brush et StrokeInputBatch.

  • Brush : inclut les champs numériques (taille, epsilon), la couleur et BrushFamily.
  • StrokeInputBatch : liste de points d'entrée avec des champs numériques.

Le module Storage simplifie la sérialisation compacte de la partie la plus complexe : StrokeInputBatch.

Pour enregistrer un tracé :

  • Sérialisez le StrokeInputBatch à l'aide de la fonction d'encodage du module de stockage. Stockez les données binaires obtenues.
  • Enregistrez séparément les propriétés essentielles du pinceau du tracé :
    • Énumération qui représente la famille de pinceaux. Bien que l'instance puisse être sérialisée, cela n'est pas efficace pour les applications qui utilisent une sélection limitée de familles de pinceaux.
    • 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
  )
}

Pour charger un objet de trait :

  • Récupérez les données binaires enregistrées pour StrokeInputBatch et désérialisez-les à l'aide de la fonction decode() du module de stockage.
  • Récupérez les propriétés Brush enregistrées et créez le pinceau.
  • Créez le trait final à l'aide du pinceau recréé et de l'objet StrokeInputBatch désérialisé.

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

Gérer le zoom, le panoramique et la rotation

Si votre application est compatible avec le zoom, le déplacement ou la rotation, vous devez fournir la transformation actuelle à InProgressStrokes. Cela permet aux traits nouvellement dessinés de correspondre à la position et à l'échelle de vos traits existants.

Pour ce faire, transmettez un Matrix au paramètre pointerEventToWorldTransform. La matrice doit représenter l'inverse de la transformation que vous appliquez au canevas de vos traits finis.

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

Exporter les traits

Vous devrez peut-être exporter votre scène de traits sous forme de fichier image statique. Cela s'avère utile pour partager le dessin avec d'autres applications, générer des miniatures ou enregistrer une version finale et non modifiable du contenu.

Pour exporter une scène, vous pouvez rendre vos traits dans un bitmap hors écran au lieu de les rendre directement à l'écran. Utilisez Android's Picture API, qui vous permet d'enregistrer des dessins sur un canevas sans avoir besoin d'un composant d'UI visible.

Le processus consiste à créer une instance Picture, à appeler beginRecording() pour obtenir un Canvas, puis à utiliser votre CanvasStrokeRenderer existant pour dessiner chaque trait sur ce Canvas. Une fois que vous avez enregistré toutes les commandes de dessin, vous pouvez utiliser Picture pour créer un Bitmap, que vous pouvez ensuite compresser et enregistrer dans un fichier.

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

Assistance pour les objets de données et les convertisseurs

Définissez une structure d'objet de sérialisation qui reflète les objets Ink API nécessaires.

Utilisez le module de stockage de l'API Ink pour encoder et décoder StrokeInputBatch.

Objets de transfert de données
@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,
}
Visiteurs ayant déjà réalisé une 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,
    )
  }
}