Die Statusbeibehaltung und der nichtflüchtige Speicher sind wichtige Aspekte von Apps, die die Eingabe mit einem Stift unterstützen. Dazu ist eine durchdachte Strategie zum Speichern des Status in Szenarien wie Konfigurationsänderungen und zum dauerhaften Speichern der Zeichnungen eines Nutzers in einer Datenbank erforderlich.
Status beibehalten
In ansichtsbasierten Apps wird der UI-Status mithilfe einer Kombination aus Folgendem verwaltet:
ViewModel-Objekte- Gespeicherter Instanzstatus mit:
- Aktivität
onSaveInstanceState() - ViewModel SavedStateHandle
- Lokaler Speicher zum Beibehalten des UI-Status während App- und Aktivitätsübergängen
- Aktivität
Weitere Informationen finden Sie unter UI-Zustände speichern.
Nichtflüchtiger Speicher
Um Funktionen wie das Speichern und Laden von Dokumenten und die mögliche Zusammenarbeit in Echtzeit zu ermöglichen, müssen Sie Striche und zugehörige Daten in einem serialisierten Format speichern. Für die Ink API ist eine manuelle Serialisierung und Deserialisierung erforderlich.
Wenn Sie einen Strich genau wiederherstellen möchten, speichern Sie dessen Brush und StrokeInputBatch.
Brush: Enthält numerische Felder (Größe, Epsilon), Farbe undBrushFamily.StrokeInputBatch: Eine Liste von Eingabepunkten mit numerischen Feldern.
Das Speichermodul vereinfacht die kompakte Serialisierung des komplexesten Teils: der StrokeInputBatch.
So speichern Sie einen Strich:
- Serialisieren Sie
StrokeInputBatchmit der Codierungsfunktion des Speichermoduls. Speichern Sie die resultierenden Binärdaten. - Speichern Sie die wichtigsten Eigenschaften des Pinsels für den Strich separat:
- Die Enumeration, die die Pinselgruppe darstellt — Obwohl die Instanz serialisiert werden kann, ist dies für Apps, die nur eine begrenzte Auswahl an Pinselgruppen verwenden, nicht effizient.
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
)
}
So laden Sie ein Strichobjekt:
- Rufen Sie die gespeicherten Binärdaten für
StrokeInputBatchab und deserialisieren Sie sie mit der Funktion decode() des Speichermoduls. - Rufen Sie die gespeicherten
Brush-Attribute ab und erstellen Sie den Pinsel. Erstellen Sie den endgültigen Strich mit dem neu erstellten Pinsel und dem deserialisierten
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) }
Zoomen, Schwenken und Drehen
Wenn Ihre App das Zoomen, Schwenken oder Drehen unterstützt, müssen Sie die aktuelle Transformation für InProgressStrokes angeben. So werden neu gezeichnete Striche an die Position und Skalierung Ihrer vorhandenen Striche angepasst.
Dazu übergeben Sie einen Matrix-Wert an den Parameter pointerEventToWorldTransform. Die Matrix sollte die Umkehrung der Transformation darstellen, die Sie auf die Leinwand mit den fertigen Strichen anwenden.
@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
)
}
}
Striche exportieren
Möglicherweise müssen Sie die Strichszene als statische Bilddatei exportieren. Das ist nützlich, wenn Sie die Zeichnung für andere Anwendungen freigeben, Thumbnails generieren oder eine endgültige, nicht bearbeitbare Version der Inhalte speichern möchten.
Wenn Sie eine Szene exportieren möchten, können Sie die Striche in ein Offscreen-Bitmap rendern, anstatt direkt auf den Bildschirm. Verwenden Sie Android's Picture API, um Zeichnungen auf einer Zeichenfläche aufzuzeichnen, ohne dass eine sichtbare UI-Komponente erforderlich ist.
Dazu müssen Sie eine Picture-Instanz erstellen, beginRecording() aufrufen, um eine Canvas zu erhalten, und dann mit Ihrer vorhandenen CanvasStrokeRenderer jeden Strich auf diese Canvas zeichnen. Nachdem Sie alle Zeichenbefehle aufgezeichnet haben, können Sie mit Picture ein Bitmap erstellen, das Sie dann komprimieren und in einer Datei speichern können.
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)
}
Hilfsklassen für Datenobjekte und Konverter
Definieren Sie eine Serialisierungsobjektstruktur, die die erforderlichen Ink API-Objekte widerspiegelt.
Verwenden Sie das Speichermodul der Ink API, um StrokeInputBatch zu codieren und zu decodieren.
Datenübertragungsobjekte
@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,
}
Nutzer mit 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,
)
}
}