حفظ وضعیت و ذخیرهسازی پایدار، جنبههای غیر بدیهی برنامههای inking، به ویژه در Compose هستند. اشیاء داده اصلی، مانند ویژگیهای قلممو و نقاطی که یک stroke را تشکیل میدهند، پیچیده هستند و به طور خودکار پایدار نمیمانند. این امر مستلزم یک استراتژی آگاهانه برای ذخیره وضعیت در سناریوهایی مانند تغییرات پیکربندی و ذخیره دائمی ترسیمات کاربر در یک پایگاه داده است.
حفظ ایالت
در Jetpack Compose، وضعیت رابط کاربری معمولاً با استفاده از remember و rememberSaveable مدیریت میشود. در حالی که rememberSaveable امکان حفظ خودکار وضعیت را در طول تغییرات پیکربندی فراهم میکند، قابلیتهای داخلی آن به انواع دادههای اولیه و اشیاء که Parcelable یا Serializable را پیادهسازی میکنند، محدود میشود.
برای اشیاء سفارشی که حاوی ویژگیهای پیچیدهای هستند، مانند Brush ، باید مکانیسمهای سریالسازی و حذف سریالسازی صریح را با استفاده از یک ذخیرهکننده وضعیت سفارشی تعریف کنید. با تعریف یک Saver سفارشی برای شیء Brush ، میتوانید ویژگیهای اساسی قلممو را هنگام تغییرات پیکربندی حفظ کنید، همانطور که در مثال 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) }
ذخیرهسازی پایدار
برای فعال کردن ویژگیهایی مانند ذخیره، بارگذاری و همکاری بالقوه در لحظه سند، strokeها و دادههای مرتبط را در قالب سریالی ذخیره کنید. برای Ink API، سریالسازی و deserialization دستی ضروری است.
برای بازیابی دقیق یک stroke، Brush و StrokeInputBatch آن را ذخیره کنید.
-
Brush): شامل فیلدهای عددی (اندازه، اپسیلون)، رنگ وBrushFamilyاست. -
StrokeInputBatch: فهرستی از نقاط ورودی با فیلدهای عددی.
ماژول Storage، سریالسازی فشرده پیچیدهترین بخش یعنی StrokeInputBatch ساده میکند.
برای نجات از سکته مغزی:
- با استفاده از تابع encode ماژول ذخیرهسازی،
StrokeInputBatchرا سریالایز کنید. دادههای دودویی حاصل را ذخیره کنید. - ویژگیهای ضروری قلممو مربوط به خط دور را جداگانه ذخیره کنید:
- شمارشی که خانواده قلممو را نشان میدهد &mdash اگرچه میتوان نمونه را سریالسازی کرد، اما این برای برنامههایی که از انتخاب محدودی از خانوادههای قلممو استفاده میکنند، کارآمد نیست.
-
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
)
}
برای بارگذاری یک شیء stroke:
- دادههای دودویی ذخیره شده برای
StrokeInputBatchرا بازیابی کرده و با استفاده از تابع decode() ماژول ذخیرهسازی، آن را deserialize کنید. - ویژگیهای ذخیرهشدهی
Brushرا بازیابی کنید و قلممو را ایجاد کنید. با استفاده از قلممو بازسازیشده و
StrokeInputBatchاز حالت سریال خارجشده، stroke نهایی را ایجاد کنید.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 انجام میدهید. این ماتریس باید معکوس تبدیلی را که روی بوم stroke های نهایی خود اعمال میکنید، نشان دهد.
@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
)
}
}
سکته مغزی صادراتی
You might need to export your stroke scene as a static image file. This is useful for sharing the drawing with other applications, generating thumbnails, or saving a final, uneditable version of the content.
To export a scene, you can render your strokes to an offscreen bitmap instead of
directly to the screen. Use
Android's Picture API, which lets you record drawings on a canvas without
needing a visible UI component.
The process involves creating a Picture instance, calling beginRecording()
to get a Canvas, and then using your existing CanvasStrokeRenderer to draw
each stroke onto that Canvas. After you record all the drawing commands, you
can use the Picture to create a Bitmap,
which you can then compress and save to a 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)
}
کمککنندههای شیء داده و مبدل
یک ساختار شیء سریالسازی تعریف کنید که اشیاء API مورد نیاز Ink را منعکس کند.
از ماژول ذخیرهسازی 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,
)
}
}