Durum koruma ve kalıcı depolama, özellikle Compose'da mürekkep uygulamalarının önemli yönleridir. Fırça özellikleri ve bir vuruşu oluşturan noktalar gibi temel veri nesneleri karmaşıktır ve otomatik olarak kalıcı hale gelmez. Bu, yapılandırma değişiklikleri gibi senaryolarda durumu kaydetmek ve kullanıcının çizimlerini kalıcı olarak bir veritabanına kaydetmek için kasıtlı bir strateji gerektirir.
Durumu koruma
Jetpack Compose'da kullanıcı arayüzü durumu genellikle
remember
ve
rememberSaveable kullanılarak yönetilir.
rememberSaveable
yapılandırma değişiklikleri genelinde otomatik durum koruma özelliği sunsa da yerleşik özellikleri, temel veri türleri ve Parcelable ya da Serializable uygulayan nesnelerle sınırlıdır.
Brush gibi karmaşık özellikler içeren özel nesneler için özel bir durum kaydedici kullanarak açıkça tanımlanmış serileştirme ve seri durumdan çıkarma mekanizmaları tanımlamanız gerekir. Brush nesnesi için özel bir Saver tanımlayarak, yapılandırma değişiklikleri meydana geldiğinde fırçanın temel özelliklerini koruyabilirsiniz. Bu durum, aşağıdaki brushStateSaver örneğinde gösterilmiştir.
fun brushStateSaver(converters: Converters): Saver<MutableState<Brush>, SerializedBrush> = Saver(
save = { converters.serializeBrush(it.value) },
restore = { mutableStateOf(converters.deserializeBrush(it)) },
)
Ardından, seçilen fırça durumunu korumak için özel Saver simgesini kullanabilirsiniz:
val currentBrush = rememberSaveable(saver = brushStateSaver(Converters())) { mutableStateOf(defaultBrush) }
Kalıcı Depolama
Belge kaydetme, yükleme ve olası gerçek zamanlı ortak çalışma gibi özellikleri etkinleştirmek için vuruşları ve ilişkili verileri seri hale getirilmiş bir biçimde saklarız. Ink API için manuel serileştirme ve seri durumdan çıkarma gereklidir.
Bir vuruşu doğru şekilde geri yüklemek için Brush ve StrokeInputBatch değerlerini kaydedin.
Brush: Sayısal alanlar (boyut, epsilon), renk veBrushFamilyiçerir.StrokeInputBatch: Sayısal alanlar içeren giriş noktalarının listesi.
Depolama modülü, en karmaşık bölüm olan StrokeInputBatch'ı kompakt bir şekilde serileştirmeyi kolaylaştırır.
Bir vuruşu kaydetmek için:
- Depolama modülünün kodlama işlevini kullanarak
StrokeInputBatchöğesini serileştirin. Elde edilen ikili verileri saklayın. - Fırça darbesinin temel özelliklerini ayrı ayrı kaydedin:
- Fırça ailesini temsil eden enum. Örnek seri hale getirilebilse de bu, sınırlı sayıda fırça ailesi kullanan uygulamalar için verimli değildir.
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
)
}
Kontur nesnesi yüklemek için:
StrokeInputBatchiçin kaydedilen ikili verileri alın ve depolama modülünün decode() işlevini kullanarak seri durumdan çıkarın.- Kaydedilen
Brushözelliklerini alın ve fırçayı oluşturun. Yeniden oluşturulan fırçayı ve seri durumdan çıkarılan
StrokeInputBatchkullanarak son fırça darbesini oluşturun.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) }
Yakınlaştırma, kaydırma ve döndürme işlemlerini yönetme
Uygulamanızda yakınlaştırma, kaydırma veya döndürme destekleniyorsa mevcut dönüşümü InProgressStrokes iletmeniz gerekir. Bu sayede yeni çizilen konturlar, mevcut konturlarınızın konumuna ve ölçeğine uygun hale gelir.
Bunu, Matrix parametresine pointerEventToWorldTransform ileterek yapabilirsiniz. Matris, bitmiş vuruşlar tuvalinize uyguladığınız dönüşümün tersini temsil etmelidir.
@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
)
}
}
Vuruşları dışa aktarma
Fırça darbesi sahnenizi statik bir resim dosyası olarak dışa aktarmanız gerekebilir. Bu, çizimi diğer uygulamalarla paylaşmak, küçük resimler oluşturmak veya içeriğin son, düzenlenemeyen bir sürümünü kaydetmek için kullanışlıdır.
Bir sahneyi dışa aktarmak için vuruşlarınızı doğrudan ekrana değil, ekran dışı bir bit eşleme olarak oluşturabilirsiniz. Görünür bir kullanıcı arayüzü bileşenine ihtiyaç duymadan tuval üzerinde çizimler kaydetmenize olanak tanıyan Android's Picture API öğesini kullanın.
Bu işlemde Picture örneği oluşturulur, beginRecording() çağrılarak Canvas alınır ve ardından mevcut CanvasStrokeRenderer kullanılarak her bir vuruş bu Canvas üzerine çizilir. Tüm çizim komutlarını kaydettikten sonra Picture simgesini kullanarak Bitmap oluşturabilir, ardından bunu sıkıştırıp dosyaya kaydedebilirsiniz.
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)
}
Veri nesnesi ve dönüştürücü yardımcıları
Gerekli Ink API nesnelerini yansıtan bir serileştirme nesne yapısı tanımlayın.
StrokeInputBatch kodlamak ve kodunu çözmek için Ink API'nin depolama modülünü kullanın.
Veri aktarım nesneleri
@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,
}
Dönüşüm sağlayan kullanıcı sayısı
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,
)
}
}