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
}
}
Related
I was making a simple custom DrawingView class which can draw with small brushes. But When I try to run it gets Nullpointer Exception
this is the stacktrace.
Process: com.mahidev.kidsdrawingapp, PID: 19949
java.lang.NullPointerException
at com.mahidev.kidsdrawingapp.DrawingView.onDraw(DrawingView.kt:64)
at android.view.View.draw(View.java:22635)
at android.view.View.updateDisplayListIfDirty(View.java:21472)
at android.view.View.draw(View.java:22335)
at android.view.ViewGroup.drawChild(ViewGro
this is drawingview - 64th line. I didn't get any clue what is happening?
canvas.drawBitmap(mCanvasBitmap!!, 0f, 0f, mCanvasPaint)
package com.mahidev.kidsdrawingapp
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.Log
import android.util.Size
import android.util.TypedValue
import android.view.MotionEvent
import android.view.View
class DrawingView(context: Context, attrs: AttributeSet) : View(context, attrs)
{
private var mDrawPath : CustomPath? = null
private var mCanvasBitmap: Bitmap? = null
private var mDrawPaint: Paint? = null
private var mCanvasPaint: Paint? = null
private var mBrushSize: Float = 0.toFloat()
private var color = Color.BLACK
private var canvas : Canvas? = null
private val mPaths = ArrayList<CustomPath>()
init
{
Log.i("Mahi", "inside init")
setUpDrawing()
}
private fun setUpDrawing()
{
Log.i("Mahi", "inside setUpDrawing")
mDrawPaint = Paint()
mDrawPath = CustomPath(color, mBrushSize)
mDrawPaint!!.color = color
mDrawPaint!!.style = Paint.Style.STROKE
mDrawPaint!!.strokeJoin = Paint.Join.ROUND
mDrawPaint!!.strokeCap = Paint.Cap.ROUND
mCanvasPaint = Paint(Paint.DITHER_FLAG)
// mBrushSize = 20.toFloat()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int)
{
Log.i("Mahi", "inside onSizeChanged")
super.onSizeChanged(w, h, oldw, oldh)
mCanvasBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
canvas = Canvas(mCanvasBitmap!!)
}
fun setSizeForBrush(newSize: Float)
{
mBrushSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, newSize, resources.displayMetrics)
mDrawPaint!!.strokeWidth = mBrushSize
}
override fun onDraw(canvas: Canvas) {
Log.i("Mahi", "onDraw")
super.onDraw(canvas)
canvas.drawBitmap(mCanvasBitmap!!, 0f, 0f, mCanvasPaint)
for(path in mPaths)
{
mDrawPaint!!.strokeWidth = path!!.brushThickness
mDrawPaint!!.color = mDrawPath!!.color
canvas.drawPath(path, mDrawPaint!!)
}
if(!mDrawPath!!.isEmpty)
{
mDrawPaint!!.strokeWidth = mDrawPath!!.brushThickness
mDrawPaint!!.color = mDrawPath!!.color
canvas.drawPath(mDrawPath!!, mDrawPaint!!)
}
}
override fun onTouchEvent(event: MotionEvent?): Boolean
{
Log.i("Mahi", "inside onTouchEvent")
val touchX = event?.x
val touchY = event?.y
when(event?.action)
{
MotionEvent.ACTION_DOWN ->
{
mDrawPath!!.color = color
mDrawPath!!.brushThickness = mBrushSize
mDrawPath!!.reset()
if (touchX != null) {
if (touchY != null) {
mDrawPath!!.moveTo(touchX, touchY)
}
}
}
MotionEvent.ACTION_MOVE ->
{
if (touchY != null) {
if (touchX != null) {
mDrawPath!!.lineTo(touchX, touchY)
}
}
}
MotionEvent.ACTION_UP ->
{
mPaths.add(mDrawPath!! )
mDrawPath = CustomPath(color, mBrushSize)
}
else -> return false
}
invalidate()
return true
return super.onTouchEvent(event)
}
internal inner class CustomPath(var color: Int, var brushThickness: Float) : Path()
{
}
}
I have scalable image view - it can be zoomed or scrolled. In this image I able to set points with pin. These points are zoomable and scrollable too with the image. But also I need to draw several circles - the path between pin A and pin B. This path should be zoomable and scrollable too. The color of each circle can be various too. Example:
I creating pins here:
override fun onLongPress(e: MotionEvent) {
super.onLongPress(e)
pinsCoordinates.add(CoordinatesEntity(e.x, e.y))
val touchCoords = floatArrayOf(e.x, e.y)
val matrixInverse = Matrix()
mMatrix!!.invert(matrixInverse) // XY to UV mapping matrix.
matrixInverse.mapPoints(touchCoords) // Touch point in bitmap-U,V coords.
val entity = CoordinatesEntity(touchCoords[0], touchCoords[1])
invertedPinsCoordinates.add(entity)
pinCountersMap["${entity.x}${entity.y}"] = (++pinCounter).toString()
RxBus.publish(IndoorPinsCountChanged(pinsCoordinates.size))
invalidate()
}
And drawing here:
private fun getPinForCoordinates(coordinates: CoordinatesEntity): Bitmap {
val paint = Paint()
paint.style = Paint.Style.FILL
paint.color = Color.parseColor("#1417BF")
val text = pinCountersMap["${coordinates.x}${coordinates.y}"] ?: ""
val copiedBitmapPin = pinBitmap.copy(pinBitmap.config, pinBitmap.isMutable)
val canvas = Canvas(copiedBitmapPin)
val xStart = when (text.length) {
1 -> copiedBitmapPin.width / 2f - 8
2 -> copiedBitmapPin.width / 2f - 12
else -> copiedBitmapPin.width / 2f - 36
}
val yStart = when (text.length) {
1 -> copiedBitmapPin.height / 2f - 2
else -> copiedBitmapPin.height / 2f - 5
}
paint.textSize = when (text.length) {
1 -> 30f
else -> 20f
}
canvas.drawText(text, xStart, yStart, paint)
return BitmapDrawable(context.resources, copiedBitmapPin).bitmap
}
#SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
invertedPinsCoordinates.forEach {
val marker = getPinForCoordinates(it)
val matrixMarker = Matrix()
matrixMarker.setTranslate(it.x, it.y)
matrixMarker.postConcat(mMatrix)
canvas.drawBitmap(marker, matrixMarker, null)
}
}
My whole class:
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.graphics.drawable.BitmapDrawable
import android.util.AttributeSet
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.widget.Toast
import androidx.appcompat.widget.AppCompatImageView
import com.signalsense.signalsenseapp.R
import com.signalsense.signalsenseapp.mvp.models.entities.CoordinatesEntity
import com.signalsense.signalsenseapp.rxbus.RxBus
import com.signalsense.signalsenseapp.rxbus.events.IndoorPinsCountChanged
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt
class ScaleImageView : AppCompatImageView, View.OnTouchListener {
private val dirtyRect = RectF()
private val savedContext: Context
private val maxScale = 2f
private val matrixValues = FloatArray(9)
private var lastTouchX = 0f
private var lastTouchY = 0f
private var paint = Paint()
var tag = "ScaleImageView"
// display width height.
private var mWidth = 0
private var mHeight = 0
private var mIntrinsicWidth = 0
private var mIntrinsicHeight = 0
private var mScale = 0f
private var mMinScale = 1f
private var mPrevDistance = 0f
private var isScaling = false
private var mPrevMoveX = 0
private var mPrevMoveY = 0
private var mDetector: GestureDetector? = null
private val pinsCoordinates = ArrayList<CoordinatesEntity>()
private var invertedPinsCoordinates = ArrayList<CoordinatesEntity>()
private val pinCountersMap = HashMap<String, String>()
private var pinCounter = 0
private lateinit var pinBitmap: Bitmap
constructor(context: Context, attr: AttributeSet?) : super(context, attr) {
savedContext = context
initialize()
}
constructor(context: Context) : super(context) {
savedContext = context
initialize()
}
private fun resetDirtyRect(eventX: Float, eventY: Float) {
dirtyRect.left = min(lastTouchX, eventX)
dirtyRect.right = max(lastTouchX, eventX)
dirtyRect.top = min(lastTouchY, eventY)
dirtyRect.bottom = max(lastTouchY, eventY)
}
override fun setImageBitmap(bm: Bitmap) {
super.setImageBitmap(bm)
initialize()
}
override fun setImageResource(resId: Int) {
super.setImageResource(resId)
initialize()
}
private fun initialize() {
this.scaleType = ScaleType.MATRIX
mMatrix = Matrix()
val d = drawable
paint.isAntiAlias = true
paint.color = Color.RED
paint.style = Paint.Style.STROKE
paint.strokeJoin = Paint.Join.ROUND
paint.strokeWidth = STROKE_WIDTH
if (d != null) {
mIntrinsicWidth = d.intrinsicWidth
mIntrinsicHeight = d.intrinsicHeight
setOnTouchListener(this)
}
mDetector = GestureDetector(savedContext,
object : GestureDetector.SimpleOnGestureListener() {
override fun onDoubleTap(e: MotionEvent): Boolean {
maxZoomTo(e.x.toInt(), e.y.toInt())
cutting()
return super.onDoubleTap(e)
}
override fun onLongPress(e: MotionEvent) {
super.onLongPress(e)
pinsCoordinates.add(CoordinatesEntity(e.x, e.y))
val touchCoords = floatArrayOf(e.x, e.y)
val matrixInverse = Matrix()
mMatrix!!.invert(matrixInverse) // XY to UV mapping matrix.
matrixInverse.mapPoints(touchCoords) // Touch point in bitmap-U,V coords.
val entity = CoordinatesEntity(touchCoords[0], touchCoords[1])
invertedPinsCoordinates.add(entity)
pinCountersMap["${entity.x}${entity.y}"] = (++pinCounter).toString()
RxBus.publish(IndoorPinsCountChanged(pinsCoordinates.size))
invalidate()
}
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
val delta = 25
val pinIndex = pinsCoordinates.indexOfFirst {
val itXStart = it.x - delta
val itXEnd = it.x + delta
val itYStart = it.y - delta
val itYEnd = it.y + delta
val tapX = e?.x ?: 0f
val tapY = e?.y ?: 0f
(tapX in itXStart..itXEnd) && (tapY in itYStart..itYEnd)
}
if (pinIndex >= 0) Toast.makeText(context, pinIndex.toString(), Toast.LENGTH_SHORT).show()
return super.onSingleTapConfirmed(e)
}
})
pinBitmap = BitmapFactory.decodeResource(
context.resources, R.drawable.ic_pin_blue_no_middle_32
).copy(Bitmap.Config.ARGB_8888, true)
}
override fun setFrame(l: Int, t: Int, r: Int, b: Int): Boolean {
mWidth = r - l
mHeight = b - t
mMatrix!!.reset()
val rNorm = r - l
mScale = rNorm.toFloat() / mIntrinsicWidth.toFloat()
var paddingHeight = 0
var paddingWidth = 0
// scaling vertical
if (mScale * mIntrinsicHeight > mHeight) {
mScale = mHeight.toFloat() / mIntrinsicHeight.toFloat()
mMatrix!!.postScale(mScale, mScale)
paddingWidth = (r - mWidth) / 2
paddingHeight = 0
// scaling horizontal
} else {
mMatrix!!.postScale(mScale, mScale)
paddingHeight = (b - mHeight) / 2
paddingWidth = 0
}
mMatrix!!.postTranslate(paddingWidth.toFloat(), paddingHeight.toFloat())
imageMatrix = mMatrix
mMinScale = mScale
Log.i(tag, "MinScale: $mMinScale")
zoomTo(mScale, mWidth / 2, mHeight / 2)
cutting()
return super.setFrame(l, t, r, b)
}
fun deleteLastPin() {
if (pinsCoordinates.isNotEmpty()) pinsCoordinates.removeLast()
if (invertedPinsCoordinates.isNotEmpty()) invertedPinsCoordinates.removeLast()
RxBus.publish(IndoorPinsCountChanged(pinsCoordinates.size))
pinCounter--
invalidate()
}
private fun getValue(matrix: Matrix?, whichValue: Int): Float {
matrix!!.getValues(matrixValues)
return matrixValues[whichValue]
}
private val scale: Float
get() = getValue(mMatrix, Matrix.MSCALE_X)
private val translateX: Float
get() = getValue(mMatrix, Matrix.MTRANS_X)
private val translateY: Float
get() = getValue(mMatrix, Matrix.MTRANS_Y)
private fun maxZoomTo(x: Int, y: Int) {
if (mMinScale != getCalculatedScale() && getCalculatedScale() - mMinScale > 0.1f) {
// threshold 0.1f
val scale = mMinScale / getCalculatedScale()
zoomTo(scale, x, y)
} else {
val scale = maxScale / getCalculatedScale()
zoomTo(scale, x, y)
}
}
private fun zoomTo(scale: Float, x: Int, y: Int) {
if (getCalculatedScale() * scale < mMinScale) {
return
}
if (scale >= 1 && getCalculatedScale() * scale > maxScale) {
return
}
Log.i(tag, "Scale: $scale, multiplied: ${scale * scale}")
mMatrix!!.postScale(scale, scale)
// move to center
mMatrix!!.postTranslate(-(mWidth * scale - mWidth) / 2,
-(mHeight * scale - mHeight) / 2)
// move x and y distance
mMatrix!!.postTranslate(-(x - mWidth / 2) * scale, 0f)
mMatrix!!.postTranslate(0f, -(y - mHeight / 2) * scale)
imageMatrix = mMatrix
}
private fun cutting() {
val width = (mIntrinsicWidth * getCalculatedScale()).toInt()
val height = (mIntrinsicHeight * getCalculatedScale()).toInt()
imageWidth = width
imageHeight = height
if (translateX < -(width - mWidth)) {
mMatrix!!.postTranslate(-(translateX + width - mWidth), 0f)
}
if (translateX > 0) {
mMatrix!!.postTranslate(-translateX, 0f)
}
if (translateY < -(height - mHeight)) {
mMatrix!!.postTranslate(0f, -(translateY + height - mHeight))
}
if (translateY > 0) {
mMatrix!!.postTranslate(0f, -translateY)
}
if (width < mWidth) {
mMatrix!!.postTranslate(((mWidth - width) / 2).toFloat(), 0f)
}
if (height < mHeight) {
mMatrix!!.postTranslate(0f, ((mHeight - height) / 2).toFloat())
}
imageMatrix = mMatrix
}
private fun distance(x0: Float, x1: Float, y0: Float, y1: Float): Float {
val x = x0 - x1
val y = y0 - y1
return sqrt((x * x + y * y).toDouble()).toFloat()
}
private fun dispDistance(): Float {
return sqrt((mWidth * mWidth + mHeight * mHeight).toDouble()).toFloat()
}
fun clear() {
path.reset()
invalidate()
}
fun save() {
val returnedBitmap = Bitmap.createBitmap(
width,
height,
Bitmap.Config.ARGB_8888)
val canvas = Canvas(returnedBitmap)
draw(canvas)
setImageBitmap(returnedBitmap)
}
#Suppress("DEPRECATION")
#SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
if (mDetector!!.onTouchEvent(event)) {
return true
}
val touchCount = event.pointerCount
when (event.action) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_1_DOWN, MotionEvent.ACTION_POINTER_2_DOWN -> {
if (touchCount >= 2) {
val distance = distance(event.getX(0), event.getX(1),
event.getY(0), event.getY(1))
mPrevDistance = distance
isScaling = true
} else {
mPrevMoveX = event.x.toInt()
mPrevMoveY = event.y.toInt()
}
if (touchCount >= 2 && isScaling) {
val dist = distance(event.getX(0), event.getX(1),
event.getY(0), event.getY(1))
var scale = (dist - mPrevDistance) / dispDistance()
mPrevDistance = dist
scale += 1f
scale *= scale
zoomTo(scale, mWidth / 2, mHeight / 2)
cutting()
} else if (!isScaling) {
val distanceX = mPrevMoveX - event.x.toInt()
val distanceY = mPrevMoveY - event.y.toInt()
mPrevMoveX = event.x.toInt()
mPrevMoveY = event.y.toInt()
mMatrix!!.postTranslate(-distanceX.toFloat(), -distanceY.toFloat())
cutting()
}
}
MotionEvent.ACTION_MOVE -> if (touchCount >= 2 && isScaling) {
val dist = distance(event.getX(0), event.getX(1),
event.getY(0), event.getY(1))
var scale = (dist - mPrevDistance) / dispDistance()
mPrevDistance = dist
scale += 1f
scale *= scale
zoomTo(scale, mWidth / 2, mHeight / 2)
cutting()
} else if (!isScaling) {
val distanceX = mPrevMoveX - event.x.toInt()
val distanceY = mPrevMoveY - event.y.toInt()
mPrevMoveX = event.x.toInt()
mPrevMoveY = event.y.toInt()
mMatrix!!.postTranslate(-distanceX.toFloat(), -distanceY.toFloat())
cutting()
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_POINTER_2_UP -> if (event.pointerCount <= 1) {
isScaling = false
}
}
return true
}
private fun getCalculatedScale() = getValue(mMatrix, Matrix.MSCALE_X)
private fun getPinForCoordinates(coordinates: CoordinatesEntity): Bitmap {
val paint = Paint()
paint.style = Paint.Style.FILL
paint.color = Color.parseColor("#1417BF")
val text = pinCountersMap["${coordinates.x}${coordinates.y}"] ?: ""
val copiedBitmapPin = pinBitmap.copy(pinBitmap.config, pinBitmap.isMutable)
val canvas = Canvas(copiedBitmapPin)
val xStart = when (text.length) {
1 -> copiedBitmapPin.width / 2f - 8
2 -> copiedBitmapPin.width / 2f - 12
else -> copiedBitmapPin.width / 2f - 36
}
val yStart = when (text.length) {
1 -> copiedBitmapPin.height / 2f - 2
else -> copiedBitmapPin.height / 2f - 5
}
paint.textSize = when (text.length) {
1 -> 30f
else -> 20f
}
canvas.drawText(text, xStart, yStart, paint)
return BitmapDrawable(context.resources, copiedBitmapPin).bitmap
}
#SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
invertedPinsCoordinates.forEach {
val marker = getPinForCoordinates(it)
val matrixMarker = Matrix()
matrixMarker.setTranslate(it.x, it.y)
matrixMarker.postConcat(mMatrix)
canvas.drawBitmap(marker, matrixMarker, null)
}
}
#SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View, event: MotionEvent): Boolean {
return super.onTouchEvent(event)
}
companion object {
const val STROKE_WIDTH = 10f
const val HALF_STROKE_WIDTH = STROKE_WIDTH / 2
var path = Path()
var imageHeight = 0
var imageWidth = 0
private var mMatrix: Matrix? = null
}
}
How to create this path from circles? And make it scrollable and zoomable with other elements (picture and pins, that alreasy scrollable and zoomable)? Please help!
If anybody interested, I found the solution. This is the algorithm to get middle points between poina a and point b:
private fun getPointsToDraw(a: CoordinatesEntity, b: CoordinatesEntity): List<CoordinatesEntity> {
val points = ArrayList<CoordinatesEntity>()
val numberOfPoints = 5
val stepX = (b.x - a.x) / numberOfPoints
val stepY = (b.y - a.y) / numberOfPoints
points.add(a)
for (i in 0 until numberOfPoints) {
val lastPoint = points.last()
points.add(CoordinatesEntity(lastPoint.x + stepX, lastPoint.y + stepY))
}
points.add(b)
return points
}
data class:
data class CoordinatesEntity(var x: Float, var y: Float)
Of course we can get only fixed points between, cause there is indefinite count.
Here is how to write them. I using prepared bitmaps in array (here is I took 4th index from this bitmap):
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
invertedPinsCoordinates.forEach {
val marker = getPinForCoordinates(it)
val matrixMarker = Matrix()
matrixMarker.setTranslate(it.x, it.y)
matrixMarker.postConcat(mMatrix)
canvas.drawBitmap(marker, matrixMarker, null)
}
//Starting to draw path with points
if(invertedPinsCoordinates.size > 1) {
val points = getPointsToDraw(invertedPinsCoordinates[invertedPinsCoordinates.size - 2],
invertedPinsCoordinates.last())
points.forEach {
val marker = levelsBitmapArray[4]
val matrixMarker = Matrix()
matrixMarker.setTranslate(it.x, it.y)
matrixMarker.postConcat(mMatrix)
canvas.drawBitmap(marker, matrixMarker, null)
}
}
}
I want to add to the image several pins at once. This image can zoom and scroll. But when I'm adding many pins (10 and more), the image starting to freezing when I zooming or scrolling it.
I'm adding new pin in "onLongPress":
data class CoordinatesEntity(var x: Float, var y: Float)
class ScaleImageView : AppCompatImageView, View.OnTouchListener {
...
private val pinsCoordinates = ArrayList<CoordinatesEntity>()
private var invertedPinsCoordinates = ArrayList<CoordinatesEntity>()
private val pinCountersMap = HashMap<String, String>()
...
mDetector = GestureDetector(savedContext,
object : GestureDetector.SimpleOnGestureListener() {
override fun onDoubleTap(e: MotionEvent): Boolean {
...
}
override fun onLongPress(e: MotionEvent) {
super.onLongPress(e)
pinsCoordinates.add(CoordinatesEntity(e.x, e.y))
val touchCoords = floatArrayOf(e.x, e.y)
val matrixInverse = Matrix()
mMatrix!!.invert(matrixInverse) // XY to UV mapping matrix.
matrixInverse.mapPoints(touchCoords) // Touch point in bitmap-U,V coords.
val entity = CoordinatesEntity(touchCoords[0], touchCoords[1])
invertedPinsCoordinates.add(entity)
pinCountersMap["${entity.x}${entity.y}"] = (++pinCounter).toString()
invalidate()
}
})
...
}
and drawing all of my pins in "onDraw". My pin - it's image plus text, that I adding programmatically to bitmap with pin:
class ScaleImageView : AppCompatImageView, View.OnTouchListener {
...
private fun getPinForCoordinates(coordinates: CoordinatesEntity): Bitmap {
val bm: Bitmap = BitmapFactory.decodeResource(
context.resources, R.drawable.ic_pin_raster_no_middle_32
).copy(Bitmap.Config.ARGB_8888, true)
val paint = Paint()
paint.style = Paint.Style.FILL
paint.color = Color.parseColor("#3874A4")
paint.textSize = 20f
val text = pinCountersMap["${coordinates.x}${coordinates.y}"] ?: ""
val canvas = Canvas(bm)
val xStart = when (text.length) {
1 -> bm.width / 2f - 6
2 -> bm.width / 2f - 12
else -> bm.width / 2f - 36
}
canvas.drawText(text, xStart, bm.height / 2f, paint)
return BitmapDrawable(context.resources, bm).bitmap
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
invertedPinsCoordinates.forEach {
val marker = getPinForCoordinates(it)
val matrixMarker = Matrix()
matrixMarker.setTranslate(it.x, it.y)
matrixMarker.postConcat(mMatrix)
canvas.drawBitmap(marker, matrixMarker, null)
}
}
...
}
I think it's freezing because of the loop inside of "onDraw". But I don't know how to draw several pins at once and keep them zoomable and scrollable with image. Please help!
My whole class:
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.graphics.drawable.BitmapDrawable
import android.util.AttributeSet
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import androidx.appcompat.widget.AppCompatImageView
import com.signalsense.signalsenseapp.R
import com.signalsense.signalsenseapp.interactors.image_interactor.ImageInteractor
import com.signalsense.signalsenseapp.interactors.image_interactor.ImageInteractorImpl
import com.signalsense.signalsenseapp.mvp.models.entities.CoordinatesEntity
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt
class ScaleImageView : AppCompatImageView, View.OnTouchListener {
private val dirtyRect = RectF()
private val savedContext: Context
private val maxScale = 2f
private val matrixValues = FloatArray(9)
private var lastTouchX = 0f
private var lastTouchY = 0f
private var paint = Paint()
var tag = "ScaleImageView"
// display width height.
private var mWidth = 0
private var mHeight = 0
private var mIntrinsicWidth = 0
private var mIntrinsicHeight = 0
private var mScale = 0f
private var mMinScale = 1f
private var mPrevDistance = 0f
private var isScaling = false
private var mPrevMoveX = 0
private var mPrevMoveY = 0
private var mDetector: GestureDetector? = null
private val pinsCoordinates = ArrayList<CoordinatesEntity>()
private var invertedPinsCoordinates = ArrayList<CoordinatesEntity>()
private var pinCounter = 0
private val pinCountersMap = HashMap<String, String>()
constructor(context: Context, attr: AttributeSet?) : super(context, attr) {
savedContext = context
initialize()
}
constructor(context: Context) : super(context) {
savedContext = context
initialize()
}
private fun resetDirtyRect(eventX: Float, eventY: Float) {
dirtyRect.left = min(lastTouchX, eventX)
dirtyRect.right = max(lastTouchX, eventX)
dirtyRect.top = min(lastTouchY, eventY)
dirtyRect.bottom = max(lastTouchY, eventY)
}
override fun setImageBitmap(bm: Bitmap) {
super.setImageBitmap(bm)
initialize()
}
override fun setImageResource(resId: Int) {
super.setImageResource(resId)
initialize()
}
private fun initialize() {
this.scaleType = ScaleType.MATRIX
mMatrix = Matrix()
val d = drawable
paint.isAntiAlias = true
paint.color = Color.RED
paint.style = Paint.Style.STROKE
paint.strokeJoin = Paint.Join.ROUND
paint.strokeWidth = STROKE_WIDTH
if (d != null) {
mIntrinsicWidth = d.intrinsicWidth
mIntrinsicHeight = d.intrinsicHeight
setOnTouchListener(this)
}
mDetector = GestureDetector(savedContext,
object : GestureDetector.SimpleOnGestureListener() {
override fun onDoubleTap(e: MotionEvent): Boolean {
maxZoomTo(e.x.toInt(), e.y.toInt())
cutting()
return super.onDoubleTap(e)
}
override fun onLongPress(e: MotionEvent) {
super.onLongPress(e)
pinsCoordinates.add(CoordinatesEntity(e.x, e.y))
val touchCoords = floatArrayOf(e.x, e.y)
val matrixInverse = Matrix()
mMatrix!!.invert(matrixInverse) // XY to UV mapping matrix.
matrixInverse.mapPoints(touchCoords) // Touch point in bitmap-U,V coords.
val entity = CoordinatesEntity(touchCoords[0], touchCoords[1])
invertedPinsCoordinates.add(entity)
pinCountersMap["${entity.x}${entity.y}"] = (++pinCounter).toString()
invalidate()
}
})
}
override fun setFrame(l: Int, t: Int, r: Int, b: Int): Boolean {
mWidth = r - l
mHeight = b - t
mMatrix!!.reset()
val rNorm = r - l
mScale = rNorm.toFloat() / mIntrinsicWidth.toFloat()
var paddingHeight = 0
var paddingWidth = 0
// scaling vertical
if (mScale * mIntrinsicHeight > mHeight) {
mScale = mHeight.toFloat() / mIntrinsicHeight.toFloat()
mMatrix!!.postScale(mScale, mScale)
paddingWidth = (r - mWidth) / 2
paddingHeight = 0
// scaling horizontal
} else {
mMatrix!!.postScale(mScale, mScale)
paddingHeight = (b - mHeight) / 2
paddingWidth = 0
}
mMatrix!!.postTranslate(paddingWidth.toFloat(), paddingHeight.toFloat())
imageMatrix = mMatrix
mMinScale = mScale
Log.i(tag, "MinScale: $mMinScale")
zoomTo(mScale, mWidth / 2, mHeight / 2)
cutting()
return super.setFrame(l, t, r, b)
}
private fun getValue(matrix: Matrix?, whichValue: Int): Float {
matrix!!.getValues(matrixValues)
return matrixValues[whichValue]
}
private val scale: Float
get() = getValue(mMatrix, Matrix.MSCALE_X)
private val translateX: Float
get() = getValue(mMatrix, Matrix.MTRANS_X)
private val translateY: Float
get() = getValue(mMatrix, Matrix.MTRANS_Y)
private fun maxZoomTo(x: Int, y: Int) {
if (mMinScale != getCalculatedScale() && getCalculatedScale() - mMinScale > 0.1f) {
// threshold 0.1f
val scale = mMinScale / getCalculatedScale()
zoomTo(scale, x, y)
} else {
val scale = maxScale / getCalculatedScale()
zoomTo(scale, x, y)
}
}
private fun zoomTo(scale: Float, x: Int, y: Int) {
if (getCalculatedScale() * scale < mMinScale) {
return
}
if (scale >= 1 && getCalculatedScale() * scale > maxScale) {
return
}
Log.i(tag, "Scale: $scale, multiplied: ${scale * scale}")
mMatrix!!.postScale(scale, scale)
// move to center
mMatrix!!.postTranslate(-(mWidth * scale - mWidth) / 2,
-(mHeight * scale - mHeight) / 2)
// move x and y distance
mMatrix!!.postTranslate(-(x - mWidth / 2) * scale, 0f)
mMatrix!!.postTranslate(0f, -(y - mHeight / 2) * scale)
imageMatrix = mMatrix
}
private fun cutting() {
val width = (mIntrinsicWidth * getCalculatedScale()).toInt()
val height = (mIntrinsicHeight * getCalculatedScale()).toInt()
imageWidth = width
imageHeight = height
if (translateX < -(width - mWidth)) {
mMatrix!!.postTranslate(-(translateX + width - mWidth), 0f)
}
if (translateX > 0) {
mMatrix!!.postTranslate(-translateX, 0f)
}
if (translateY < -(height - mHeight)) {
mMatrix!!.postTranslate(0f, -(translateY + height - mHeight))
}
if (translateY > 0) {
mMatrix!!.postTranslate(0f, -translateY)
}
if (width < mWidth) {
mMatrix!!.postTranslate(((mWidth - width) / 2).toFloat(), 0f)
}
if (height < mHeight) {
mMatrix!!.postTranslate(0f, ((mHeight - height) / 2).toFloat())
}
imageMatrix = mMatrix
}
private fun distance(x0: Float, x1: Float, y0: Float, y1: Float): Float {
val x = x0 - x1
val y = y0 - y1
return sqrt((x * x + y * y).toDouble()).toFloat()
}
private fun dispDistance(): Float {
return sqrt((mWidth * mWidth + mHeight * mHeight).toDouble()).toFloat()
}
fun clear() {
path.reset()
invalidate()
}
fun save() {
val returnedBitmap = Bitmap.createBitmap(
width,
height,
Bitmap.Config.ARGB_8888)
val canvas = Canvas(returnedBitmap)
draw(canvas)
setImageBitmap(returnedBitmap)
}
#Suppress("DEPRECATION")
#SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
if (mDetector!!.onTouchEvent(event)) {
return true
}
val touchCount = event.pointerCount
when (event.action) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_1_DOWN, MotionEvent.ACTION_POINTER_2_DOWN -> {
if (touchCount >= 2) {
val distance = distance(event.getX(0), event.getX(1),
event.getY(0), event.getY(1))
mPrevDistance = distance
isScaling = true
} else {
mPrevMoveX = event.x.toInt()
mPrevMoveY = event.y.toInt()
}
if (touchCount >= 2 && isScaling) {
val dist = distance(event.getX(0), event.getX(1),
event.getY(0), event.getY(1))
var scale = (dist - mPrevDistance) / dispDistance()
mPrevDistance = dist
scale += 1f
scale *= scale
zoomTo(scale, mWidth / 2, mHeight / 2)
cutting()
} else if (!isScaling) {
val distanceX = mPrevMoveX - event.x.toInt()
val distanceY = mPrevMoveY - event.y.toInt()
mPrevMoveX = event.x.toInt()
mPrevMoveY = event.y.toInt()
mMatrix!!.postTranslate(-distanceX.toFloat(), -distanceY.toFloat())
cutting()
}
}
MotionEvent.ACTION_MOVE -> if (touchCount >= 2 && isScaling) {
val dist = distance(event.getX(0), event.getX(1),
event.getY(0), event.getY(1))
var scale = (dist - mPrevDistance) / dispDistance()
mPrevDistance = dist
scale += 1f
scale *= scale
zoomTo(scale, mWidth / 2, mHeight / 2)
cutting()
} else if (!isScaling) {
val distanceX = mPrevMoveX - event.x.toInt()
val distanceY = mPrevMoveY - event.y.toInt()
mPrevMoveX = event.x.toInt()
mPrevMoveY = event.y.toInt()
mMatrix!!.postTranslate(-distanceX.toFloat(), -distanceY.toFloat())
cutting()
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_POINTER_2_UP -> if (event.pointerCount <= 1) {
isScaling = false
}
}
return true
}
private fun getCalculatedScale() = getValue(mMatrix, Matrix.MSCALE_X)
private fun getPinForCoordinates(coordinates: CoordinatesEntity): Bitmap {
val bm: Bitmap = BitmapFactory.decodeResource(
context.resources, R.drawable.ic_pin_raster_no_middle_32
).copy(Bitmap.Config.ARGB_8888, true)
val paint = Paint()
paint.style = Paint.Style.FILL
paint.color = Color.parseColor("#3874A4")
paint.textSize = 20f
val text = pinCountersMap["${coordinates.x}${coordinates.y}"] ?: ""
val canvas = Canvas(bm)
val xStart = when (text.length) {
1 -> bm.width / 2f - 6
2 -> bm.width / 2f - 12
else -> bm.width / 2f - 36
}
canvas.drawText(text, xStart, bm.height / 2f, paint)
return BitmapDrawable(context.resources, bm).bitmap
}
#SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
invertedPinsCoordinates.forEach {
val marker = getPinForCoordinates(it)
val matrixMarker = Matrix()
matrixMarker.setTranslate(it.x, it.y)
matrixMarker.postConcat(mMatrix)
canvas.drawBitmap(marker, matrixMarker, null)
}
}
#SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View, event: MotionEvent): Boolean {
return super.onTouchEvent(event)
}
companion object {
const val STROKE_WIDTH = 10f
const val HALF_STROKE_WIDTH = STROKE_WIDTH / 2
var path = Path()
var imageHeight = 0
var imageWidth = 0
private var mMatrix: Matrix? = null
}
}
I understand - the reason not in the loop mainly, but in "BitmapFactory.decodeResource". Here is we're reading the file from the disk - it's long operation and if we keep it in the memory, without decoding from resources again and again, everything will be ok.
I have to make following Ui, In which when I play this small circle will move in an arc, when I stop it will stop. I can also set the timing of rotation of the small blue circle.
Till now I have implemented the following code: this gives me an arc of the circle, but I am not able to rotate the smaller circler over the bigger one.
public class CustoCustomProgressBar : View{
private var path : Path ? =null
private var mPrimaryPaint: Paint? = null
private var mSecondaryPaint: Paint? = null
private var mBackgroundPaint: Paint? = null
private var mTextPaint: TextPaint? = null
private var mProgressDrawable : Drawable ? = null
private var mRectF: RectF? = null
private var mDrawText = false
private var mTextColor = 0
private var mSecondaryProgressColor = 0
private var mPrimaryProgressColor = 0
private var mBackgroundColor = 0
private var mStrokeWidth = 0
private var mProgress = 0
var secodaryProgress = 0
private set
private var firstArcprogress = 0
private var secondArcProgress = 0
private var thirdArcProgress = 0
private var mFristArcCapSize = 0
private var mSecondArcCapSize = 0
private var mThirdArcCapSize = 0
private var isFristCapVisible = false
private var isSecondCapVisible = false
private var isThirdCapVisible = false
private var capColor = 0
private var mPrimaryCapSize = 0
private var mSecondaryCapSize = 0
var isPrimaryCapVisible = false
var isSecondaryCapVisible = false
private var x = 0
private var y = 0
private var mWidth = 0
private var mHeight = 0
constructor(context: Context) : super(context) {
init(context, null)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
init(context, attrs)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
init(context, attrs)
}
fun init(context: Context, attrs: AttributeSet?) {
val a: TypedArray
a = if (attrs != null) {
context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.CustomProgressBar,
0, 0
)
} else {
throw IllegalArgumentException("Must have to pass the attributes")
}
try {
mProgressDrawable = resources.getDrawable(R.drawable.test)
mDrawText = a.getBoolean(R.styleable.CustomProgressBar_showProgressText, false)
mBackgroundColor =
a.getColor(
R.styleable.CustomProgressBar_backgroundColor,
resources.getColor(R.color.white)
)
mPrimaryProgressColor =
a.getColor(
R.styleable.CustomProgressBar_progressColor,
resources.getColor(R.color.white)
)
mSecondaryProgressColor =
a.getColor(
R.styleable.CustomProgressBar_secondaryProgressColor,
resources.getColor(R.color.black)
)
capColor =
a.getColor(
R.styleable.CustomProgressBar_capColor,
resources.getColor(R.color.color_9bc6e6_mind)
)
firstArcprogress =a.getInt(R.styleable.CustomProgressBar_firstArcProgress, 0)
mProgress = a.getInt(R.styleable.CustomProgressBar_progress, 0)
secodaryProgress = a.getInt(R.styleable.CustomProgressBar_secondaryProgress, 0)
mStrokeWidth = a.getDimensionPixelSize(R.styleable.CustomProgressBar_strokeWidth, 10)
mTextColor = a.getColor(
R.styleable.CustomProgressBar_textColor,
resources.getColor(R.color.black)
)
mPrimaryCapSize = a.getInt(R.styleable.CustomProgressBar_primaryCapSize, 20)
mSecondaryCapSize = a.getInt(R.styleable.CustomProgressBar_secodaryCapSize, 20)
isPrimaryCapVisible =
a.getBoolean(R.styleable.CustomProgressBar_primaryCapVisibility, true)
isSecondaryCapVisible =
a.getBoolean(R.styleable.CustomProgressBar_secodaryCapVisibility, true)
isFristCapVisible = a.getBoolean(R.styleable.CustomProgressBar_firstCapVisibility, true)
isSecondCapVisible =
a.getBoolean(R.styleable.CustomProgressBar_secodaryCapVisibility, false)
isThirdCapVisible =
a.getBoolean(R.styleable.CustomProgressBar_thirdCapVisibility, false)
} finally {
a.recycle()
}
mBackgroundPaint = Paint()
mBackgroundPaint!!.setAntiAlias(true)
mBackgroundPaint!!.setStyle(Paint.Style.STROKE)
mBackgroundPaint!!.setStrokeWidth(mStrokeWidth.toFloat())
mBackgroundPaint!!.setColor(mBackgroundColor)
mPrimaryPaint = Paint()
mPrimaryPaint!!.setAntiAlias(true)
mPrimaryPaint!!.setStyle(Paint.Style.STROKE)
mPrimaryPaint!!.setStrokeWidth(mStrokeWidth.toFloat())
mPrimaryPaint!!.setColor(capColor)
mSecondaryPaint = Paint()
mSecondaryPaint!!.setAntiAlias(true)
mSecondaryPaint!!.setStyle(Paint.Style.STROKE)
mSecondaryPaint!!.setStrokeWidth((mStrokeWidth - 2).toFloat())
mSecondaryPaint!!.setColor(mSecondaryProgressColor)
mTextPaint = TextPaint()
mTextPaint!!.color = mTextColor
mRectF = RectF()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
mRectF!![paddingLeft.toFloat(), paddingTop.toFloat(), (w - paddingRight).toFloat()] =
(h - paddingBottom).toFloat()
mTextPaint!!.textSize = (w / 5).toFloat()
x = w / 2 - (mTextPaint!!.measureText("$mProgress%") / 2).toInt()
y = (h / 2 - (mTextPaint!!.descent() + mTextPaint!!.ascent()) / 2).toInt()
mWidth = w
mHeight = h
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
mPrimaryPaint?.setStyle(Paint.Style.STROKE)
mSecondaryPaint?.setStyle(Paint.Style.STROKE)
// for drawing a full progress .. The background circle
mRectF?.let {
mBackgroundPaint?.let { it1 -> canvas.drawArc(it, 270F, 180F, false, it1) } }
path = Path()
path?.arcTo(mRectF,270F,180F,true)
mRectF?.let {
mBackgroundPaint?.let { it1 ->
canvas.drawArc(it, 95F, 80F, false, it1)
}
}
mRectF?.let {
mBackgroundPaint?.let { it1 ->
canvas.drawArc(it, 180F, 80F, false, it1)
}
}
// // for drawing a secondary progress circle
// val secondarySwipeangle = secodaryProgress * 360 / 100
// mRectF?.let { mSecondaryPaint?.let { it1 ->
// canvas.drawArc(it, 270F, secondarySwipeangle.toFloat(), false,
// it1
// )
// } }
//
val firstArcProgresSwipeAngle = firstArcprogress * 180 / 100
// mRectF?.let {
// mPrimaryPaint?.let { it1 ->
// canvas.drawArc(
// it, 270F, firstArcProgresSwipeAngle.toFloat(), false,
// it1
// )
// }
// }
// // for drawing a main progress circle
// val primarySwipeangle = mProgress * 360 / 100
// mRectF?.let { mPrimaryPaint?.let { it1 ->
// canvas.drawArc(it, 270F, primarySwipeangle.toFloat(), false,
// it1
// )
// } }
// // for cap of secondary progress
// val r = (height - paddingLeft * 2) / 2 // Calculated from canvas width
// var trad = (secondarySwipeangle - 90) * (Math.PI / 180.0) // = 5.1051
// var x = (r * Math.cos(trad)).toInt()
// var y = (r * Math.sin(trad)).toInt()
// mSecondaryPaint?.setStyle(Paint.Style.FILL)
// if (isSecondaryCapVisible) mSecondaryPaint?.let {
// canvas.drawCircle(
// (x + mWidth / 2).toFloat(),
// (y + mHeight / 2).toFloat(),
// mSecondaryCapSize.toFloat(),
// it
// )
// }
val r = (height - paddingLeft * 2) / 2
// for cap of primary progress
var trad = (firstArcProgresSwipeAngle - 90) * (Math.PI / 180.0) // = 5.1051
x = (r * Math.cos(trad)).toInt()
y = (r * Math.sin(trad)).toInt()
mPrimaryPaint?.setStyle(Paint.Style.FILL)
if (isPrimaryCapVisible) mPrimaryPaint?.let {
canvas.drawCircle(
(x + mWidth / 2).toFloat(),
(y + mHeight / 2).toFloat(),
mPrimaryCapSize.toFloat(),
it
)
}
if (mDrawText) mTextPaint?.let {
canvas.drawText(
"$mProgress%", x.toFloat(), y.toFloat(),
it
)
}
}
fun setDrawText(mDrawText: Boolean) {
this.mDrawText = mDrawText
invalidate()
}
override fun setBackgroundColor(mBackgroundColor: Int) {
this.mBackgroundColor = mBackgroundColor
mBackgroundPaint?.setColor(mBackgroundColor)
invalidate()
}
fun setStrokeWidth(mStrokeWidth: Int) {
this.mStrokeWidth = mStrokeWidth
invalidate()
}
fun setSecondaryProgress(mSecondaryProgress: Int) {
secodaryProgress = mSecondaryProgress
invalidate()
}
fun setTextColor(mTextColor: Int) {
this.mTextColor = mTextColor
mTextPaint!!.color = mTextColor
invalidate()
}
var secondaryProgressColor: Int
get() = mSecondaryProgressColor
set(mSecondaryProgressColor) {
this.mSecondaryProgressColor = mSecondaryProgressColor
mSecondaryPaint?.setColor(mSecondaryProgressColor)
invalidate()
}
var primaryProgressColor: Int
get() = mPrimaryProgressColor
set(mPrimaryProgressColor) {
this.mPrimaryProgressColor = mPrimaryProgressColor
mPrimaryPaint?.setColor(mPrimaryProgressColor)
invalidate()
}
var progress: Int
get() = mProgress
set(mProgress) {
while (this.mProgress <= mProgress) {
postInvalidateDelayed(150)
this.mProgress++
}
}
fun getBackgroundColor(): Int {
return mBackgroundColor
}
var primaryCapSize: Int
get() = mPrimaryCapSize
set(mPrimaryCapSize) {
this.mPrimaryCapSize = mPrimaryCapSize
invalidate()
}
var secondaryCapSize: Int
get() = mSecondaryCapSize
set(mSecondaryCapSize) {
this.mSecondaryCapSize = mSecondaryCapSize
invalidate()
}
var arcprogress: Int
get() = firstArcprogress
set(firstArcprogress) {
while (this.firstArcprogress <= firstArcprogress) {
postInvalidateDelayed(150)
this.firstArcprogress = firstArcprogress
}
}
fun getPath(): Path? {
return path
}
fun getXCOORDINTE(): Float{
return x.toFloat()
}
fun getYCoordinate(): Float{
return y.toFloat()
}
}
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)
}