يُعدّ الحفاظ على الحالة والتخزين الدائم من الجوانب المهمة في تطبيقات الكتابة بالحبر الرقمي، خاصةً في Compose. إنّ عناصر البيانات الأساسية، مثل خصائص الفرشاة والنقاط التي تشكّل ضربة فرشاة، معقّدة ولا يتم الاحتفاظ بها تلقائيًا. ويتطلّب ذلك استراتيجية مدروسة لحفظ الحالة أثناء سيناريوهات مثل تغييرات الإعدادات وحفظ رسومات المستخدم بشكل دائم في قاعدة بيانات.
State preservation
In Jetpack Compose, UI state is typically managed using
remember
and
rememberSaveable.
While
rememberSaveable
offers automatic state preservation across configuration changes, its built-in
capabilities are limited to primitive data types and objects that implement
Parcelable or
Serializable.
For custom objects that contain complex properties, such as
Brush, you must define explicit
serialization and deserialization mechanisms using a custom state saver. By
defining a custom Saver
for the Brush object, you can preserve the brush's essential attributes when
configuration changes occur, as shown in the following brushStateSaver
example.
fun brushStateSaver(converters: Converters): Saver<MutableState<Brush>, SerializedBrush> = Saver(
save = { converters.serializeBrush(it.value) },
restore = { mutableStateOf(converters.deserializeBrush(it)) },
)
You can then use the custom Saver to
preserve the selected brush state:
val currentBrush = rememberSaveable(saver = brushStateSaver(Converters())) { mutableStateOf(defaultBrush) }
التخزين الثابت
لتفعيل ميزات مثل حفظ المستندات وتحميلها وإمكانية التعاون في الوقت الفعلي، يجب تخزين ضربات القلم والبيانات المرتبطة بها بتنسيق تسلسلي. بالنسبة إلى Ink API، يجب إجراء التسلسل والتسلسل العكسي يدويًا.
لاستعادة ضربة بدقة، احفظ Brush وStrokeInputBatch.
Brush: تتضمّن هذه السمة حقولاً رقمية (المقاس، إبسيلون)، واللون، وBrushFamily.StrokeInputBatch: قائمة بنقاط الإدخال التي تحتوي على حقول رقمية.
تسهّل وحدة Storage عملية التسلسل المضغوط للجزء الأكثر تعقيدًا، وهو StrokeInputBatch.
لحفظ ضربة:
- يمكنك تسلسل
StrokeInputBatchباستخدام دالة الترميز في وحدة التخزين. تخزين البيانات الثنائية الناتجة - احفظ بشكل منفصل الخصائص الأساسية لـ "فرشاة" الضربة:
- تعداد يمثّل مجموعة الفرش &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,
)
}
}