Merge pull request #271 from Naveen3Singh/improve_flood_fill
Improve Flood-fill algorithm
This commit is contained in:
commit
91985a977d
|
@ -1,14 +1,15 @@
|
||||||
package com.simplemobiletools.draw.pro.extensions
|
package com.simplemobiletools.draw.pro.extensions
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
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 {
|
fun Bitmap.vectorFloodFill(color: Int, x: Int, y: Int, tolerance: Int): MyPath {
|
||||||
val floodFiller = QueueLinearFloodFiller(this).apply {
|
val floodFiller = VectorFloodFiller(this).apply {
|
||||||
fillColor = color
|
fillColor = color
|
||||||
setTolerance(tolerance)
|
this.tolerance = tolerance
|
||||||
}
|
}
|
||||||
|
|
||||||
floodFiller.floodFill(x, y)
|
floodFiller.floodFill(x, y)
|
||||||
return floodFiller.image!!
|
return floodFiller.path
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
package com.simplemobiletools.draw.pro.extensions
|
||||||
|
|
||||||
|
fun <K, V> LinkedHashMap<K, V>.removeFirst(): Pair<K, V> {
|
||||||
|
val key = keys.first()
|
||||||
|
val value = values.first()
|
||||||
|
remove(key)
|
||||||
|
return key to value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <K, V> LinkedHashMap<K, V>.removeLast(): Pair<K, V> {
|
||||||
|
val key = keys.last()
|
||||||
|
val value = values.last()
|
||||||
|
remove(key)
|
||||||
|
return key to value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <K, V> LinkedHashMap<K, V>.removeLastOrNull(): Pair<K?, V?> {
|
||||||
|
val key = keys.lastOrNull()
|
||||||
|
val value = values.lastOrNull()
|
||||||
|
remove(key)
|
||||||
|
return key to value
|
||||||
|
}
|
|
@ -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<FloodFillRange>? = 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)
|
|
||||||
}
|
|
|
@ -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<FloodFillRange>
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
|
@ -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()
|
|
||||||
}
|
|
|
@ -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<CanvasOp>()
|
|
||||||
|
|
||||||
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<MyParcelable> = object : Parcelable.Creator<MyParcelable> {
|
|
||||||
override fun createFromParcel(source: Parcel) = MyParcelable(source)
|
|
||||||
|
|
||||||
override fun newArray(size: Int) = arrayOf<MyParcelable>()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -73,7 +73,7 @@ object Svg {
|
||||||
path.readObject(it.data, activity)
|
path.readObject(it.data, activity)
|
||||||
val options = PaintOptions(it.color, it.strokeWidth, it.isEraser)
|
val options = PaintOptions(it.color, it.strokeWidth, it.isEraser)
|
||||||
|
|
||||||
canvas.addPath(path, options)
|
canvas.addOperation(path, options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,11 +16,8 @@ import com.bumptech.glide.request.RequestOptions
|
||||||
import com.simplemobiletools.commons.extensions.toast
|
import com.simplemobiletools.commons.extensions.toast
|
||||||
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
|
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
|
||||||
import com.simplemobiletools.draw.pro.R
|
import com.simplemobiletools.draw.pro.R
|
||||||
import com.simplemobiletools.draw.pro.extensions.contains
|
import com.simplemobiletools.draw.pro.extensions.*
|
||||||
import com.simplemobiletools.draw.pro.extensions.floodFill
|
|
||||||
import com.simplemobiletools.draw.pro.interfaces.CanvasListener
|
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.MyPath
|
||||||
import com.simplemobiletools.draw.pro.models.PaintOptions
|
import com.simplemobiletools.draw.pro.models.PaintOptions
|
||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
|
@ -31,17 +28,16 @@ import kotlin.math.min
|
||||||
class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
||||||
private val MIN_ERASER_WIDTH = 20f
|
private val MIN_ERASER_WIDTH = 20f
|
||||||
private val MAX_HISTORY_COUNT = 1000
|
private val MAX_HISTORY_COUNT = 1000
|
||||||
private val BITMAP_MAX_HISTORY_COUNT = 60
|
private val FLOOD_FILL_TOLERANCE = 10
|
||||||
private val DEFAULT_FLOOD_FILL_TOLERANCE = 190
|
|
||||||
|
|
||||||
private val mScaledTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
|
private val mScaledTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
|
||||||
|
|
||||||
private var mOperations = ArrayList<CanvasOp>()
|
private var mOperations = LinkedHashMap<MyPath, PaintOptions>()
|
||||||
var mBackgroundBitmap: Bitmap? = null
|
var mBackgroundBitmap: Bitmap? = null
|
||||||
var mListener: CanvasListener? = null
|
var mListener: CanvasListener? = null
|
||||||
|
|
||||||
private var mUndoneOperations = ArrayList<CanvasOp>()
|
private var mUndoneOperations = LinkedHashMap<MyPath, PaintOptions>()
|
||||||
private var mLastOperations = ArrayList<CanvasOp>()
|
private var mLastOperations = LinkedHashMap<MyPath, PaintOptions>()
|
||||||
private var mLastBackgroundBitmap: Bitmap? = null
|
private var mLastBackgroundBitmap: Bitmap? = null
|
||||||
|
|
||||||
private var mPaint = Paint()
|
private var mPaint = Paint()
|
||||||
|
@ -89,21 +85,17 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
||||||
updateUndoVisibility()
|
updateUndoVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
public override fun onSaveInstanceState(): Parcelable {
|
public override fun onSaveInstanceState(): Parcelable? {
|
||||||
val superState = super.onSaveInstanceState()
|
DrawingStateHolder.operations = mOperations
|
||||||
val savedState = MyParcelable(superState!!)
|
return super.onSaveInstanceState()
|
||||||
savedState.operations = mOperations
|
|
||||||
return savedState
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override fun onRestoreInstanceState(state: Parcelable) {
|
public override fun onRestoreInstanceState(state: Parcelable) {
|
||||||
if (state !is MyParcelable) {
|
val savedOperations = DrawingStateHolder.operations
|
||||||
super.onRestoreInstanceState(state)
|
if (savedOperations != null) {
|
||||||
return
|
mOperations = savedOperations
|
||||||
}
|
}
|
||||||
|
super.onRestoreInstanceState(state)
|
||||||
super.onRestoreInstanceState(state.superState)
|
|
||||||
mOperations = state.operations
|
|
||||||
updateUndoVisibility()
|
updateUndoVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,19 +214,9 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mOperations.isNotEmpty()) {
|
if (mOperations.isNotEmpty()) {
|
||||||
val bitmapOps = mOperations.filterIsInstance<CanvasOp.BitmapOp>()
|
for ((path, paintOptions) in mOperations) {
|
||||||
val bitmapOp = bitmapOps.lastOrNull()
|
changePaint(paintOptions)
|
||||||
if (bitmapOp != null) {
|
canvas.drawPath(path, mPaint)
|
||||||
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<CanvasOp.PathOp>()
|
|
||||||
for (pathOp in pathOps) {
|
|
||||||
changePaint(pathOp.paintOptions)
|
|
||||||
canvas.drawPath(pathOp.path, mPaint)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,7 +227,7 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
||||||
|
|
||||||
fun undo() {
|
fun undo() {
|
||||||
if (mOperations.isEmpty() && mLastOperations.isNotEmpty()) {
|
if (mOperations.isEmpty() && mLastOperations.isNotEmpty()) {
|
||||||
mOperations = mLastOperations.clone() as ArrayList<CanvasOp>
|
mOperations = mLastOperations.clone() as LinkedHashMap<MyPath, PaintOptions>
|
||||||
mBackgroundBitmap = mLastBackgroundBitmap
|
mBackgroundBitmap = mLastBackgroundBitmap
|
||||||
mLastOperations.clear()
|
mLastOperations.clear()
|
||||||
updateUndoVisibility()
|
updateUndoVisibility()
|
||||||
|
@ -254,8 +236,10 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mOperations.isNotEmpty()) {
|
if (mOperations.isNotEmpty()) {
|
||||||
val lastOp = mOperations.removeLast()
|
val (path, paintOptions) = mOperations.removeLastOrNull()
|
||||||
mUndoneOperations.add(lastOp)
|
if (paintOptions != null && path != null) {
|
||||||
|
mUndoneOperations[path] = paintOptions
|
||||||
|
}
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
updateUndoRedoVisibility()
|
updateUndoRedoVisibility()
|
||||||
|
@ -263,8 +247,8 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
||||||
|
|
||||||
fun redo() {
|
fun redo() {
|
||||||
if (mUndoneOperations.isNotEmpty()) {
|
if (mUndoneOperations.isNotEmpty()) {
|
||||||
val undoneOperation = mUndoneOperations.removeLast()
|
val (path, paintOptions) = mUndoneOperations.removeLast()
|
||||||
addOperation(undoneOperation)
|
addOperation(path, paintOptions)
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
updateUndoRedoVisibility()
|
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) {
|
private fun changePaint(paintOptions: PaintOptions) {
|
||||||
mPaint.color = if (paintOptions.isEraser) mBackgroundColor else paintOptions.color
|
mPaint.color = if (paintOptions.isEraser) mBackgroundColor else paintOptions.color
|
||||||
mPaint.strokeWidth = paintOptions.strokeWidth
|
mPaint.strokeWidth = paintOptions.strokeWidth
|
||||||
|
@ -342,7 +320,7 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearCanvas() {
|
fun clearCanvas() {
|
||||||
mLastOperations = mOperations.clone() as ArrayList<CanvasOp>
|
mLastOperations = mOperations.clone() as LinkedHashMap<MyPath, PaintOptions>
|
||||||
mLastBackgroundBitmap = mBackgroundBitmap
|
mLastBackgroundBitmap = mBackgroundBitmap
|
||||||
mBackgroundBitmap = null
|
mBackgroundBitmap = null
|
||||||
mPath.reset()
|
mPath.reset()
|
||||||
|
@ -397,8 +375,9 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
||||||
val color = mPaintOptions.color
|
val color = mPaintOptions.color
|
||||||
|
|
||||||
ensureBackgroundThread {
|
ensureBackgroundThread {
|
||||||
val img = bitmap.floodFill(color = color, x = touchedX, y = touchedY, tolerance = DEFAULT_FLOOD_FILL_TOLERANCE)
|
val path = bitmap.vectorFloodFill(color = color, x = touchedX, y = touchedY, tolerance = FLOOD_FILL_TOLERANCE)
|
||||||
addOperation(CanvasOp.BitmapOp(img))
|
val paintOpts = PaintOptions(color = color, strokeWidth = 5f)
|
||||||
|
addOperation(path, paintOpts)
|
||||||
post { invalidate() }
|
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 + 2)
|
||||||
mPath.lineTo(mCurX + 1, mCurY)
|
mPath.lineTo(mCurX + 1, mCurY)
|
||||||
}
|
}
|
||||||
addOperation(CanvasOp.PathOp(mPath, mPaintOptions))
|
addOperation(mPath, mPaintOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addOperation(operation: CanvasOp) {
|
fun addOperation(path: MyPath, paintOptions: PaintOptions) {
|
||||||
mOperations.add(operation)
|
mOperations[path] = paintOptions
|
||||||
|
|
||||||
// maybe free up some memory
|
// maybe free up some memory
|
||||||
while (mOperations.size > MAX_HISTORY_COUNT) {
|
while (mOperations.size > MAX_HISTORY_COUNT) {
|
||||||
val item = mOperations.removeFirst()
|
mOperations.removeFirst()
|
||||||
if (item is CanvasOp.BitmapOp) {
|
|
||||||
item.bitmap.recycle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val ops = mOperations.filterIsInstance<CanvasOp.BitmapOp>()
|
|
||||||
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<CanvasOp>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPathsMap(): Map<MyPath, PaintOptions> {
|
fun getPathsMap() = mOperations
|
||||||
val pathOps = mOperations
|
|
||||||
.filterIsInstance<CanvasOp.PathOp>()
|
|
||||||
.map { it.path to it.paintOptions }
|
|
||||||
.toTypedArray()
|
|
||||||
return mapOf(*pathOps)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDrawingHashCode(): Long {
|
fun getDrawingHashCode(): Long {
|
||||||
return if (mOperations.isEmpty()) {
|
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<MyPath, PaintOptions>? = null
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue