diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/actions/Action.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/actions/Action.kt new file mode 100644 index 000000000..adbc82662 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/actions/Action.kt @@ -0,0 +1,11 @@ +package com.simplemobiletools.gallery.pro.actions + +import android.graphics.Path +import java.io.Serializable +import java.io.Writer + +interface Action : Serializable { + fun perform(path: Path) + + fun perform(writer: Writer) +} diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/actions/Line.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/actions/Line.kt new file mode 100644 index 000000000..a38a70b1f --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/actions/Line.kt @@ -0,0 +1,37 @@ +package com.simplemobiletools.gallery.pro.actions + +import android.graphics.Path +import java.io.Writer +import java.security.InvalidParameterException + +class Line : Action { + + val x: Float + val y: Float + + constructor(data: String) { + if (!data.startsWith("L")) + throw InvalidParameterException("The Line data should start with 'L'.") + + try { + val xy = data.substring(1).split(",".toRegex()).dropLastWhile(String::isEmpty).toTypedArray() + x = xy[0].trim().toFloat() + y = xy[1].trim().toFloat() + } catch (ignored: Exception) { + throw InvalidParameterException("Error parsing the given Line data.") + } + } + + constructor(x: Float, y: Float) { + this.x = x + this.y = y + } + + override fun perform(path: Path) { + path.lineTo(x, y) + } + + override fun perform(writer: Writer) { + writer.write("L$x,$y") + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/actions/Move.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/actions/Move.kt new file mode 100644 index 000000000..bf96489bb --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/actions/Move.kt @@ -0,0 +1,37 @@ +package com.simplemobiletools.gallery.pro.actions + +import android.graphics.Path +import java.io.Writer +import java.security.InvalidParameterException + +class Move : Action { + + val x: Float + val y: Float + + constructor(data: String) { + if (!data.startsWith("M")) + throw InvalidParameterException("The Move data should start with 'M'.") + + try { + val xy = data.substring(1).split(",".toRegex()).dropLastWhile(String::isEmpty).toTypedArray() + x = xy[0].trim().toFloat() + y = xy[1].trim().toFloat() + } catch (ignored: Exception) { + throw InvalidParameterException("Error parsing the given Move data.") + } + } + + constructor(x: Float, y: Float) { + this.x = x + this.y = y + } + + override fun perform(path: Path) { + path.moveTo(x, y) + } + + override fun perform(writer: Writer) { + writer.write("M$x,$y") + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/actions/Quad.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/actions/Quad.kt new file mode 100644 index 000000000..fb281c29a --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/actions/Quad.kt @@ -0,0 +1,46 @@ +package com.simplemobiletools.gallery.pro.actions + +import android.graphics.Path +import java.io.Writer +import java.security.InvalidParameterException + +class Quad : Action { + + val x1: Float + val y1: Float + val x2: Float + val y2: Float + + constructor(data: String) { + if (!data.startsWith("Q")) + throw InvalidParameterException("The Quad data should start with 'Q'.") + + try { + val parts = data.split("\\s+".toRegex()).dropLastWhile(String::isEmpty).toTypedArray() + val xy1 = parts[0].substring(1).split(",".toRegex()).dropLastWhile(String::isEmpty).toTypedArray() + val xy2 = parts[1].split(",".toRegex()).dropLastWhile(String::isEmpty).toTypedArray() + + x1 = xy1[0].trim().toFloat() + y1 = xy1[1].trim().toFloat() + x2 = xy2[0].trim().toFloat() + y2 = xy2[1].trim().toFloat() + } catch (ignored: Exception) { + throw InvalidParameterException("Error parsing the given Quad data.") + } + } + + constructor(x1: Float, y1: Float, x2: Float, y2: Float) { + this.x1 = x1 + this.y1 = y1 + this.x2 = x2 + this.y2 = y2 + } + + override fun perform(path: Path) { + path.quadTo(x1, y1, x2, y2) + } + + override fun perform(writer: Writer) { + writer.write("Q$x1,$y1 $x2,$y2") + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/models/MyPath.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/models/MyPath.kt new file mode 100644 index 000000000..564eab0c8 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/models/MyPath.kt @@ -0,0 +1,82 @@ +package com.simplemobiletools.gallery.pro.models + +import android.app.Activity +import android.graphics.Path +import com.simplemobiletools.commons.extensions.toast +import com.simplemobiletools.gallery.pro.R +import com.simplemobiletools.gallery.pro.actions.Action +import com.simplemobiletools.gallery.pro.actions.Line +import com.simplemobiletools.gallery.pro.actions.Move +import com.simplemobiletools.gallery.pro.actions.Quad +import java.io.ObjectInputStream +import java.io.Serializable +import java.security.InvalidParameterException +import java.util.* + +// https://stackoverflow.com/a/8127953 +class MyPath : Path(), Serializable { + val actions = LinkedList() + + private fun readObject(inputStream: ObjectInputStream) { + inputStream.defaultReadObject() + + val copiedActions = actions.map { it } + copiedActions.forEach { + it.perform(this) + } + } + + fun readObject(pathData: String, activity: Activity) { + val tokens = pathData.split("\\s+".toRegex()).dropLastWhile(String::isEmpty).toTypedArray() + var i = 0 + try { + while (i < tokens.size) { + when (tokens[i][0]) { + 'M' -> addAction(Move(tokens[i])) + 'L' -> addAction(Line(tokens[i])) + 'Q' -> { + // Quad actions are of the following form: + // "Qx1,y1 x2,y2" + // Since we split the tokens by whitespace, we need to join them again + if (i + 1 >= tokens.size) + throw InvalidParameterException("Error parsing the data for a Quad.") + + addAction(Quad(tokens[i] + " " + tokens[i + 1])) + ++i + } + } + ++i + } + } catch (e: Exception) { + activity.toast(R.string.unknown_error_occurred) + } + } + + override fun reset() { + actions.clear() + super.reset() + } + + private fun addAction(action: Action) { + when (action) { + is Move -> moveTo(action.x, action.y) + is Line -> lineTo(action.x, action.y) + is Quad -> quadTo(action.x1, action.y1, action.x2, action.y2) + } + } + + override fun moveTo(x: Float, y: Float) { + actions.add(Move(x, y)) + super.moveTo(x, y) + } + + override fun lineTo(x: Float, y: Float) { + actions.add(Line(x, y)) + super.lineTo(x, y) + } + + override fun quadTo(x1: Float, y1: Float, x2: Float, y2: Float) { + actions.add(Quad(x1, y1, x2, y2)) + super.quadTo(x1, y1, x2, y2) + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/models/PaintOptions.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/models/PaintOptions.kt new file mode 100644 index 000000000..34c128dca --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/models/PaintOptions.kt @@ -0,0 +1,7 @@ +package com.simplemobiletools.gallery.pro.models + +import android.graphics.Color + +data class PaintOptions(var color: Int = Color.BLACK, var strokeWidth: Float = 5f, var isEraser: Boolean = false) { + fun getColorToExport() = if (isEraser) "none" else "#${Integer.toHexString(color).substring(2)}" +} diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/views/EditorDrawCanvas.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/views/EditorDrawCanvas.kt index c409de011..0451f0667 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/views/EditorDrawCanvas.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/views/EditorDrawCanvas.kt @@ -3,12 +3,42 @@ package com.simplemobiletools.gallery.pro.views import android.content.Context import android.graphics.Bitmap import android.graphics.Canvas +import android.graphics.Paint import android.util.AttributeSet +import android.view.MotionEvent import android.view.View +import com.simplemobiletools.gallery.pro.extensions.config +import com.simplemobiletools.gallery.pro.models.MyPath +import com.simplemobiletools.gallery.pro.models.PaintOptions +import java.util.* class EditorDrawCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) { + private var mCurX = 0f + private var mCurY = 0f + private var mStartX = 0f + private var mStartY = 0f + private var mColor = 0 + private var mWasMultitouch = false + + private var mPaths = LinkedHashMap() + private var mPaint = Paint() + private var mPath = MyPath() + private var mPaintOptions = PaintOptions() + private var backgroundBitmap: Bitmap? = null + init { + mColor = context.config.primaryColor + mPaint.apply { + color = mColor + style = Paint.Style.STROKE + strokeJoin = Paint.Join.ROUND + strokeCap = Paint.Cap.ROUND + strokeWidth = 40f + isAntiAlias = true + } + } + override fun onDraw(canvas: Canvas) { super.onDraw(canvas) canvas.save() @@ -19,9 +49,76 @@ class EditorDrawCanvas(context: Context, attrs: AttributeSet) : View(context, at canvas.drawBitmap(backgroundBitmap!!, left.toFloat(), top.toFloat(), null) } + for ((key, value) in mPaths) { + changePaint(value) + canvas.drawPath(key, mPaint) + } + + changePaint(mPaintOptions) + canvas.drawPath(mPath, mPaint) + canvas.restore() } + override fun onTouchEvent(event: MotionEvent): Boolean { + val x = event.x + val y = event.y + + when (event.action and MotionEvent.ACTION_MASK) { + MotionEvent.ACTION_DOWN -> { + mWasMultitouch = false + mStartX = x + mStartY = y + actionDown(x, y) + } + MotionEvent.ACTION_MOVE -> { + if (event.pointerCount == 1 && !mWasMultitouch) { + actionMove(x, y) + } + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> actionUp() + MotionEvent.ACTION_POINTER_DOWN -> mWasMultitouch = true + } + + invalidate() + return true + } + + private fun actionDown(x: Float, y: Float) { + mPath.reset() + mPath.moveTo(x, y) + mCurX = x + mCurY = y + } + + private fun actionMove(x: Float, y: Float) { + mPath.quadTo(mCurX, mCurY, (x + mCurX) / 2, (y + mCurY) / 2) + mCurX = x + mCurY = y + } + + private fun actionUp() { + if (!mWasMultitouch) { + 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 + mPath = MyPath() + mPaintOptions = PaintOptions(mPaintOptions.color, mPaintOptions.strokeWidth, mPaintOptions.isEraser) + } + + private fun changePaint(paintOptions: PaintOptions) { + mPaint.color = paintOptions.color + mPaint.strokeWidth = paintOptions.strokeWidth + } + fun updateBackgroundBitmap(bitmap: Bitmap) { backgroundBitmap = bitmap invalidate()