상태 보존 및 영구 스토리지는 잉크 앱의 중요한 측면입니다. 이를 위해서는 구성 변경과 같은 시나리오에서 상태를 저장하고 사용자의 그림을 데이터베이스에 영구적으로 저장하기 위한 신중한 전략이 필요합니다.
상태 보존
뷰 기반 앱에서 UI 상태는 다음의 조합을 사용하여 관리됩니다.
ViewModel객체- 다음을 사용하여 저장된 인스턴스 상태:
- 활동
onSaveInstanceState() - ViewModel SavedStateHandle
- 앱 및 활동 전환 중에 UI 상태를 유지하기 위한 로컬 저장소
- 활동
UI 상태 저장을 참고하세요.
영구 스토리지
문서 저장, 로드, 잠재적인 실시간 공동작업과 같은 기능을 사용 설정하려면 획과 관련 데이터를 직렬화된 형식으로 저장하세요. Ink API의 경우 수동 직렬화 및 역직렬화가 필요합니다.
획을 정확하게 복원하려면 Brush 및 StrokeInputBatch을 저장하세요.
Brush: 숫자 필드 (크기, 입실론), 색상,BrushFamily을 포함합니다.StrokeInputBatch: 숫자 필드가 있는 입력 포인트 목록입니다.
저장소 모듈은 가장 복잡한 부분인 StrokeInputBatch을 간결하게 직렬화합니다.
획을 저장하려면 다음 단계를 따르세요.
- 스토리지 모듈의 인코딩 함수를 사용하여
StrokeInputBatch를 직렬화합니다. 결과 바이너리 데이터를 저장합니다. - 스트로크의 브러시 필수 속성을 별도로 저장합니다.
- 브러시 패밀리를 나타내는 열거형입니다. 인스턴스를 직렬화할 수 있지만 제한된 브러시 패밀리 선택을 사용하는 앱에는 효율적이지 않습니다.
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
)
}
}
획 내보내기
획 장면을 정적 이미지 파일로 내보내야 할 수도 있습니다. 이는 그림을 다른 애플리케이션과 공유하거나, 썸네일을 생성하거나, 수정할 수 없는 최종 버전의 콘텐츠를 저장하는 데 유용합니다.
장면을 내보내려면 스트로크를 화면에 직접 렌더링하는 대신 오프스크린 비트맵으로 렌더링하면 됩니다. 표시되는 UI 구성요소 없이 캔버스에 그림을 기록할 수 있는 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,
)
}
}