I have successfully achieved saving to the gallery after I finish drawing on canvas, but now I need to open an image from galley ( for example my old drawing) and use it as a background, after some searching there no answer which can help me. I already started some attempts to do this, but any advice or solutions how I can achieve it will be great.
There is my custom view with Canvas:
(Permission for READ and WRITE is already granted and handled in another class)
var drawingColor: Int = ResourcesCompat.getColor(resources, R.color.colorBlack, null)
var strokeDrawWidth: Float = 12f
private var path = Path()
private val paths = ArrayList<Triple<Path, Int, Float>>()
private val undonePaths = ArrayList<Triple<Path, Int, Float>>()
private val extraCanvas: Canvas? = null
private var bitmapBackground: Bitmap? = 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 = drawingColor
isAntiAlias = true
isDither = true
style = Paint.Style.STROKE
strokeJoin = Paint.Join.ROUND
strokeCap = Paint.Cap.ROUND
strokeWidth = strokeDrawWidth
}
fun loadCanvasBackground(bitmap: Bitmap) {
bitmapBackground = bitmap
invalidate()
}
fun saveCanvasDrawing() {
canvasCustomView.isDrawingCacheEnabled = true
val extraBitmap: Bitmap = canvasCustomView.drawingCache
MediaStore.Images.Media.insertImage(context.contentResolver, extraBitmap, "drawing", "Paint R")
}
fun resetCanvasDrawing() {
path.reset()
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)
if (bitmapBackground != null) {
extraCanvas?.drawBitmap(bitmapBackground!!, 0f, 0f, paint)
}
for (p in paths) {
paint.strokeWidth = p.third
paint.color = p.second
canvas?.drawPath(p.first, paint)
}
paint.color = drawingColor
paint.strokeWidth = strokeDrawWidth
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(Triple(path, drawingColor, strokeDrawWidth))
path = Path()
}
}
return true
}
override fun isSaveEnabled(): Boolean {
return true
}
So no-one answered and after some searching and experiments I get working solution, use it or adapt for your need if you will get in the same situation
( answer is based on the code from my question - so if you miss some dependencies please check it )
Init companion object with request code for gallery action in your ACTIVITY
companion object {
private const val GALLERY_REQUEST_CODE = 102
}
Create a method to pick image from gallery ( you need to receive Uri )
private fun pickFromGallery() {
val intent = Intent(Intent.ACTION_PICK)
intent.type = "image/*"
val imageTypes = arrayOf("image/jpeg", "image/png")
intent.putExtra(Intent.EXTRA_MIME_TYPES, imageTypes)
startActivityForResult(intent, GALLERY_REQUEST_CODE)
}
Than you need to override onActivityResult() method to receive your Uri and send it to the custom view
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == GALLERY_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
val uri: Uri? = data?.data
if (uri != null) {
val bitmap = MediaStore.Images.Media.getBitmap(this.contentResolver, uri)
canvasCustomView.loadCanvasBackground(bitmap)
}
}
}
}
Now in onDraw() method ( in your Custom View ) you need to use .drawBitmap to set your received Uri aka bitmap as a background to your canvas
override fun onDraw(canvas: Canvas?) {
if (bitmapBackground != null) {
canvas?.drawBitmap(bitmapBackground!!, 0f, 0f, paint)
}
Related
Im trying to get the detected object from the OverlayView. This project is TensorFlow Lite.
The detected object will be displayed in the text view and then will also be broadcasted using text to speech after long press.
This is the MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
activityMainBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(activityMainBinding.root)
btnSpeak = findViewById(R.id.buttonbtn)
val etSpeak: TextView = findViewById(R.id.DetectedNameTxtView)
// TextToSpeech(Context: this, OnInitListener: this)
tts = TextToSpeech(this, this)
btnSpeak!!.isEnabled = false;
}
private fun speakOut() {
//set the text from the textView to lowercase
val etSpeak: TextView = findViewById(R.id.DetectedNameTxtView)
val detected = etSpeak?.text.toString().lowercase()
if (detected == null || detected == "textview"){
val noDetected = "No Object Detected";
tts!!.speak(noDetected, TextToSpeech.QUEUE_FLUSH, null,"")
} else{
Log.e("detectionMain",etSpeak.toString())
tts!!.speak(detected, TextToSpeech.QUEUE_FLUSH, null,"")
}
}
override fun onInit(status: Int) {
if (status == TextToSpeech.SUCCESS) {
val result = tts!!.setLanguage(Locale.US)
if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {
Log.e("TTS","The Language not supported!")
} else {
btnSpeak!!.isEnabled = true
}
} else {
Log.e("TTS", "Initilization Failed!")
}
}
override fun onBackPressed() {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
// Workaround for Android Q memory leak issue in IRequestFinishCallback$Stub.
// (https://issuetracker.google.com/issues/139738913)
finishAfterTransition()
} else {
super.onBackPressed()
}
}
// GestureDetecctor to detect long press
private val gestureDetector = GestureDetector(object : SimpleOnGestureListener() {
override fun onLongPress(e: MotionEvent) {
speakOut()
// Toast to notify the Long Press
Toast.makeText(applicationContext, "Long Press Detected", Toast.LENGTH_SHORT).show()
}
})
// onTouchEvent to confirm presence of Touch due to Long Press
override fun onTouchEvent(event: MotionEvent?): Boolean {
return gestureDetector.onTouchEvent(event)
}
This is the OverlayView. The string that I need to transfer is the drawabletext.
private var results: List<Detection> = LinkedList<Detection>()
override fun draw(canvas: Canvas) {
super.draw(canvas)
for (result in results) {
val boundingBox = result.boundingBox
val top = boundingBox.top * scaleFactor
val bottom = boundingBox.bottom * scaleFactor
val left = boundingBox.left * scaleFactor
val right = boundingBox.right * scaleFactor
// Draw bounding box around detected objects
val drawableRect = RectF(left, top, right, bottom)
canvas.drawRect(drawableRect, boxPaint)
// Create text to display alongside detected objects
var drawableText = result.categories[0].label
// Draw rect behind display text
textBackgroundPaint.getTextBounds(drawableText, 0, drawableText.length, bounds)
val textWidth = bounds.width()
val textHeight = bounds.height()
canvas.drawRect(
left,
top,
left + textWidth + Companion.BOUNDING_RECT_TEXT_PADDING,
top + textHeight + Companion.BOUNDING_RECT_TEXT_PADDING,
textBackgroundPaint
)
// Draw text for detected object
canvas.drawText(drawableText, left, top + bounds.height(), textPaint)
}
}
I'm developing an android app with drawing functionality. I want to save those drawing Paths, so i can redraw them when I reopen the activity from history. I just want to save everything either its a path or custom shape on canvas.
private val drawShapes = Stack<ShapeAndPaint?>()
private val redoShapes = Stack<ShapeAndPaint?>()
private var currentShape: ShapeAndPaint? = null
var isDrawingEnabled = false
private set
private var viewChangeListener: BrushViewChangeListener? = null
var currentShapeBuilder: ShapeBuilder? = null
// eraser parameters
private var isErasing = false
var serializablePath = SerializablePath()
var customPath = CustomPath()
// endregion
#SuppressLint("Range")
private fun createPaint(): Paint {
val paint = Paint()
paint.isAntiAlias = true
paint.isDither = true
paint.style = Paint.Style.STROKE
paint.strokeJoin = Paint.Join.ROUND
paint.strokeCap = Paint.Cap.ROUND
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)
// apply shape builder parameters
currentShapeBuilder?.apply {
paint.strokeWidth = this.shapeSize
paint.alpha = this.shapeOpacity
paint.color = this.shapeColor
}
if (WhiteBoardActivity.dullPenSelected){
paint.alpha = 40
}
return paint
}
private fun createEraserPaint(): Paint {
val paint = createPaint()
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
return paint
}
private fun setupBrushDrawing() {
//Caution: This line is to disable hardware acceleration to make eraser feature work properly
setLayerType(LAYER_TYPE_HARDWARE, null)
visibility = GONE
currentShapeBuilder = ShapeBuilder()
}
fun clearAll() {
drawShapes.clear()
redoShapes.clear()
invalidate()
}
fun setBrushViewChangeListener(brushViewChangeListener: BrushViewChangeListener?) {
viewChangeListener = brushViewChangeListener
}
public override fun onDraw(canvas: Canvas) {
for (shape in drawShapes) {
shape?.shape?.draw(canvas, shape.paint)
}
}
#SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
return if (isDrawingEnabled) {
val touchX = event.x
val touchY = event.y
when (event.action) {
MotionEvent.ACTION_DOWN -> onTouchEventDown(touchX, touchY)
MotionEvent.ACTION_MOVE -> onTouchEventMove(touchX, touchY)
MotionEvent.ACTION_UP -> onTouchEventUp(touchX, touchY)
}
invalidate()
true
} else {
false
}
}
private fun onTouchEventDown(touchX: Float, touchY: Float) {
createShape()
currentShape?.shape?.startShape(touchX, touchY)
customPath.ActionLine(touchX,touchY)
}
private fun onTouchEventMove(touchX: Float, touchY: Float) {
currentShape?.shape?.moveShape(touchX, touchY)
customPath.ActionMove(touchX,touchY)
}
private fun onTouchEventUp(touchX: Float, touchY: Float) {
currentShape?.apply {
shape.stopShape()
endShape(touchX, touchY)
}
}
private fun createShape() {
var paint = createPaint()
var shape: AbstractShape = BrushShape()
if (isErasing) {
paint = createEraserPaint()
} else {
when(currentShapeBuilder?.shapeType){
ShapeType.BRUSH -> {
shape = BrushShape()
}
else -> {}
}
}
currentShape = ShapeAndPaint(shape, paint)
drawShapes.push(currentShape)
viewChangeListener?.onStartDrawing()
}
private fun endShape(touchX: Float, touchY: Float) {
if (currentShape?.shape?.hasBeenTapped() == true) {
// just a tap, this is not a shape, so remove it
drawShapes.remove(currentShape)
//handleTap(touchX, touchY);
}
viewChangeListener?.apply {
onStopDrawing()
onViewAdd(this#DrawingView)
}
}
fun undo(): Boolean {
if (!drawShapes.empty()) {
redoShapes.push(drawShapes.pop())
invalidate()
}
viewChangeListener?.onViewRemoved(this)
return !drawShapes.empty()
}
fun redo(): Boolean {
if (!redoShapes.empty()) {
drawShapes.push(redoShapes.pop())
invalidate()
}
viewChangeListener?.onViewAdd(this)
return !redoShapes.empty()
}
// region eraser
fun brushEraser() {
isDrawingEnabled = true
isErasing = true
}
// endregion
// region Setters/Getters
fun enableDrawing(brushDrawMode: Boolean) {
isDrawingEnabled = brushDrawMode
isErasing = !brushDrawMode
if (brushDrawMode) {
visibility = VISIBLE
}
}
fun getBitmap(): Bitmap? {
destroyDrawingCache()
this.buildDrawingCache()
return this.drawingCache
}
fun setPaintAlpha(i: Int) {
createPaint().alpha = i
}
// endregion
val drawingPath: Pair<Stack<ShapeAndPaint?>, Stack<ShapeAndPaint?>>
get() = Pair(drawShapes, redoShapes)
companion object {
private const val DEFAULT_ERASER_SIZE = 50.0f
var eraserSize = DEFAULT_ERASER_SIZE
}
// region constructors
init {
setupBrushDrawing()
}
}
this is my DrawingView class.
is there any possible solution? Thanks in advance.
I am trying to build a realtime text recognization app using google MLKit vision, it's showing text properly but when I am trying to click on a particular line it is only showing the last line text.
Here is Overlay Code :
TextGraphic.kt
class TextGraphic(overlay: GraphicOverlay?,
private val element: Text.Line,
font: Typeface,
fontSize: Float,
color: Int) : Graphic(overlay!!) {
private val rectPaint: Paint = Paint()
private val textPaint: Paint
override fun draw(canvas: Canvas?) {
val rect = RectF(element.boundingBox)
canvas!!.drawRect(rect, rectPaint)
canvas.drawText(element.text, rect.left, rect.bottom, textPaint)
}
companion object {
private const val TAG = "TextGraphic"
private const val TEXT_COLOR = Color.BLACK
private const val STROKE_WIDTH = 2.0f
}
init {
rectPaint.color = color
rectPaint.style = Paint.Style.FILL_AND_STROKE
rectPaint.strokeWidth = STROKE_WIDTH
textPaint = Paint()
textPaint.color = TEXT_COLOR
textPaint.textSize = fontSize
textPaint.typeface = font
postInvalidate()
}}
GraphicOverlay.kt
class GraphicOverlay(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
private val lock = Any()
private var previewWidth = 0
private var widthScaleFactor = 1.0f
private var previewHeight = 0
private var heightScaleFactor = 1.0f
private var facing = CameraCharacteristics.LENS_FACING_BACK
private val graphics: MutableSet<Graphic> = HashSet()
abstract class Graphic(private val overlay: GraphicOverlay) {
abstract fun draw(canvas: Canvas?)
fun scaleX(horizontal: Float): Float {
return horizontal * overlay.widthScaleFactor
}
fun scaleY(vertical: Float): Float {
return vertical * overlay.heightScaleFactor
}
val applicationContext: Context
get() = overlay.context.applicationContext
fun translateX(x: Float): Float {
return if (overlay.facing == CameraCharacteristics.LENS_FACING_FRONT) {
overlay.width - scaleX(x)
} else {
scaleX(x)
}
}
fun translateY(y: Float): Float {
return scaleY(y)
}
fun postInvalidate() {
overlay.postInvalidate()
}
}
fun clear() {
synchronized(lock) { graphics.clear() }
postInvalidate()
}
fun add(graphic: Graphic) {
synchronized(lock) { graphics.add(graphic) }
postInvalidate()
}
fun remove(graphic: Graphic) {
synchronized(lock) { graphics.remove(graphic) }
postInvalidate()
}
fun setCameraInfo(previewWidth: Int, previewHeight: Int, facing: Int) {
synchronized(lock) {
this.previewWidth = previewWidth
this.previewHeight = previewHeight
this.facing = facing
}
postInvalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
synchronized(lock) {
if (previewWidth != 0 && previewHeight != 0) {
widthScaleFactor = width.toFloat() / previewWidth.toFloat()
heightScaleFactor = height.toFloat() / previewHeight.toFloat()
}
for (graphic in graphics) {
graphic.draw(canvas)
}
}
}}
Inside my Fragment where I am clicking :
private fun processTextFromImage(visionText: Text, imageProxy: ImageProxy) {
binding.graphicOverlay.clear()
for (block in visionText.textBlocks) {
for (line in block.lines) {
val textGraphic = TextGraphic(binding.graphicOverlay, line, font, fontSize, color = fontColor)
binding.graphicOverlay.apply {
add(textGraphic)
setOnClickListener {
Toast.makeText(it.context, line.text, Toast.LENGTH_SHORT).show()
}
}
for (element in line.elements) {
textFoundListener(element.text)
}
}
}
}
Is there any better way to to display overlay, this overlay is too fast and my click is only displays the last line text.
If anyone can help me in this, thanks a lot.
I'm trying to make custom image view that have rounded corners and a custom transition to change a border radius smoothly.
In CircleTransition, I try to get imageCornerRadius but it's always return 0 which ruined the transaction. But in activity, when I get imageCornerRadius, it returns the value in xml file. So how i can get the imageCornerRadius to perform the transition.
This is declare of my custom view
RoundedImageView
custom attribute
<declare-styleable name="RoundedImageView">
<attr name="imageCornerRadius" format="dimension" />
</declare-styleable>
class RoundedImageView : AppCompatImageView {
constructor(context: Context) : super(context) {
Log.d("debug", "first constructor")
}
constructor(context: Context, attrSet: AttributeSet) : super(context, attrSet) {
Log.d("debug", "second constructor")
init(attrSet)
}
constructor(context: Context, attrSet: AttributeSet, defStyleAttr: Int) : super(
context,
attrSet,
defStyleAttr
) {
Log.d("debug", "third constructor")
init(attrSet)
}
private fun init(attrSet: AttributeSet){
context.theme.obtainStyledAttributes(
attrSet,
R.styleable.RoundedImageView,
0,
0
).apply {
try {
imageCornerRadius = getDimensionPixelSize(
R.styleable.RoundedImageView_imageCornerRadius,
0
).toFloat()
} finally {
recycle()
}
}
}
// Custom attr
var imageCornerRadius: Float = 0F
//Attr for drawing
private lateinit var bitmapRect: RectF
val rect = RectF(drawable.bounds)
val holePath = Path()
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
Log.d("size changed", "w = $w h = $h")
bitmapRect = RectF(0f, 0f, w.toFloat(), h.toFloat())
}
override fun onDraw(canvas: Canvas?) {
val drawableWidth = this.width
val drawableHeight = this.height
/* Clip */
holePath.apply {
reset()
addRoundRect(
0F,
0F,
rect.right + drawableWidth,
rect.bottom + drawableHeight,
imageCornerRadius,
imageCornerRadius,
Path.Direction.CW
)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
canvas?.clipPath(holePath)
} else {
#Suppress("DEPRECATION")
canvas?.clipPath(holePath, Region.Op.REPLACE)
}
// Draw image
super.onDraw(canvas)
}
}
My custom transition change Size, Coordinate, imageCornerRadius
CircleTransition.kt
class CircleTransition() : Transition() {
private val TAG = CircleTransition::class.java.simpleName
private val BOUNDS = TAG + ":viewBounds"
private val CORNER_RADIUS = TAG + ":imageCornerRadius"
private val PROPS = arrayOf(BOUNDS, CORNER_RADIUS)
init {
Log.d("debug", "Circle Transition called")
}
override fun captureStartValues(transitionValues: TransitionValues?) {
captureValues(transitionValues)
}
override fun captureEndValues(transitionValues: TransitionValues?) {
captureValues(transitionValues)
}
fun captureValues(transitionValues: TransitionValues?) {
val view = transitionValues?.view
//get View Bound
val bound = RectF()
bound.left = view?.left?.toFloat() ?: return
bound.top = view.top.toFloat()
bound.right = view.right.toFloat()
bound.bottom = view.bottom.toFloat()
transitionValues.values.put(BOUNDS, bound)
//get view Corner radius
if(view is RoundedImageView){
val cornerRadius = view.imageCornerRadius
transitionValues.values.put(CORNER_RADIUS, cornerRadius)
}
}
override fun getTransitionProperties(): Array<String> {
return PROPS
}
override fun createAnimator(
sceneRoot: ViewGroup?,
startValues: TransitionValues?,
endValues: TransitionValues?
): Animator? {
if (startValues == null || endValues == null) {
return null
}
val view = endValues.view as RoundedImageView
//startScene
val sBound = startValues.values[BOUNDS] as RectF? ?: return null
//How I get imageCornerRadius
val sCornerRadius = startValues.values[CORNER_RADIUS] as Float? ?: return null
val sWidth = sBound.right - sBound.left
val sHeight = sBound.top - sBound.bottom
//endScene
val eBound = endValues.values[BOUNDS] as RectF? ?: return null
//How I get imageCornerRadius
val eCornerRadius = endValues.values[CORNER_RADIUS] as Float? ?: return null
val eWidth = eBound.right - eBound.left
val eHeight = eBound.top - eBound.bottom
if (sBound == eBound && sCornerRadius == eCornerRadius) {
return null
}
val widthAnimator: ValueAnimator =
ValueAnimator.ofInt(sWidth.toInt(), eWidth.toInt()).apply {
addUpdateListener {
val layoutParams = view.layoutParams
layoutParams.width = it.animatedValue as Int
view.layoutParams = layoutParams
}
}
val heightAnimator: ValueAnimator =
ValueAnimator.ofInt(sHeight.toInt() * -1, eHeight.toInt() * -1).apply {
interpolator = AccelerateInterpolator()
addUpdateListener {
val layoutParams = view.layoutParams
layoutParams.height = it.animatedValue as Int
view.layoutParams = layoutParams
}
}
val cornerRadiusAnimator = ValueAnimator.ofFloat(96F, 0F).apply {
addUpdateListener {
view.imageCornerRadius = it.animatedValue as Float
}
}
// set endView have the same size, coorinate like startScene
view.x = sBound.left
view.y = sBound.top
// view.layoutParams = ViewGroup.LayoutParams(sBound.width().toInt(), sBound.height().toInt())
// move view
val startX = sBound.left
val startY = sBound.top
val moveXTo = eBound.left
val moveYTo = eBound.top
val moveXAnimator: Animator =
ObjectAnimator.ofFloat(view, "x", startX, moveXTo.toFloat())
val moveYAnimator: Animator =
ObjectAnimator.ofFloat(view, "y", startY, moveYTo.toFloat()).apply {
addUpdateListener {
view.invalidate()
}
}
val animatorSet = AnimatorSet()
animatorSet.playTogether(
widthAnimator,
heightAnimator,
cornerRadiusAnimator,
moveXAnimator,
moveYAnimator
)
return animatorSet
}
}
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
}
}
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
}
}