diff --git a/app/src/main/kotlin/com/simplemobiletools/draw/pro/extensions/Bitmap.kt b/app/src/main/kotlin/com/simplemobiletools/draw/pro/extensions/Bitmap.kt index 40bea58..4ee4d92 100644 --- a/app/src/main/kotlin/com/simplemobiletools/draw/pro/extensions/Bitmap.kt +++ b/app/src/main/kotlin/com/simplemobiletools/draw/pro/extensions/Bitmap.kt @@ -1,14 +1,15 @@ package com.simplemobiletools.draw.pro.extensions import android.graphics.Bitmap -import com.simplemobiletools.draw.pro.helpers.QueueLinearFloodFiller +import com.simplemobiletools.draw.pro.helpers.VectorFloodFiller +import com.simplemobiletools.draw.pro.models.MyPath -fun Bitmap.floodFill(color: Int, x: Int, y: Int, tolerance: Int = 10): Bitmap { - val floodFiller = QueueLinearFloodFiller(this).apply { +fun Bitmap.vectorFloodFill(color: Int, x: Int, y: Int, tolerance: Int): MyPath { + val floodFiller = VectorFloodFiller(this).apply { fillColor = color - setTolerance(tolerance) + this.tolerance = tolerance } floodFiller.floodFill(x, y) - return floodFiller.image!! + return floodFiller.path } diff --git a/app/src/main/kotlin/com/simplemobiletools/draw/pro/extensions/LinkedHashMap.kt b/app/src/main/kotlin/com/simplemobiletools/draw/pro/extensions/LinkedHashMap.kt new file mode 100644 index 0000000..4c7e21e --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/draw/pro/extensions/LinkedHashMap.kt @@ -0,0 +1,22 @@ +package com.simplemobiletools.draw.pro.extensions + +fun LinkedHashMap.removeFirst(): Pair { + val key = keys.first() + val value = values.first() + remove(key) + return key to value +} + +fun LinkedHashMap.removeLast(): Pair { + val key = keys.last() + val value = values.last() + remove(key) + return key to value +} + +fun LinkedHashMap.removeLastOrNull(): Pair { + val key = keys.lastOrNull() + val value = values.lastOrNull() + remove(key) + return key to value +} diff --git a/app/src/main/kotlin/com/simplemobiletools/draw/pro/helpers/FloodFill.kt b/app/src/main/kotlin/com/simplemobiletools/draw/pro/helpers/FloodFill.kt deleted file mode 100644 index c793a2b..0000000 --- a/app/src/main/kotlin/com/simplemobiletools/draw/pro/helpers/FloodFill.kt +++ /dev/null @@ -1,165 +0,0 @@ -package com.simplemobiletools.draw.pro.helpers - -import android.graphics.Bitmap -import android.graphics.Color -import java.util.* - -// Original algorithm by J. Dunlap http:// www.codeproject.com/KB/GDI-plus/queuelinearflood-fill.aspx -// Java port by Owen Kaluza -// Android port by Darrin Smith (Standard Android) -class QueueLinearFloodFiller(img: Bitmap) { - - var image: Bitmap? = null - private set - - var tolerance = intArrayOf(0, 0, 0) - private var width = 0 - private var height = 0 - private var pixels: IntArray? = null - var fillColor = 0 - private val startColor = intArrayOf(0, 0, 0) - private lateinit var pixelsChecked: BooleanArray - private var ranges: Queue? = null - - init { - copyImage(img) - } - - fun setTargetColor(targetColor: Int) { - startColor[0] = Color.red(targetColor) - startColor[1] = Color.green(targetColor) - startColor[2] = Color.blue(targetColor) - } - - fun setTolerance(value: Int) { - tolerance = intArrayOf(value, value, value) - } - - private fun copyImage(img: Bitmap) { - // Copy data from provided Image to a BufferedImage to write flood fill to, use getImage to retrieve - // cache data in member variables to decrease overhead of property calls - width = img.width - height = img.height - image = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) - image = img.copy(img.config, true) - pixels = IntArray(width * height) - image!!.getPixels(pixels, 0, width, 0, 0, width, height) - } - - private fun prepare() { - // Called before starting flood-fill - pixelsChecked = BooleanArray(pixels!!.size) - ranges = LinkedList() - } - - // Fills the specified point on the bitmap with the currently selected fill color. - // int x, int y: The starting coordinates for the fill - fun floodFill(x: Int, y: Int) { - // Setup - prepare() - if (startColor[0] == 0) { - // ***Get starting color. - val startPixel = pixels!!.getOrNull(width * y + x) ?: return - startColor[0] = startPixel shr 16 and 0xff - startColor[1] = startPixel shr 8 and 0xff - startColor[2] = startPixel and 0xff - } - - // ***Do first call to flood-fill. - linearFill(x, y) - - // ***Call flood-fill routine while flood-fill ranges still exist on the queue - var range: FloodFillRange - while (ranges!!.size > 0) { - // **Get Next Range Off the Queue - range = ranges!!.remove() - - // **Check Above and Below Each Pixel in the flood-fill Range - var downPxIdx = width * (range.Y + 1) + range.startX - var upPxIdx = width * (range.Y - 1) + range.startX - val upY = range.Y - 1 // so we can pass the y coordinate by ref - val downY = range.Y + 1 - for (i in range.startX..range.endX) { - // *Start Fill Upwards - // if we're not above the top of the bitmap and the pixel above this one is within the color tolerance - if (range.Y > 0 && !pixelsChecked[upPxIdx] && checkPixel(upPxIdx)) { - linearFill(i, upY) - } - - // *Start Fill Downwards - // if we're not below the bottom of the bitmap and the pixel below this one is within the color tolerance - if (range.Y < height - 1 && !pixelsChecked[downPxIdx] && checkPixel(downPxIdx)) { - linearFill(i, downY) - } - downPxIdx++ - upPxIdx++ - } - } - image!!.setPixels(pixels, 0, width, 0, 0, width, height) - } - - // Finds the furthermost left and right boundaries of the fill area - // on a given y coordinate, starting from a given x coordinate, filling as it goes. - // Adds the resulting horizontal range to the queue of flood-fill ranges, - // to be processed in the main loop. - // - // int x, int y: The starting coordinates - private fun linearFill(x: Int, y: Int) { - // ***Find Left Edge of Color Area - var lFillLoc = x // the location to check/fill on the left - var pxIdx = width * y + x - while (true) { - // **fill with the color - pixels!![pxIdx] = fillColor - - // **indicate that this pixel has already been checked and filled - pixelsChecked[pxIdx] = true - - // **de-increment - lFillLoc-- // de-increment counter - pxIdx-- // de-increment pixel index - - // **exit loop if we're at edge of bitmap or color area - if (lFillLoc < 0 || pixelsChecked[pxIdx] || !checkPixel(pxIdx)) { - break - } - } - lFillLoc++ - - // ***Find Right Edge of Color Area - var rFillLoc = x // the location to check/fill on the left - pxIdx = width * y + x - while (true) { - // **fill with the color - pixels!![pxIdx] = fillColor - - // **indicate that this pixel has already been checked and filled - pixelsChecked[pxIdx] = true - - // **increment - rFillLoc++ // increment counter - pxIdx++ // increment pixel index - - // **exit loop if we're at edge of bitmap or color area - if (rFillLoc >= width || pixelsChecked[pxIdx] || !checkPixel(pxIdx)) { - break - } - } - rFillLoc-- - - // add range to queue - val r = FloodFillRange(lFillLoc, rFillLoc, y) - ranges!!.offer(r) - } - - // Sees if a pixel is within the color tolerance range. - private fun checkPixel(px: Int): Boolean { - val red = pixels!![px] ushr 16 and 0xff - val green = pixels!![px] ushr 8 and 0xff - val blue = pixels!![px] and 0xff - return red >= startColor[0] - tolerance[0] && red <= startColor[0] + tolerance[0] && green >= startColor[1] - tolerance[1] && green <= startColor[1] + tolerance[1] && blue >= startColor[2] - tolerance[2] && blue <= startColor[2] + tolerance[2] - } - - // Represents a linear range to be filled and branched from. - private inner class FloodFillRange(var startX: Int, var endX: Int, var Y: Int) -} diff --git a/app/src/main/kotlin/com/simplemobiletools/draw/pro/helpers/VectorFloodFiller.kt b/app/src/main/kotlin/com/simplemobiletools/draw/pro/helpers/VectorFloodFiller.kt new file mode 100644 index 0000000..19033cc --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/draw/pro/helpers/VectorFloodFiller.kt @@ -0,0 +1,147 @@ +package com.simplemobiletools.draw.pro.helpers + +import android.graphics.Bitmap +import android.graphics.Color +import com.simplemobiletools.draw.pro.models.MyPath +import java.util.* + +// Original algorithm by J. Dunlap http:// www.codeproject.com/KB/GDI-plus/queuelinearflood-fill.aspx +// Java port by Owen Kaluza +// Android port by Darrin Smith (Standard Android) +class VectorFloodFiller(image: Bitmap) { + val path = MyPath() + + private var width = 0 + private var height = 0 + private var pixels: IntArray? = null + + private lateinit var pixelsChecked: BooleanArray + private lateinit var ranges: Queue + + var fillColor = 0 + var tolerance = 0 + private var startColorRed = 0 + private var startColorGreen = 0 + private var startColorBlue = 0 + + init { + width = image.width + height = image.height + pixels = IntArray(width * height) + image.getPixels(pixels, 0, width, 0, 0, width, height) + } + + private fun prepare() { + // Called before starting flood-fill + pixelsChecked = BooleanArray(pixels!!.size) + ranges = LinkedList() + } + + // Fills the specified point on the bitmap with the currently selected fill color. + // int x, int y: The starting coordinates for the fill + fun floodFill(x: Int, y: Int) { + // Setup + prepare() + + // Get starting color. + val startPixel = pixels!!.getOrNull(width * y + x) ?: return + if (startPixel == fillColor) { + // No-op. + return + } + startColorRed = Color.red(startPixel) + startColorGreen = Color.green(startPixel) + startColorBlue = Color.blue(startPixel) + + // Do first call to flood-fill. + linearFill(x, y) + + // Call flood-fill routine while flood-fill ranges still exist on the queue + var range: FloodFillRange + while (ranges.size > 0) { + // Get Next Range Off the Queue + range = ranges.remove() + + // Check Above and Below Each Pixel in the flood-fill Range + var downPxIdx = width * (range.Y + 1) + range.startX + var upPxIdx = width * (range.Y - 1) + range.startX + val upY = range.Y - 1 // so we can pass the y coordinate by ref + val downY = range.Y + 1 + for (i in range.startX..range.endX) { + // Start Fill Upwards + // if we're not above the top of the bitmap and the pixel above this one is within the color tolerance + if (range.Y > 0 && !pixelsChecked[upPxIdx] && isPixelColorWithinTolerance(upPxIdx)) { + linearFill(i, upY) + } + + // Start Fill Downwards + // if we're not below the bottom of the bitmap and the pixel below this one is within the color tolerance + if (range.Y < height - 1 && !pixelsChecked[downPxIdx] && isPixelColorWithinTolerance(downPxIdx)) { + linearFill(i, downY) + } + downPxIdx++ + upPxIdx++ + } + } + } + + // Finds the furthermost left and right boundaries of the fill area + // on a given y coordinate, starting from a given x coordinate, filling as it goes. + // Adds the resulting horizontal range to the queue of flood-fill ranges, + // to be processed in the main loop. + // + // int x, int y: The starting coordinates + private fun linearFill(x: Int, y: Int) { + // Find Left Edge of Color Area + var lFillLoc = x // the location to check/fill on the left + var pxIdx = width * y + x + path.moveTo(x.toFloat(), y.toFloat()) + while (true) { + pixelsChecked[pxIdx] = true + lFillLoc-- + pxIdx-- + // exit loop if we're at edge of bitmap or color area + if (lFillLoc < 0 || pixelsChecked[pxIdx] || !isPixelColorWithinTolerance(pxIdx)) { + break + } + } + vectorFill(pxIdx + 1) + lFillLoc++ + + // Find Right Edge of Color Area + var rFillLoc = x // the location to check/fill on the left + pxIdx = width * y + x + while (true) { + pixelsChecked[pxIdx] = true + rFillLoc++ + pxIdx++ + if (rFillLoc >= width || pixelsChecked[pxIdx] || !isPixelColorWithinTolerance(pxIdx)) { + break + } + } + vectorFill(pxIdx - 1) + rFillLoc-- + + // add range to queue + val r = FloodFillRange(lFillLoc, rFillLoc, y) + ranges.offer(r) + } + + // vector fill pixels with color + private fun vectorFill(pxIndex: Int) { + val x = (pxIndex % width).toFloat() + val y = (pxIndex - x) / width + path.lineTo(x, y) + } + + // Sees if a pixel is within the color tolerance range. + private fun isPixelColorWithinTolerance(px: Int): Boolean { + val red = pixels!![px] ushr 16 and 0xff + val green = pixels!![px] ushr 8 and 0xff + val blue = pixels!![px] and 0xff + return red >= startColorRed - tolerance && red <= startColorRed + tolerance && green >= startColorGreen - tolerance && green <= startColorGreen + tolerance && blue >= startColorBlue - tolerance && blue <= startColorBlue + tolerance + } + + // Represents a linear range to be filled and branched from. + private inner class FloodFillRange(var startX: Int, var endX: Int, var Y: Int) +} diff --git a/app/src/main/kotlin/com/simplemobiletools/draw/pro/models/CanvasOp.kt b/app/src/main/kotlin/com/simplemobiletools/draw/pro/models/CanvasOp.kt deleted file mode 100644 index 9b1d65f..0000000 --- a/app/src/main/kotlin/com/simplemobiletools/draw/pro/models/CanvasOp.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.simplemobiletools.draw.pro.models - -import android.graphics.Bitmap -import java.io.Serializable - -sealed class CanvasOp : Serializable { - class PathOp( - val path: MyPath, - val paintOptions: PaintOptions - ) : CanvasOp() - - class BitmapOp( - val bitmap: Bitmap - ) : CanvasOp() -} diff --git a/app/src/main/kotlin/com/simplemobiletools/draw/pro/models/MyParcelable.kt b/app/src/main/kotlin/com/simplemobiletools/draw/pro/models/MyParcelable.kt deleted file mode 100644 index 4f5e1b9..0000000 --- a/app/src/main/kotlin/com/simplemobiletools/draw/pro/models/MyParcelable.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.simplemobiletools.draw.pro.models - -import android.os.Parcel -import android.os.Parcelable -import android.view.View - -internal class MyParcelable : View.BaseSavedState { - var operations = ArrayList() - - constructor(superState: Parcelable) : super(superState) - - constructor(parcel: Parcel) : super(parcel) { - val size = parcel.readInt() - for (i in 0 until size) { - val serializable = parcel.readSerializable() - if (serializable is MyPath) { - val paintOptions = PaintOptions(parcel.readInt(), parcel.readFloat(), parcel.readInt() == 1) - val operation = CanvasOp.PathOp(serializable, paintOptions) - operations.add(operation) - } - } - } - - override fun writeToParcel(out: Parcel, flags: Int) { - super.writeToParcel(out, flags) - out.writeInt(operations.size) - for (operation in operations) { - if (operation is CanvasOp.PathOp) { - val path = operation.path - val paintOptions = operation.paintOptions - out.writeSerializable(path) - out.writeInt(paintOptions.color) - out.writeFloat(paintOptions.strokeWidth) - out.writeInt(if (paintOptions.isEraser) 1 else 0) - } - } - } - - companion object { - @JvmField - val CREATOR: Parcelable.Creator = object : Parcelable.Creator { - override fun createFromParcel(source: Parcel) = MyParcelable(source) - - override fun newArray(size: Int) = arrayOf() - } - } -} diff --git a/app/src/main/kotlin/com/simplemobiletools/draw/pro/models/Svg.kt b/app/src/main/kotlin/com/simplemobiletools/draw/pro/models/Svg.kt index 6ce9735..8843a9b 100644 --- a/app/src/main/kotlin/com/simplemobiletools/draw/pro/models/Svg.kt +++ b/app/src/main/kotlin/com/simplemobiletools/draw/pro/models/Svg.kt @@ -73,7 +73,7 @@ object Svg { path.readObject(it.data, activity) val options = PaintOptions(it.color, it.strokeWidth, it.isEraser) - canvas.addPath(path, options) + canvas.addOperation(path, options) } } diff --git a/app/src/main/kotlin/com/simplemobiletools/draw/pro/views/MyCanvas.kt b/app/src/main/kotlin/com/simplemobiletools/draw/pro/views/MyCanvas.kt index da527dd..21bcdad 100644 --- a/app/src/main/kotlin/com/simplemobiletools/draw/pro/views/MyCanvas.kt +++ b/app/src/main/kotlin/com/simplemobiletools/draw/pro/views/MyCanvas.kt @@ -16,11 +16,8 @@ import com.bumptech.glide.request.RequestOptions import com.simplemobiletools.commons.extensions.toast import com.simplemobiletools.commons.helpers.ensureBackgroundThread import com.simplemobiletools.draw.pro.R -import com.simplemobiletools.draw.pro.extensions.contains -import com.simplemobiletools.draw.pro.extensions.floodFill +import com.simplemobiletools.draw.pro.extensions.* import com.simplemobiletools.draw.pro.interfaces.CanvasListener -import com.simplemobiletools.draw.pro.models.CanvasOp -import com.simplemobiletools.draw.pro.models.MyParcelable import com.simplemobiletools.draw.pro.models.MyPath import com.simplemobiletools.draw.pro.models.PaintOptions import java.util.concurrent.ExecutionException @@ -31,17 +28,16 @@ import kotlin.math.min class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) { private val MIN_ERASER_WIDTH = 20f private val MAX_HISTORY_COUNT = 1000 - private val BITMAP_MAX_HISTORY_COUNT = 60 - private val DEFAULT_FLOOD_FILL_TOLERANCE = 190 + private val FLOOD_FILL_TOLERANCE = 10 private val mScaledTouchSlop = ViewConfiguration.get(context).scaledTouchSlop - private var mOperations = ArrayList() + private var mOperations = LinkedHashMap() var mBackgroundBitmap: Bitmap? = null var mListener: CanvasListener? = null - private var mUndoneOperations = ArrayList() - private var mLastOperations = ArrayList() + private var mUndoneOperations = LinkedHashMap() + private var mLastOperations = LinkedHashMap() private var mLastBackgroundBitmap: Bitmap? = null private var mPaint = Paint() @@ -89,21 +85,17 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) { updateUndoVisibility() } - public override fun onSaveInstanceState(): Parcelable { - val superState = super.onSaveInstanceState() - val savedState = MyParcelable(superState!!) - savedState.operations = mOperations - return savedState + public override fun onSaveInstanceState(): Parcelable? { + DrawingStateHolder.operations = mOperations + return super.onSaveInstanceState() } public override fun onRestoreInstanceState(state: Parcelable) { - if (state !is MyParcelable) { - super.onRestoreInstanceState(state) - return + val savedOperations = DrawingStateHolder.operations + if (savedOperations != null) { + mOperations = savedOperations } - - super.onRestoreInstanceState(state.superState) - mOperations = state.operations + super.onRestoreInstanceState(state) updateUndoVisibility() } @@ -222,19 +214,9 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) { } if (mOperations.isNotEmpty()) { - val bitmapOps = mOperations.filterIsInstance() - val bitmapOp = bitmapOps.lastOrNull() - if (bitmapOp != null) { - canvas.drawBitmap(bitmapOp.bitmap, 0f, 0f, null) - } - - // only perform path ops after last bitmap op as any previous path operations are already visible due to the bitmap op - val startIndex = if (bitmapOp != null) mOperations.indexOf(bitmapOp) else 0 - val endIndex = mOperations.lastIndex - val pathOps = mOperations.slice(startIndex..endIndex).filterIsInstance() - for (pathOp in pathOps) { - changePaint(pathOp.paintOptions) - canvas.drawPath(pathOp.path, mPaint) + for ((path, paintOptions) in mOperations) { + changePaint(paintOptions) + canvas.drawPath(path, mPaint) } } @@ -245,7 +227,7 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) { fun undo() { if (mOperations.isEmpty() && mLastOperations.isNotEmpty()) { - mOperations = mLastOperations.clone() as ArrayList + mOperations = mLastOperations.clone() as LinkedHashMap mBackgroundBitmap = mLastBackgroundBitmap mLastOperations.clear() updateUndoVisibility() @@ -254,8 +236,10 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) { } if (mOperations.isNotEmpty()) { - val lastOp = mOperations.removeLast() - mUndoneOperations.add(lastOp) + val (path, paintOptions) = mOperations.removeLastOrNull() + if (paintOptions != null && path != null) { + mUndoneOperations[path] = paintOptions + } invalidate() } updateUndoRedoVisibility() @@ -263,8 +247,8 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) { fun redo() { if (mUndoneOperations.isNotEmpty()) { - val undoneOperation = mUndoneOperations.removeLast() - addOperation(undoneOperation) + val (path, paintOptions) = mUndoneOperations.removeLast() + addOperation(path, paintOptions) invalidate() } updateUndoRedoVisibility() @@ -327,12 +311,6 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) { } } - fun addPath(path: MyPath, options: PaintOptions) { - val pathOp = CanvasOp.PathOp(path, options) - mOperations.add(pathOp) - updateUndoVisibility() - } - private fun changePaint(paintOptions: PaintOptions) { mPaint.color = if (paintOptions.isEraser) mBackgroundColor else paintOptions.color mPaint.strokeWidth = paintOptions.strokeWidth @@ -342,7 +320,7 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) { } fun clearCanvas() { - mLastOperations = mOperations.clone() as ArrayList + mLastOperations = mOperations.clone() as LinkedHashMap mLastBackgroundBitmap = mBackgroundBitmap mBackgroundBitmap = null mPath.reset() @@ -397,8 +375,9 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) { val color = mPaintOptions.color ensureBackgroundThread { - val img = bitmap.floodFill(color = color, x = touchedX, y = touchedY, tolerance = DEFAULT_FLOOD_FILL_TOLERANCE) - addOperation(CanvasOp.BitmapOp(img)) + val path = bitmap.vectorFloodFill(color = color, x = touchedX, y = touchedY, tolerance = FLOOD_FILL_TOLERANCE) + val paintOpts = PaintOptions(color = color, strokeWidth = 5f) + addOperation(path, paintOpts) post { invalidate() } } } @@ -413,37 +392,19 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) { mPath.lineTo(mCurX + 1, mCurY + 2) mPath.lineTo(mCurX + 1, mCurY) } - addOperation(CanvasOp.PathOp(mPath, mPaintOptions)) + addOperation(mPath, mPaintOptions) } - private fun addOperation(operation: CanvasOp) { - mOperations.add(operation) + fun addOperation(path: MyPath, paintOptions: PaintOptions) { + mOperations[path] = paintOptions // maybe free up some memory while (mOperations.size > MAX_HISTORY_COUNT) { - val item = mOperations.removeFirst() - if (item is CanvasOp.BitmapOp) { - item.bitmap.recycle() - } - } - - val ops = mOperations.filterIsInstance() - if (ops.size > BITMAP_MAX_HISTORY_COUNT) { - val start = ops.lastIndex - BITMAP_MAX_HISTORY_COUNT - val bitmapOp = ops.slice(start..ops.lastIndex).first() - - val startIndex = mOperations.indexOf(bitmapOp) - mOperations = mOperations.slice(startIndex..mOperations.lastIndex) as ArrayList + mOperations.removeFirst() } } - fun getPathsMap(): Map { - val pathOps = mOperations - .filterIsInstance() - .map { it.path to it.paintOptions } - .toTypedArray() - return mapOf(*pathOps) - } + fun getPathsMap() = mOperations fun getDrawingHashCode(): Long { return if (mOperations.isEmpty()) { @@ -484,3 +445,8 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) { } } } + +// since we don't use view models, this serves as a simple state holder to save drawing operations +object DrawingStateHolder { + var operations: LinkedHashMap? = null +}