שמירת מצב ואחסון קבוע הם היבטים חשובים של אפליקציות לציור בדיו דיגיטלי, במיוחד ב-Compose. אובייקטי הנתונים הבסיסיים, כמו מאפייני המברשת והנקודות שיוצרות קו, הם מורכבים ולא נשמרים אוטומטית. לכן נדרשת אסטרטגיה מחושבת לשמירת מצב במהלך תרחישים כמו שינויים בהגדרות ושמירה קבועה של ציורים של משתמשים במסד נתונים.
状态保留
在 Jetpack Compose 中,界面状态通常使用 remember 和 rememberSaveable 进行管理。虽然 rememberSaveable 可在配置更改期间自动保留状态,但其内置功能仅限于实现 Parcelable 或 Serializable 的基本数据类型和对象。
对于包含复杂属性(例如 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) }
אחסון קבוע
כדי להפעיל תכונות כמו שמירה, טעינה ושיתוף פעולה בזמן אמת, צריך לאחסן את הקווים והנתונים המשויכים בפורמט סריאלי. ב-Ink API, צריך לבצע סריאליזציה ודה-סריאליזציה באופן ידני.
כדי לשחזר קו בצורה מדויקת, שומרים את Brush וStrokeInputBatch שלו.
-
Brush: כולל שדות מספריים (גודל, אפסילון), צבע ו-BrushFamily. -
StrokeInputBatch: רשימה של נקודות קלט עם שדות מספריים.
מודול האחסון מפשט את הסריאליזציה של החלק המורכב ביותר: StrokeInputBatch.
כדי לשמור קו:
- מבצעים סריאליזציה של
StrokeInputBatchבאמצעות פונקציית הקידוד של מודול האחסון. אחסון הנתונים הבינאריים שמתקבלים. - שומרים בנפרד את המאפיינים החיוניים של המברשת של הקו:
- ה-enum שמייצג את משפחת המברשות &mdash למרות שאפשר לבצע סריאליזציה של המופע, זה לא יעיל לאפליקציות שמשתמשות במבחר מוגבל של משפחות מברשות
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
)
}
כדי לטעון אובייקט של קו:
- מאחזרים את הנתונים הבינאריים השמורים של
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. כך, הקווים החדשים שציירתם יתאימו למיקום ולגודל של הקווים הקיימים.
כדי לעשות זאת, מעבירים את הפרמטר Matrix אל pointerEventToWorldTransform. המטריצה צריכה לייצג את ההיפוך של השינוי שאתם מבצעים בבד הציור של הקווים הסופיים.
@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
)
}
}
ייצוא של משיכות מכחול
יכול להיות שתצטרכו לייצא את סצנת המשיכות כקובץ תמונה סטטית. האפשרות הזו שימושית לשיתוף הציור עם אפליקציות אחרות, ליצירת תמונות ממוזערות או לשמירת גרסה סופית של התוכן שלא ניתן לערוך.
כדי לייצא סצנה, אפשר לעבד את המשיכות לביטמפ מחוץ למסך במקום ישירות למסך. אפשר להשתמש ב-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,
)
}
}