Adnan Aslam
Adnan Aslam

Reputation: 43

Is there a way to Serialize/Deserialize jetpack compose canvas path?

I have created a a canvas application. A user can draw on canvas. Now I want to save the progress of it's drawing in the Room Database. But I'm getting error for Serialization/Deserialization of androidx.compose.ui.graphics.Path.

Here is my code for Serialization/Deserialization of the entity:

@Entity(tableName = "canvas_projects")
data class CanvasProject(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    val name: String,
    val ratio: Float,
    @TypeConverters(BitmapConverter::class) val canvasBackground: Bitmap?,
    @TypeConverters(PathListConverter::class) val paths: List<PathWithProperties>
)

data class PathWithProperties(
    @TypeConverters(PathConverter::class) val path: Path,
    val pathProperties: PathProperties
)

object PathConverter {
    private val gson = Gson()
    @TypeConverter
    fun fromPath(path: Path) : String {
        return gson.toJson(path)
    }

    @TypeConverter
    fun toPath(data: String) : Path {
        return gson.fromJson(data, Path::class.java)
    }
}

data class PathProperties(
    val strokeWidth: Float,
    val color: String,
    val alphas: Float,
    val strokeCap: StrokeCap = StrokeCap.Round,
    val strokeJoin: StrokeJoin = StrokeJoin.Round,
    val eraseMode: Boolean,
    val isBitmap: Boolean,
    val bitmap: ImageBitmap? = null,
    val bitmapRect: Rect? = null
)

object BitmapConverter {
    @TypeConverter
    fun fromBitmap(bitmap: Bitmap?): ByteArray? {
        if (bitmap != null) {
            val outputStream = ByteArrayOutputStream()
            bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
            return outputStream.toByteArray()
        } else return null
    }

    @TypeConverter
    fun toBitmap(byteArray: ByteArray?): Bitmap? {
        return if (byteArray != null) BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) else null
    }
}

object PathListConverter {
    private val gson = Gson()

    @TypeConverter
    fun fromPathList(pathList: List<PathWithProperties>): String {
        return gson.toJson(pathList)
    }

    @TypeConverter
    fun toPathList(data: String): List<PathWithProperties> {
        val listType = object : TypeToken<List<PathWithProperties>>() {}.type
        return gson.fromJson(data, listType)
    }
}

But I'm getting the below error:

com.google.gson.JsonIOException: Interfaces can't be instantiated! Register an InstanceCreator or a TypeAdapter for this type. Interface name: androidx.compose.ui.graphics.Path

How can I solve this issue?

Upvotes: 2

Views: 112

Answers (1)

Thracian
Thracian

Reputation: 67238

You can't serialize androidx.compose.ui.graphics.Path but you can create a class, for instance SaveablePath, that contains or is a Path and define operations like moveTo(x, y), lineTo(x, y) in an Interface or UseCase

and serialize and deserialize them such as

PathDrawEntity(operationType, x, y, other serializable/parcelable properties)

and use this class to draw everything back when you read from database using path object

class SaveablePath(
    private val internalPath: Path,
    val saveUseCase: SaveUseCase,
):Path {

    override fun moveTo(x: Float, y: Float) {
        internalPath.moveTo(x, y)
         saveUseCase.moveTo(x,y)
    }

    // Do samething with others
    override fun relativeMoveTo(dx: Float, dy: Float) {
        internalPath.rMoveTo(dx, dy)
    }

    override fun lineTo(x: Float, y: Float) {
        internalPath.lineTo(x, y)
    }

    override fun relativeLineTo(dx: Float, dy: Float) {
        internalPath.rLineTo(dx, dy)
    }

    override fun quadraticBezierTo(x1: Float, y1: Float, x2: Float, y2: Float) {
        internalPath.quadTo(x1, y1, x2, y2)
    }

    override fun quadraticTo(x1: Float, y1: Float, x2: Float, y2: Float) {
        internalPath.quadTo(x1, y1, x2, y2)
    }

    override fun relativeQuadraticBezierTo(dx1: Float, dy1: Float, dx2: Float, dy2: Float) {
        internalPath.rQuadTo(dx1, dy1, dx2, dy2)
    }
}

and a function to read saved operations and apply them to Path when read to draw on Canvas.

Basically you convert Path operations to serializable classes and after reading them from db you draw using these classes by mapping to Path operations.

@Immutable data class RelativeMoveTo(val dx: Float, val dy: Float) : PathNode()

@Immutable data class MoveTo(val x: Float, val y: Float) : PathNode()

@Immutable data class RelativeLineTo(val dx: Float, val dy: Float) : PathNode()

@Immutable data class LineTo(val x: Float, val y: Float) : PathNode()

@Immutable data class RelativeHorizontalTo(val dx: Float) : PathNode()

@Immutable data class HorizontalTo(val x: Float) : PathNode()

@Immutable data class RelativeVerticalTo(val dy: Float) : PathNode()

@Immutable data class VerticalTo(val y: Float) : PathNode()

@Immutable
data class RelativeCurveTo(
    val dx1: Float,
    val dy1: Float,
    val dx2: Float,
    val dy2: Float,
    val dx3: Float,
    val dy3: Float
) : PathNode(isCurve = true)

@Immutable
data class CurveTo(
    val x1: Float,
    val y1: Float,
    val x2: Float,
    val y2: Float,
    val x3: Float,
    val y3: Float
) : PathNode(isCurve = true)

@Immutable
data class RelativeReflectiveCurveTo(
    val dx1: Float,
    val dy1: Float,
    val dx2: Float,
    val dy2: Float
) : PathNode(isCurve = true)

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathNode.kt;drc=dcaa116fbfda77e64a319e1668056ce3b032469f;l=50

Other option is to save Canvas with GraphicsLayer to convert to ImageBitmap then to Bitmap or Base64 string or ByteArray to save canvas in db or in a File.

enter image description here

@Preview
@Composable
fun GraphicsLayerToImageBitmapSample() {

    val coroutineScope = rememberCoroutineScope()
    val graphicsLayer = rememberGraphicsLayer()

    var imageBitmap by remember {
        mutableStateOf<ImageBitmap?>(null)
    }

    var touchPosition by remember {
        mutableStateOf(Offset.Unspecified)
    }

    val painter = painterResource(R.drawable.avatar_2_raster)

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {

        Canvas(
            modifier = Modifier
                .pointerInput(Unit) {
                    detectTapGestures { offset: Offset ->
                        touchPosition = offset
                    }
                }
                .drawWithContent {
                    drawContent()
                    graphicsLayer.record {
                        [email protected]()
                    }
                }

                .fillMaxWidth()
                .aspectRatio(1f),
        ) {


            with(painter) {
                draw(size)
            }

            if (touchPosition != Offset.Unspecified) {
                drawCircle(
                    color = Color.Blue,
                    radius = size.width * .1f,
                    center = touchPosition,
                    style = Stroke(
                        8.dp.toPx(), pathEffect = PathEffect.dashPathEffect(
                            floatArrayOf(20f, 20f)
                        )
                    )
                )
            }
        }

        Button(
            modifier = Modifier.fillMaxWidth(),
            onClick = {
                coroutineScope.launch {
                    imageBitmap = graphicsLayer.toImageBitmap()
                }
            }
        ) {
            Text("Convert graphicsLayer to ImageBitmap")
        }

        Text(text = "Screenshot of Composable", fontSize = 22.sp)
        imageBitmap?.let {
            Image(
                bitmap = it,
                modifier = Modifier
                    .fillMaxWidth(.7f)
                    .aspectRatio(1f),
                contentDescription = null
            )
        }
    }
}

Upvotes: 3

Related Questions