Search code examples
androidkotlincanvaspaintundo-redo

Undo/Redo with Canvas Bitmap drawing (KOTLIN)


So im trying to to Undo/Redo action and there is a few answers on stackoverflow about this problem, but any of them is not helping with my issue. So I have my custom view for canvas implementation, where I have an arrays to store paths of my drawing, but any time Im start storing in it just do nothing. Any advices or link are appreciated.

My custom view class:

private const val STROKE_WIDTH = 12f

class CanvasCustomView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {
    private var path = Path()

    private val paths = ArrayList<Path>()
    private val undonePaths = ArrayList<Path>()

    private lateinit var extraCanvas: Canvas
    private lateinit var extraBitmap: Bitmap

    private var motionTouchEventX = 0f
    private var motionTouchEventY = 0f

    private var currentX = 0f
    private var currentY = 0f

    private val touchTolerance = ViewConfiguration.get(context).scaledTouchSlop

    private fun touchStart() {
        path.reset()
        path.moveTo(motionTouchEventX, motionTouchEventY)
        currentX = motionTouchEventX
        currentY = motionTouchEventY
    }

    private fun touchMove() {
        val distanceX = abs(motionTouchEventX - currentX)
        val distanceY = abs(motionTouchEventY - currentY)

        if (distanceX >= touchTolerance || distanceY >= touchTolerance) {
            path.quadTo(
                currentX,
                currentY,
                (motionTouchEventX + currentX) / 2,
                (motionTouchEventY + currentY) / 2)
            currentX = motionTouchEventX
            currentY = motionTouchEventY
            extraCanvas.drawPath(path, paint)
        }
        invalidate()
    }

    private fun touchUp() {
        path.reset()
    }

    fun undoCanvasDrawing(){
// im trying to do undo here
    }

    fun clearCanvasDrawing() {
        extraCanvas.drawColor(0, PorterDuff.Mode.CLEAR)
        path.reset()
        invalidate()
    }

    private val backgroundColor = ResourcesCompat.getColor(resources, R.color.colorBackground, null)
    private val drawColor = ResourcesCompat.getColor(resources, R.color.colorPaint, null)

    private val paint = Paint().apply {
        color = drawColor
        isAntiAlias = true
        isDither = true
        style = Paint.Style.STROKE
        strokeJoin = Paint.Join.ROUND
        strokeCap = Paint.Cap.ROUND
        strokeWidth = STROKE_WIDTH
    }

    override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
        super.onSizeChanged(width, height, oldWidth, oldHeight)

        if (::extraBitmap.isInitialized) extraBitmap.recycle()

        extraBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)

        extraCanvas = Canvas(extraBitmap)
        extraCanvas.drawColor(backgroundColor)
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        canvas?.drawBitmap(extraBitmap, 0f, 0f, null)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        if (event == null)
            return false

        motionTouchEventX = event.x
        motionTouchEventY = event.y

        when (event.action) {
            MotionEvent.ACTION_DOWN -> touchStart()
            MotionEvent.ACTION_MOVE -> touchMove()
            MotionEvent.ACTION_UP -> touchUp()
        }
        return true
    }
}

Solution

  • So I decide to not use Bitmap in case you need to store a collection of Pathses,and it's very expensive. So there is my solution with undo/redo and reset functionality

    import android.content.Context
    import android.graphics.*
    import android.util.AttributeSet
    import android.util.Log
    import android.view.MotionEvent
    import android.view.View
    import android.view.ViewConfiguration
    import androidx.core.content.res.ResourcesCompat
    import kotlin.math.abs
    
    class CanvasCustomView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {
        companion object {
            private const val STROKE_WIDTH = 12f
        }
    
        private var path = Path()
    
        private val paths = ArrayList<Path>()
        private val undonePaths = ArrayList<Path>()
    
        private val extraCanvas: Canvas? = null
    
        private var motionTouchEventX = 0f
        private var motionTouchEventY = 0f
    
        private var currentX = 0f
        private var currentY = 0f
    
        private val touchTolerance = ViewConfiguration.get(context).scaledTouchSlop
    
        private val paint = Paint().apply {
            color = ResourcesCompat.getColor(resources, R.color.colorBlack, null)
            isAntiAlias = true
            isDither = true
            style = Paint.Style.STROKE
            strokeJoin = Paint.Join.ROUND
            strokeCap = Paint.Cap.ROUND
            strokeWidth = STROKE_WIDTH
        }
    
        fun resetCanvasDrawing() {
            path.reset() // Avoiding saving redo from Path()
            paths.clear()
            invalidate()
        }
    
        fun undoCanvasDrawing() {
            if (paths.size > 0) {
                undonePaths.add(paths.removeAt(paths.size - 1))
                invalidate()
            } else {
                Log.d("UNDO_ERROR", "Something went wrong with UNDO action")
            }
        }
    
        fun redoCanvasDrawing() {
            if (undonePaths.size > 0) {
                paths.add(undonePaths.removeAt(undonePaths.size - 1))
                invalidate()
            } else {
                Log.d("REDO_ERROR", "Something went wrong with REDO action")
            }
        }
    
        override fun onDraw(canvas: Canvas?) {
            super.onDraw(canvas)
            for (Path in paths) {
                canvas?.drawPath(Path, paint)
            }
            canvas?.drawPath(path, paint)
        }
    
        override fun onTouchEvent(event: MotionEvent?): Boolean {
            if (event == null)
                return false
    
            motionTouchEventX = event.x
            motionTouchEventY = event.y
    
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    undonePaths.clear()
                    path.reset()
                    path.moveTo(motionTouchEventX, motionTouchEventY)
                    currentX = motionTouchEventX
                    currentY = motionTouchEventY
                    invalidate()
                }
    
                MotionEvent.ACTION_MOVE -> {
                    val distanceX = abs(motionTouchEventX - currentX)
                    val distanceY = abs(motionTouchEventY - currentY)
    
                    if (distanceX >= touchTolerance || distanceY >= touchTolerance) {
                        path.quadTo(
                            currentX,
                            currentY,
                            (motionTouchEventX + currentX) / 2,
                            (currentY + motionTouchEventY) / 2)
                        currentX = motionTouchEventX
                        currentY = motionTouchEventY
                    }
                    invalidate()
                }
    
                MotionEvent.ACTION_UP -> {
                    path.lineTo(currentX, currentY)
                    extraCanvas?.drawPath(path, paint)
                    paths.add(path)
                    path = Path()
                }
            }
            return true
        }
    }