Merge branch 'SimpleMobileTools:master' into patch-1

This commit is contained in:
CactiChameleon9
2023-03-12 20:35:23 +00:00
committed by GitHub
26 changed files with 262 additions and 319 deletions

8
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,8 @@
### Reporting
Before you report something, read the reporting rules [here](https://github.com/SimpleMobileTools/General-Discussion#how-do-i-suggest-an-improvement-ask-a-question-or-report-an-issue) please.
### Contributing as a developer
Some instructions about code style and everything that has to be done to increase the change of your code getting accepted can be found at the [General Discussion](https://github.com/SimpleMobileTools/General-Discussion#contribution-rules-for-developers) section.
### Contributing as a non developer
In case you just want to for example improve a translation, you can find the way of doing it [here](https://github.com/SimpleMobileTools/General-Discussion#how-can-i-suggest-an-edit-to-a-file).

View File

@@ -63,6 +63,6 @@ android {
} }
dependencies { dependencies {
implementation 'com.github.SimpleMobileTools:Simple-Commons:f80a1e1ad8' implementation 'com.github.SimpleMobileTools:Simple-Commons:a7d47190b9'
implementation "androidx.print:print:1.0.0" implementation "androidx.print:print:1.0.0"
} }

View File

@@ -17,6 +17,7 @@ import android.widget.Toast
import androidx.print.PrintHelper import androidx.print.PrintHelper
import com.simplemobiletools.commons.dialogs.ColorPickerDialog import com.simplemobiletools.commons.dialogs.ColorPickerDialog
import com.simplemobiletools.commons.dialogs.ConfirmationAdvancedDialog import com.simplemobiletools.commons.dialogs.ConfirmationAdvancedDialog
import com.simplemobiletools.commons.dialogs.ConfirmationDialog
import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.LICENSE_GLIDE import com.simplemobiletools.commons.helpers.LICENSE_GLIDE
import com.simplemobiletools.commons.helpers.PERMISSION_WRITE_STORAGE import com.simplemobiletools.commons.helpers.PERMISSION_WRITE_STORAGE
@@ -129,6 +130,10 @@ class MainActivity : SimpleActivity(), CanvasListener {
if (!isImageCaptureIntent) { if (!isImageCaptureIntent) {
checkWhatsNewDialog() checkWhatsNewDialog()
} }
if (isPackageInstalled("com.simplemobiletools.draw")) {
ConfirmationDialog(this, "", R.string.upgraded_to_pro, R.string.ok, 0, false) {}
}
} }
override fun onResume() { override fun onResume() {

View File

@@ -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
} }

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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()
}

View File

@@ -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>()
}
}
}

View File

@@ -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)
} }
} }

View File

@@ -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
@@ -29,17 +26,16 @@ import kotlin.math.abs
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 = 1
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()
@@ -62,6 +58,7 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
private var mIsBucketFillOn = false private var mIsBucketFillOn = false
private var mWasMultitouch = false private var mWasMultitouch = false
private var mIgnoreTouches = false private var mIgnoreTouches = false
private var mIgnoreMultitouchChanges = false
private var mWasScalingInGesture = false private var mWasScalingInGesture = false
private var mWasMovingCanvasInGesture = false private var mWasMovingCanvasInGesture = false
private var mBackgroundColor = 0 private var mBackgroundColor = 0
@@ -87,21 +84,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()
} }
@@ -166,7 +159,7 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
actionMove(newValueX, newValueY) actionMove(newValueX, newValueY)
} }
if (mAllowMovingZooming && mWasMultitouch) { if (mAllowMovingZooming && mWasMultitouch && !mIgnoreMultitouchChanges) {
mPosX += x - mLastTouchX mPosX += x - mLastTouchX
mPosY += y - mLastTouchY mPosY += y - mLastTouchY
mWasMovingCanvasInGesture = true mWasMovingCanvasInGesture = true
@@ -175,6 +168,7 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
mLastTouchX = x mLastTouchX = x
mLastTouchY = y mLastTouchY = y
mIgnoreMultitouchChanges = false
} }
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
mActivePointerId = INVALID_POINTER_ID mActivePointerId = INVALID_POINTER_ID
@@ -185,12 +179,14 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
MotionEvent.ACTION_POINTER_DOWN -> { MotionEvent.ACTION_POINTER_DOWN -> {
if (mAllowMovingZooming) { if (mAllowMovingZooming) {
mWasMultitouch = true mWasMultitouch = true
mIgnoreMultitouchChanges = true
mTouchSloppedBeforeMultitouch = mLastMotionEvent.isTouchSlop(pointerIndex, mStartX, mStartY) mTouchSloppedBeforeMultitouch = mLastMotionEvent.isTouchSlop(pointerIndex, mStartX, mStartY)
} }
} }
MotionEvent.ACTION_POINTER_UP -> { MotionEvent.ACTION_POINTER_UP -> {
if (mAllowMovingZooming) { if (mAllowMovingZooming) {
mIgnoreTouches = true mIgnoreTouches = true
mIgnoreMultitouchChanges = true
actionUp(!mWasScalingInGesture && !mWasMovingCanvasInGesture) actionUp(!mWasScalingInGesture && !mWasMovingCanvasInGesture)
} }
} }
@@ -220,19 +216,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)
} }
} }
@@ -243,7 +229,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()
@@ -252,8 +238,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()
@@ -261,8 +249,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()
@@ -325,12 +313,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
@@ -340,7 +322,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()
@@ -395,8 +377,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() }
} }
} }
@@ -411,37 +394,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()) {
@@ -489,3 +454,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
}

View File

@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">Yksinkertainen arvonta</string> <string name="app_name">Yksinkertainen piirto</string>
<string name="app_launcher_name">Piir</string> <string name="app_launcher_name">Piirto</string>
<string name="open_file">Avaa tiedosto</string> <string name="open_file">Avaa tiedosto</string>
<string name="eraser">Pyyhekumi</string> <string name="eraser">Pyyhekumi</string>
<string name="eyedropper">Silmätippari</string> <string name="eyedropper">Silmätippari</string>
<string name="bucket_fill">Bucket fill</string> <string name="bucket_fill">Ämpärin täyttö</string>
<string name="failed_to_load_image">Kuvan %s lataaminen epäonnistui</string> <string name="failed_to_load_image">Kuvan %s lataaminen epäonnistui</string>
<!-- Settings --> <!-- Settings -->
<string name="show_brush_size">Näytä siveltimen koon työkalu</string> <string name="show_brush_size">Näytä siveltimen koon työkalu</string>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Simpleng Pagguhit</string>
<string name="app_launcher_name">Gumuhit</string>
<string name="open_file">Magbukas ng file</string>
<string name="eraser">Pambura</string>
<string name="eyedropper">Eyedropper</string>
<string name="bucket_fill">Punan ng pintura</string>
<string name="failed_to_load_image">Nagkaproblema sa pag-load ng larawang %s</string>
<!-- Settings -->
<string name="show_brush_size">Ipakita ang pampalit ng laki ng brush</string>
<string name="allow_zooming_moving_canvas">Payagan ang pag-zoom at paggalaw sa canvas gamit ang mga gesture</string>
<string name="clear">Burahin</string>
<string name="change_background_color">Baguhin ang kulay ng background</string>
</resources>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">Basit Çiz</string> <string name="app_name">Basit Çizim</string>
<string name="app_launcher_name">Çiz</string> <string name="app_launcher_name">Çiz</string>
<string name="open_file">Dosya aç</string> <string name="open_file">Dosya aç</string>
<string name="eraser">Silici</string> <string name="eraser">Silici</string>

View File

@@ -55,5 +55,5 @@ https://www.facebook.com/simplemobiletools
ريديت: ريديت:
https://www.reddit.com/r/SimpleMobileTools https://www.reddit.com/r/SimpleMobileTools
برقية: تيليجرام:
https://t.me/SimpleMobileTools https://t.me/SimpleMobileTools

View File

@@ -1 +1 @@
Yksinkertainen piirtää Pro Yksinkertainen piirto Pro

View File

@@ -0,0 +1 @@
Gumuhit gamit ang mabilisang sketchbook na ito nang walang panulat at papel

View File

@@ -0,0 +1 @@
Simpleng Pagguhit Pro

View File

@@ -1 +1 @@
Papper och penna för dina snabba skisser. Papper och penna för dina snabba skisser

View File

@@ -1 +1 @@
Simple Draw Pro Basit Çizim Pro