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.
Conservation de l'état
Dans Jetpack Compose, l'état de l'UI est généralement géré à l'aide de remember et rememberSaveable.
Bien que rememberSaveable offre une préservation automatique de l'état lors des modifications de configuration, ses fonctionnalités intégrées sont limitées aux types de données primitifs et aux objets qui implémentent Parcelable ou Serializable.
Pour les objets personnalisés contenant des propriétés complexes, telles que Brush, vous devez définir des mécanismes de sérialisation et de désérialisation explicites à l'aide d'un économiseur d'état personnalisé. En définissant un Saver personnalisé pour l'objet Brush, vous pouvez conserver les attributs essentiels du pinceau lorsque des modifications de configuration se produisent, comme illustré dans l'exemple brushStateSaver suivant.
fun brushStateSaver(converters: Converters): Saver<MutableState<Brush>, SerializedBrush> = Saver(
save = { converters.serializeBrush(it.value) },
restore = { mutableStateOf(converters.deserializeBrush(it)) },
)
Vous pouvez ensuite utiliser le Saver personnalisé pour conserver l'état du pinceau sélectionné :
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 etBrushFamily.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.
colorLongsizeepsilon
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
StrokeInputBatchet désérialisez-les à l'aide de la fonction decode() du module de stockage. - Récupérez les propriétés
Brushenregistrées et créez le pinceau. Créez le trait final à l'aide du pinceau recréé et de l'objet
StrokeInputBatchdé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,
)
}
}