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
Per abilitare funzionalità come il salvataggio, il caricamento e la potenziale collaborazione in tempo reale, memorizza i tratti e i dati associati in un formato serializzato. Per l'API Ink, sono necessarie la serializzazione e la deserializzazione manuali.
Per ripristinare con precisione un tratto, salva il relativo Brush e StrokeInputBatch.
Brush: include campi numerici (dimensione, epsilon), colore eBrushFamily.StrokeInputBatch: Un elenco di punti di input con campi numerici.
Il modulo Storage semplifica la serializzazione compatta della parte più complessa: il
StrokeInputBatch.
Per salvare un tratto:
- Serializza
StrokeInputBatchutilizzando la funzione di codifica del modulo di archiviazione. Archivia i dati binari risultanti. - Salva separatamente le proprietà essenziali del pennello del tratto:
- L'enumerazione che rappresenta la famiglia di pennelli. Anche se l'istanza può essere serializzata, questa operazione non è efficiente per le app che utilizzano una selezione limitata di famiglie di pennelli.
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
)
}
Per caricare un oggetto tratto:
- Recupera i dati binari salvati per
StrokeInputBatche deserializzali utilizzando la funzione decode() del modulo di archiviazione. - Recupera le proprietà
Brushsalvate e crea il pennello. Crea il tratto finale utilizzando il pennello ricreato e il
StrokeInputBatchdeserializzato.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) }
Gestire zoom, panoramica e rotazione
Se la tua app supporta lo zoom, lo spostamento o la rotazione, devi fornire la trasformazione attuale a InProgressStrokes. In questo modo, i tratti appena disegnati corrispondono
alla posizione e alla scala dei tratti esistenti.
Per farlo, passa un Matrix al parametro pointerEventToWorldTransform. La matrice deve rappresentare l'inverso della trasformazione che
applichi al canvas dei tratti finiti.
@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
)
}
}
Esportare i tratti
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)
}
Helper per oggetti dati e convertitori
Definisci una struttura di oggetti di serializzazione che rispecchi gli oggetti API Ink necessari.
Utilizza il modulo di archiviazione dell'API Ink per codificare e decodificare StrokeInputBatch.
Oggetti di trasferimento dei dati
@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,
}
Utenti che hanno completato una conversione
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,
)
}
}