Add color filler tool
- Simplified the undo/redo code using `Operation` abstraction - Color fill changes is not saved with SVGs. More research/development is needed on this. - Bitmap operations are not saved during `onSaveInstanceState` because of the obvious memory/space issue. Perhaps using `Path` objects in the flood-fill algorithm can remedy this and the above issue
This commit is contained in:
parent
797cfabcb9
commit
2c4dccdda9
|
@ -63,6 +63,7 @@ class MainActivity : SimpleActivity(), CanvasListener {
|
|||
private var lastSavePromptTS = 0L
|
||||
private var isEraserOn = false
|
||||
private var isEyeDropperOn = false
|
||||
private var isColorFillOn = false
|
||||
private var isImageCaptureIntent = false
|
||||
private var isEditIntent = false
|
||||
private var lastBitmapPath = ""
|
||||
|
@ -105,6 +106,7 @@ class MainActivity : SimpleActivity(), CanvasListener {
|
|||
true
|
||||
}
|
||||
|
||||
color_fill.setOnClickListener { colorFillClicked() }
|
||||
checkIntents()
|
||||
if (!isImageCaptureIntent) {
|
||||
checkWhatsNewDialog()
|
||||
|
@ -393,6 +395,20 @@ class MainActivity : SimpleActivity(), CanvasListener {
|
|||
eye_dropper.setImageResource(iconId)
|
||||
}
|
||||
|
||||
private fun colorFillClicked() {
|
||||
isColorFillOn = !isColorFillOn
|
||||
|
||||
val iconId = if (isColorFillOn) {
|
||||
R.drawable.ic_color_fill_off_vector
|
||||
} else {
|
||||
R.drawable.ic_color_fill_on_vector
|
||||
}
|
||||
|
||||
color_fill.setImageResource(iconId)
|
||||
|
||||
my_canvas.toggleColorFill(isColorFillOn)
|
||||
}
|
||||
|
||||
private fun confirmImage() {
|
||||
when {
|
||||
isEditIntent -> {
|
||||
|
@ -575,6 +591,7 @@ class MainActivity : SimpleActivity(), CanvasListener {
|
|||
eraser.applyColorFilter(contrastColor)
|
||||
redo.applyColorFilter(contrastColor)
|
||||
eye_dropper.applyColorFilter(contrastColor)
|
||||
color_fill.applyColorFilter(contrastColor)
|
||||
if (isBlackAndWhiteTheme()) {
|
||||
stroke_width_bar.setColors(0, contrastColor, 0)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package com.simplemobiletools.draw.pro.extensions
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import com.simplemobiletools.draw.pro.helpers.QueueLinearFloodFiller
|
||||
|
||||
|
||||
fun Bitmap.floodFill(color: Int, x: Int, y: Int, tolerance: Int = 10): Bitmap {
|
||||
val floodFiller = QueueLinearFloodFiller(this).apply {
|
||||
fillColor = color
|
||||
setTolerance(tolerance)
|
||||
}
|
||||
|
||||
floodFiller.floodFill(x, y)
|
||||
return floodFiller.image!!
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package com.simplemobiletools.draw.pro.extensions
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
|
||||
|
||||
val View.boundingBox: Rect
|
||||
get() {
|
||||
val rect = Rect()
|
||||
getDrawingRect(rect)
|
||||
val location = IntArray(2)
|
||||
getLocationOnScreen(location)
|
||||
rect.offset(location[0], location[1])
|
||||
return rect
|
||||
}
|
||||
|
||||
fun View.contains(x: Int, y: Int) = boundingBox.contains(x, y)
|
|
@ -0,0 +1,167 @@
|
|||
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!![width * y + x]
|
||||
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,16 @@
|
|||
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()
|
||||
}
|
|
@ -3,30 +3,37 @@ package com.simplemobiletools.draw.pro.models
|
|||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.view.View
|
||||
import java.util.*
|
||||
|
||||
|
||||
internal class MyParcelable : View.BaseSavedState {
|
||||
var paths = LinkedHashMap<MyPath, PaintOptions>()
|
||||
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 key = parcel.readSerializable() as MyPath
|
||||
val paintOptions = PaintOptions(parcel.readInt(), parcel.readFloat(), parcel.readInt() == 1)
|
||||
paths[key] = paintOptions
|
||||
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(paths.size)
|
||||
for ((path, paintOptions) in paths) {
|
||||
out.writeSerializable(path)
|
||||
out.writeInt(paintOptions.color)
|
||||
out.writeFloat(paintOptions.strokeWidth)
|
||||
out.writeInt(if (paintOptions.isEraser) 1 else 0)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ import com.simplemobiletools.draw.pro.activities.MainActivity
|
|||
import com.simplemobiletools.draw.pro.activities.SimpleActivity
|
||||
import com.simplemobiletools.draw.pro.views.MyCanvas
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
|
||||
object Svg {
|
||||
fun saveSvg(activity: SimpleActivity, path: String, canvas: MyCanvas) {
|
||||
|
@ -27,7 +26,7 @@ object Svg {
|
|||
if (outputStream != null) {
|
||||
val backgroundColor = (canvas.background as ColorDrawable).color
|
||||
val writer = BufferedWriter(OutputStreamWriter(outputStream))
|
||||
writeSvg(writer, backgroundColor, canvas.mPaths, canvas.width, canvas.height)
|
||||
writeSvg(writer, backgroundColor, canvas.getPathsMap(), canvas.width, canvas.height)
|
||||
writer.close()
|
||||
activity.toast(R.string.file_saved)
|
||||
} else {
|
||||
|
|
|
@ -16,7 +16,10 @@ 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.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
|
||||
|
@ -27,14 +30,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 mScaledTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
|
||||
|
||||
var mPaths = LinkedHashMap<MyPath, PaintOptions>()
|
||||
private var mOperations = ArrayList<CanvasOp>()
|
||||
var mBackgroundBitmap: Bitmap? = null
|
||||
var mListener: CanvasListener? = null
|
||||
|
||||
private var mLastPaths = LinkedHashMap<MyPath, PaintOptions>()
|
||||
private var mUndonePaths = LinkedHashMap<MyPath, PaintOptions>()
|
||||
private var mUndoneOperations = ArrayList<CanvasOp>()
|
||||
private var mLastOperations = ArrayList<CanvasOp>()
|
||||
private var mLastBackgroundBitmap: Bitmap? = null
|
||||
|
||||
private var mPaint = Paint()
|
||||
|
@ -54,6 +59,7 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
|||
private var mCurrBrushSize = 0f
|
||||
private var mAllowMovingZooming = true
|
||||
private var mIsEraserOn = false
|
||||
private var mIsColorFillOn = false
|
||||
private var mWasMultitouch = false
|
||||
private var mIgnoreTouches = false
|
||||
private var mWasScalingInGesture = false
|
||||
|
@ -78,13 +84,13 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
|||
}
|
||||
|
||||
mScaleDetector = ScaleGestureDetector(context, ScaleListener())
|
||||
pathsUpdated()
|
||||
updateUndoVisibility()
|
||||
}
|
||||
|
||||
public override fun onSaveInstanceState(): Parcelable {
|
||||
val superState = super.onSaveInstanceState()
|
||||
val savedState = MyParcelable(superState!!)
|
||||
savedState.paths = mPaths
|
||||
savedState.operations = mOperations
|
||||
return savedState
|
||||
}
|
||||
|
||||
|
@ -95,8 +101,8 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
|||
}
|
||||
|
||||
super.onRestoreInstanceState(state.superState)
|
||||
mPaths = state.paths
|
||||
pathsUpdated()
|
||||
mOperations = state.operations
|
||||
updateUndoVisibility()
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
|
@ -147,8 +153,8 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
|||
mLastTouchX = x
|
||||
mLastTouchY = y
|
||||
actionDown(newValueX, newValueY)
|
||||
mUndonePaths.clear()
|
||||
mListener?.toggleRedoVisibility(false)
|
||||
mUndoneOperations.clear()
|
||||
updateRedoVisibility(false)
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (mTouchSloppedBeforeMultitouch) {
|
||||
|
@ -156,7 +162,7 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
|||
mTouchSloppedBeforeMultitouch = false
|
||||
}
|
||||
|
||||
if (!mAllowMovingZooming || (!mScaleDetector!!.isInProgress && event.pointerCount == 1 && !mWasMultitouch)) {
|
||||
if (!mIsColorFillOn && (!mAllowMovingZooming || (!mScaleDetector!!.isInProgress && event.pointerCount == 1 && !mWasMultitouch))) {
|
||||
actionMove(newValueX, newValueY)
|
||||
}
|
||||
|
||||
|
@ -207,14 +213,27 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
|||
canvas.scale(mScaleFactor, mScaleFactor, mCenter!!.x, mCenter!!.y)
|
||||
|
||||
if (mBackgroundBitmap != null) {
|
||||
val left = (width - mBackgroundBitmap!!.width) / 2
|
||||
val top = (height - mBackgroundBitmap!!.height) / 2
|
||||
canvas.drawBitmap(mBackgroundBitmap!!, left.toFloat(), top.toFloat(), null)
|
||||
val bitmap = mBackgroundBitmap!!
|
||||
val left = (width - bitmap.width) / 2f
|
||||
val top = (height - bitmap.height) / 2f
|
||||
canvas.drawBitmap(bitmap, left, top, null)
|
||||
}
|
||||
|
||||
for ((key, value) in mPaths) {
|
||||
changePaint(value)
|
||||
canvas.drawPath(key, mPaint)
|
||||
if (mOperations.isNotEmpty()) {
|
||||
val bitmapOps = mOperations.filterIsInstance<CanvasOp.BitmapOp>()
|
||||
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<CanvasOp.PathOp>()
|
||||
for (pathOp in pathOps) {
|
||||
changePaint(pathOp.paintOptions)
|
||||
canvas.drawPath(pathOp.path, mPaint)
|
||||
}
|
||||
}
|
||||
|
||||
changePaint(mPaintOptions)
|
||||
|
@ -223,44 +242,30 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
|||
}
|
||||
|
||||
fun undo() {
|
||||
if (mPaths.isEmpty() && mLastPaths.isNotEmpty()) {
|
||||
mPaths = mLastPaths.clone() as LinkedHashMap<MyPath, PaintOptions>
|
||||
if (mOperations.isEmpty() && mLastOperations.isNotEmpty()) {
|
||||
mOperations = mLastOperations.clone() as ArrayList<CanvasOp>
|
||||
mBackgroundBitmap = mLastBackgroundBitmap
|
||||
mLastPaths.clear()
|
||||
pathsUpdated()
|
||||
mLastOperations.clear()
|
||||
updateUndoVisibility()
|
||||
invalidate()
|
||||
return
|
||||
}
|
||||
|
||||
if (mPaths.isEmpty()) {
|
||||
return
|
||||
if (mOperations.isNotEmpty()) {
|
||||
val lastOp = mOperations.removeLast()
|
||||
mUndoneOperations.add(lastOp)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
val lastPath = mPaths.values.lastOrNull()
|
||||
val lastKey = mPaths.keys.lastOrNull()
|
||||
|
||||
mPaths.remove(lastKey)
|
||||
if (lastPath != null && lastKey != null) {
|
||||
mUndonePaths[lastKey] = lastPath
|
||||
mListener?.toggleRedoVisibility(true)
|
||||
}
|
||||
pathsUpdated()
|
||||
invalidate()
|
||||
updateUndoRedoVisibility()
|
||||
}
|
||||
|
||||
fun redo() {
|
||||
if (mUndonePaths.keys.isEmpty()) {
|
||||
mListener?.toggleRedoVisibility(false)
|
||||
return
|
||||
if (mUndoneOperations.isNotEmpty()) {
|
||||
val undoneOperation = mUndoneOperations.removeLast()
|
||||
addOperation(undoneOperation)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
val lastKey = mUndonePaths.keys.last()
|
||||
addPath(lastKey, mUndonePaths.values.last())
|
||||
mUndonePaths.remove(lastKey)
|
||||
if (mUndonePaths.isEmpty()) {
|
||||
mListener?.toggleRedoVisibility(false)
|
||||
}
|
||||
invalidate()
|
||||
updateUndoRedoVisibility()
|
||||
}
|
||||
|
||||
fun toggleEraser(isEraserOn: Boolean) {
|
||||
|
@ -269,6 +274,10 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
|||
invalidate()
|
||||
}
|
||||
|
||||
fun toggleColorFill(isColorFillOn: Boolean) {
|
||||
mIsColorFillOn = isColorFillOn
|
||||
}
|
||||
|
||||
fun setColor(newColor: Int) {
|
||||
mPaintOptions.color = newColor
|
||||
}
|
||||
|
@ -300,17 +309,10 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
|||
ensureBackgroundThread {
|
||||
val size = Point()
|
||||
activity.windowManager.defaultDisplay.getSize(size)
|
||||
val options = RequestOptions()
|
||||
.format(DecodeFormat.PREFER_ARGB_8888)
|
||||
.disallowHardwareConfig()
|
||||
.fitCenter()
|
||||
val options = RequestOptions().format(DecodeFormat.PREFER_ARGB_8888).disallowHardwareConfig().fitCenter()
|
||||
|
||||
try {
|
||||
val builder = Glide.with(context)
|
||||
.asBitmap()
|
||||
.load(path)
|
||||
.apply(options)
|
||||
.submit(size.x, size.y)
|
||||
val builder = Glide.with(context).asBitmap().load(path).apply(options).submit(size.x, size.y)
|
||||
|
||||
mBackgroundBitmap = builder.get()
|
||||
activity.runOnUiThread {
|
||||
|
@ -324,8 +326,9 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
|||
}
|
||||
|
||||
fun addPath(path: MyPath, options: PaintOptions) {
|
||||
mPaths[path] = options
|
||||
pathsUpdated()
|
||||
val pathOp = CanvasOp.PathOp(path, options)
|
||||
mOperations.add(pathOp)
|
||||
updateUndoVisibility()
|
||||
}
|
||||
|
||||
private fun changePaint(paintOptions: PaintOptions) {
|
||||
|
@ -337,12 +340,12 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
|||
}
|
||||
|
||||
fun clearCanvas() {
|
||||
mLastPaths = mPaths.clone() as LinkedHashMap<MyPath, PaintOptions>
|
||||
mLastOperations = mOperations.clone() as ArrayList<CanvasOp>
|
||||
mLastBackgroundBitmap = mBackgroundBitmap
|
||||
mBackgroundBitmap = null
|
||||
mPath.reset()
|
||||
mPaths.clear()
|
||||
pathsUpdated()
|
||||
mOperations.clear()
|
||||
updateUndoVisibility()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
|
@ -360,28 +363,84 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
|||
}
|
||||
|
||||
private fun actionUp(forceLineDraw: Boolean) {
|
||||
if (!mWasMultitouch || forceLineDraw) {
|
||||
mPath.lineTo(mCurX, mCurY)
|
||||
|
||||
// draw a dot on click
|
||||
if (mStartX == mCurX && mStartY == mCurY) {
|
||||
mPath.lineTo(mCurX, mCurY + 2)
|
||||
mPath.lineTo(mCurX + 1, mCurY + 2)
|
||||
mPath.lineTo(mCurX + 1, mCurY)
|
||||
}
|
||||
mPaths[mPath] = mPaintOptions
|
||||
if (mIsColorFillOn) {
|
||||
colorFill()
|
||||
} else if (!mWasMultitouch || forceLineDraw) {
|
||||
drawADot()
|
||||
}
|
||||
|
||||
pathsUpdated()
|
||||
updateUndoVisibility()
|
||||
mPath = MyPath()
|
||||
mPaintOptions = PaintOptions(mPaintOptions.color, mPaintOptions.strokeWidth, mPaintOptions.isEraser)
|
||||
}
|
||||
|
||||
private fun pathsUpdated() {
|
||||
mListener?.toggleUndoVisibility(mPaths.isNotEmpty() || mLastPaths.isNotEmpty())
|
||||
private fun updateUndoRedoVisibility() {
|
||||
updateUndoVisibility()
|
||||
updateRedoVisibility()
|
||||
}
|
||||
|
||||
fun getDrawingHashCode() = mPaths.hashCode().toLong() + (mBackgroundBitmap?.hashCode()?.toLong() ?: 0L)
|
||||
private fun updateUndoVisibility() {
|
||||
mListener?.toggleUndoVisibility(mOperations.isNotEmpty() || mLastOperations.isNotEmpty())
|
||||
}
|
||||
|
||||
private fun updateRedoVisibility(visible: Boolean = mUndoneOperations.isNotEmpty()) {
|
||||
mListener?.toggleRedoVisibility(visible)
|
||||
}
|
||||
|
||||
private fun colorFill() {
|
||||
val touchedX = mCurX.toInt()
|
||||
val touchedY = mCurY.toInt()
|
||||
if (contains(touchedX, touchedY)) {
|
||||
val bitmap = getBitmap()
|
||||
val color = mPaintOptions.color
|
||||
val img = bitmap.floodFill(color = color, x = touchedX, y = touchedY)
|
||||
addOperation(CanvasOp.BitmapOp(img))
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawADot() {
|
||||
mPath.lineTo(mCurX, mCurY)
|
||||
|
||||
// draw a dot on click
|
||||
if (mStartX == mCurX && mStartY == mCurY) {
|
||||
mPath.lineTo(mCurX, mCurY + 2)
|
||||
mPath.lineTo(mCurX + 1, mCurY + 2)
|
||||
mPath.lineTo(mCurX + 1, mCurY)
|
||||
}
|
||||
mOperations.add(CanvasOp.PathOp(mPath, mPaintOptions))
|
||||
}
|
||||
|
||||
private fun addOperation(operation: CanvasOp) {
|
||||
mOperations.add(operation)
|
||||
|
||||
// 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<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> {
|
||||
val pathOps = mOperations
|
||||
.filterIsInstance<CanvasOp.PathOp>()
|
||||
.map { it.path to it.paintOptions }
|
||||
.toTypedArray()
|
||||
return mapOf(*pathOps)
|
||||
}
|
||||
|
||||
fun getDrawingHashCode() = mOperations.hashCode().toLong() + (mBackgroundBitmap?.hashCode()?.toLong() ?: 0L)
|
||||
|
||||
private fun MotionEvent?.isTouchSlop(pointerIndex: Int, startX: Float, startY: Float): Boolean {
|
||||
return if (this == null || actionMasked != MotionEvent.ACTION_MOVE) {
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M18,14c0,-4 -6,-10.8 -6,-10.8s-1.33,1.51 -2.73,3.52l8.59,8.59c0.09,-0.42 0.14,-0.86 0.14,-1.31zM17.12,17.12L12.5,12.5 5.27,5.27 4,6.55l3.32,3.32C6.55,11.32 6,12.79 6,14c0,3.31 2.69,6 6,6 1.52,0 2.9,-0.57 3.96,-1.5l2.63,2.63 1.27,-1.27 -2.74,-2.74z"/>
|
||||
</vector>
|
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp" android:viewportHeight="48"
|
||||
android:viewportWidth="48" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFF"
|
||||
android:pathData="M10.561,5.804 L13.074,3.079 33.411,25.136c0.896,0.972 1.344,2.166 1.344,3.581 0,1.416 -0.448,2.609 -1.344,3.581L23.009,43.58c-0.896,0.972 -1.948,1.458 -3.156,1.458 -1.208,0 -2.26,-0.486 -3.156,-1.458L6.295,32.298c-0.896,-0.972 -1.344,-2.166 -1.344,-3.581 0,-1.416 0.448,-2.609 1.344,-3.581L17.341,13.157ZM19.853,15.882 L8.224,28.495L31.483,28.495ZM39.606,46.116c-1.169,0 -2.162,-0.444 -2.98,-1.331 -0.818,-0.887 -1.227,-1.965 -1.227,-3.233 0,-0.718 0.156,-1.5 0.468,-2.345 0.312,-0.845 0.74,-1.711 1.286,-2.599 0.312,-0.549 0.692,-1.141 1.14,-1.775 0.448,-0.634 0.886,-1.225 1.315,-1.775 0.429,0.549 0.867,1.141 1.315,1.775 0.448,0.634 0.828,1.225 1.14,1.775 0.545,0.887 0.974,1.754 1.286,2.599 0.312,0.845 0.468,1.627 0.468,2.345 0,1.268 -0.409,2.345 -1.227,3.233 -0.818,0.887 -1.812,1.331 -2.98,1.331z" android:strokeWidth="1.21722"/>
|
||||
</vector>
|
|
@ -44,11 +44,19 @@
|
|||
android:id="@+id/redo"
|
||||
android:layout_width="@dimen/normal_icon_size"
|
||||
android:layout_height="@dimen/normal_icon_size"
|
||||
android:layout_toStartOf="@+id/eraser"
|
||||
android:layout_toStartOf="@+id/color_fill"
|
||||
android:padding="@dimen/medium_margin"
|
||||
android:src="@drawable/ic_redo_vector"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/color_fill"
|
||||
android:layout_width="@dimen/normal_icon_size"
|
||||
android:layout_height="@dimen/normal_icon_size"
|
||||
android:layout_toStartOf="@+id/eraser"
|
||||
android:padding="@dimen/medium_margin"
|
||||
android:src="@drawable/ic_color_fill_on_vector" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/eraser"
|
||||
android:layout_width="@dimen/normal_icon_size"
|
||||
|
|
Loading…
Reference in New Issue