Comparing two user-drawn Canvas lines in Kotlin - android

I need to compare, for example, two straight lines. One of these lines will be set by me initially (or drawn), will have certain coordinates and thickness. The second one will be drawn by the user. Finally, I need to see the result of comparing these lines as a percentage. Can it be implement like in the following code below? I had an idea to draw my own line, put its coordinates from Path into an array somehow, and then compare it with the new one. But i don't know.
And, and yet, can you tell me how to make the background transparent? I am not satisfied with the replacement of a white background with a static picture, the animation will take place in the far background
Thank you!
enter image description here
import android.content.Context
import android.graphics.*
import android.util.Log
import android.view.View
import android.view.MotionEvent
import android.view.ViewConfiguration
import androidx.core.content.res.ResourcesCompat
class SomeDraw(context: Context) : View(context) {
// Holds the path you are currently drawing.
private var path = Path()
private val drawColor = ResourcesCompat.getColor(resources, R.color.purple_700, null)
private val backgroundColor = ResourcesCompat.getColor(resources, R.color.transparent, null)
private lateinit var extraCanvas: Canvas
private lateinit var extraBitmap: Bitmap
private lateinit var frame: Rect
// Set up the paint with which to draw.
private val paint = Paint().apply {
color = drawColor
// Smooths out edges of what is drawn without affecting shape.
isAntiAlias = true
// Dithering affects how colors with higher-precision than the device are down-sampled.
isDither = true
style = Paint.Style.STROKE // default: FILL
strokeJoin = Paint.Join.ROUND // default: MITER
strokeCap = Paint.Cap.ROUND // default: BUTT
strokeWidth = 70.0F // default: Hairline-width (really thin)
}
/**
* Don't draw every single pixel.
* If the finger has has moved less than this distance, don't draw. scaledTouchSlop, returns
* the distance in pixels a touch can wander before we think the user is scrolling.
*/
private val touchTolerance = ViewConfiguration.get(context).scaledTouchSlop
private var currentX = 0f
private var currentY = 0f
private var motionTouchEventX = 0f
private var motionTouchEventY = 0f
/**
* Called whenever the view changes size.
* Since the view starts out with no size, this is also called after
* the view has been inflated and has a valid size.
*/
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)
// Calculate a rectangular frame around the picture.
val inset = 0
frame = Rect(inset, inset, width - inset, height - inset)
}
override fun onDraw(canvas: Canvas) {
// Draw the bitmap that has the saved path.
canvas.drawBitmap(extraBitmap, 0f, 0f, null)
// Draw a frame around the canvas.
extraCanvas.drawRect(frame, paint)
}
/**
* No need to call and implement MyCanvasView#performClick, because MyCanvasView custom view
* does not handle click actions.
*/
override fun onTouchEvent(event: MotionEvent): Boolean {
motionTouchEventX = event.x
motionTouchEventY = event.y
when (event.action) {
MotionEvent.ACTION_DOWN -> touchStart()
MotionEvent.ACTION_MOVE -> touchMove()
MotionEvent.ACTION_UP -> touchUp()
}
return true
}
/**
* The following methods factor out what happens for different touch events,
* as determined by the onTouchEvent() when statement.
* This keeps the when conditional block
* concise and makes it easier to change what happens for each event.
* No need to call invalidate because we are not drawing anything.
*/
private fun touchStart() {
path.reset()
path.moveTo(motionTouchEventX, motionTouchEventY)
currentX = motionTouchEventX
currentY = motionTouchEventY
}
private fun touchMove() {
val dx = Math.abs(motionTouchEventX - currentX)
val dy = Math.abs(motionTouchEventY - currentY)
if (dx >= touchTolerance || dy >= touchTolerance) {
// QuadTo() adds a quadratic bezier from the last point,
// approaching control point (x1,y1), and ending at (x2,y2).
path.quadTo(currentX, currentY, (motionTouchEventX + currentX) / 2, (motionTouchEventY + currentY) / 2)
currentX = motionTouchEventX
currentY = motionTouchEventY
// Draw the path in the extra bitmap to save it.
extraCanvas.drawPath(path, paint)
val pathTrue = path
Log.d("AAA", ""+pathTrue.equals(path))
}
// Invalidate() is inside the touchMove() under ACTION_MOVE because there are many other
// types of motion events passed into this listener, and we don't want to invalidate the
// view for those.
invalidate()
}
private fun touchUp() {
// Reset the path so it doesn't get drawn again.
path.reset()
}
}
I tried to work with Path but nothing worked. Tried various ways to set transparency to the background (color, apply alpha, delete) - nothing helped

Related

Custom view will not update if run after onViewCreated()

I have built a custom view class called progressCircle which is a snapchat-like, circular progress bar - this is inside a constraint layout, over a circular button.
This view has parameter angle which when called from onViewCreated(), will work perfectly if run
progressCircle.angle = 100f
However, I am trying to animate this onClick. If I run this same code, onClick, the progressCircle will not show up?! After trial and error, I found that updating the background colour here made the view visible & it was updated. IE;
button.setOnClickListener {
progressCircle.setBackgroundColor(android.R.color.transparent)
progressCircle.angle = 270f
}
Whats going on here, and how can I fix this so I can animate it properly...
Edit:
class ProgressCircle(context: Context, attrs: AttributeSet) : View(context, attrs) {
private val paint: Paint
private val rect: RectF
private val fillPaint: Paint
private val fillRect: RectF
var angle: Float
var startAngle: Float = 0f
init {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ProgressCircle)
angle = typedArray.getFloat(R.styleable.ProgressCircle_angle, 0f)
typedArray.recycle()
val offsetAngle = 0f
val color = getColor(context, R.color.outputON)
val strokeWidth = 15f
val circleSize = 276f
paint = Paint().apply {
setAntiAlias(true)
setStyle(Paint.Style.STROKE)
setStrokeWidth(strokeWidth)
setColor(color)
}
rect = RectF(
strokeWidth,
strokeWidth,
(circleSize - strokeWidth),
(circleSize - strokeWidth)
)
fillPaint = Paint().apply {
setAntiAlias(true)
setStyle(Paint.Style.FILL)
setColor(getColor(context, R.color.flat_blue_1))
}
val offsetFill = strokeWidth
fillRect = RectF(
offsetFill,
offsetFill,
(circleSize - offsetFill),
(circleSize - offsetFill)
)
//Initial Angle (optional, it can be zero)
angle = offsetAngle
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (getColor(context, R.color.flat_blue_1) > 0) {
canvas.drawArc(rect, 0f, 360f, false, fillPaint)
}
canvas.drawArc(rect, startAngle, angle, false, paint)
}
}
TIA
By default, a View only redraws itself when something has changed - i.e., when the view is "invalidated" as per the View documentation:
Drawing is handled by walking the tree and recording the drawing commands of any View that needs to update. After this, the drawing commands of the entire tree are issued to screen, clipped to the newly damaged area.
If you want your custom view to redraw itself when the angle property is changed, you need to call invalidate():
var angle: Float
set(value) {
// Do the default behavior of setting the value
field = value
// Then call invalidate() to force a redraw
invalidate()
}
}

Draw circle under finger android canvas

Good day!
I faced such a problem that I need to draw a circle under the touch of my finger.
I tried to use the following code:
private fun pressFinger() {
mFirst.setOnTouchListener(View.OnTouchListener { v, event ->
val x = event.x
val y = event.y
when (event.action) {
MotionEvent.ACTION_DOWN -> {
drawCircle(x, y)
}
}
return#OnTouchListener true
})
}
fun drawCircle(x: Float, y: Float) {
val width: Int = x.toInt()
val height: Int = y.toInt()
val bitmap = (mFirst.getDrawable() as BitmapDrawable).bitmap
val workingBitmap: Bitmap = Bitmap.createBitmap(bitmap)
val mutableBitmap = workingBitmap.copy(Bitmap.Config.ARGB_8888, true)
val canvasBitmap = Canvas(mutableBitmap)
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.style = Paint.Style.FILL
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
canvasBitmap.drawCircle(width.toFloat(), height.toFloat(), 180F, paint)
mFirst.setImageBitmap(mutableBitmap)
drawUtils.draw()
}
The problem is that the circle is not drawn under the finger, but on the side.
I also want to note that the image placed inside the ImageView has a larger size and I use scaleType = "cropCenter".
It turns out that I need to get the position of the finger exactly on the image placed in the ImageView, and not just on the screen.
If someone knows the correct answer to the question, I would be grateful.
This is really bad code. Why are you editing the background image in the view? The easier thing would be to save the coordinates in the onTouch function, then invalidate the view. Then in the view's onDraw, you draw a circle at that location. This way you avoid all the scaling issues, and you don't have to make 2 COPIES!!! of the bitmap which is horribly inefficient.
canvasBitmap.drawCircle(width.toFloat() - 180F / 2, height.toFloat() - 180F / 2, 180F, paint)
Have a try.

How to refresh "onDraw()" every 100 millisecond or less to move "bitmap"

I want to move "bitmap" from top to down and then from down to top.
I use "invalidate()" to refresh "onDraw()" after "y1" increases or decreases, everything working fine but "bitmap" movement is very slow.
I can increases or decreases y1 like "y1 += 15" but "bitmap" will move not smooth
I want refresh "onDraw()" every 100ms or less.
How to do it?
This screenshot
package com.example.duc25.runningman
import android.content.Context
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Paint
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.widget.Toast
import java.util.*
class Man(contex: Context, var screenW: Float, var screenH: Float): View(contex){
var bitmap1 = BitmapFactory.decodeResource(resources, R.drawable.step1)
var x1: Float = 0F
var y1: Float = 0F
var boolean1: Boolean = true
override fun onTouchEvent(event: MotionEvent): Boolean{
if(event.action === MotionEvent.ACTION_DOWN){
if(boolean1 == true){
boolean1 = false
}else{
boolean1 = true
}
}
return true
}
override fun onDraw(canvas: Canvas){
set_X(canvas)
if (boolean1) {
if (y1 <= (screenH / 100) * 70){
y1 += 1F
}else{
boolean1 = false
}
}else{
if(y1 >= 0) {
y1 -= 1F
}else{
boolean1 = true
}
}
invalidate()
}
fun set_X(canvas: Canvas) {
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
canvas.drawBitmap(bitmap1, x1, y1, paint)
}
}
Instead of using invalidate(), you probably want to use postInvalidateOnAnimation(). This will trigger an animation at 60 fps, or ~ every 16 millis.
You could also use some sort of postInvalidateDelayed() if you want a slower fps (you probably want 60 fps).
My guess you are getting lag because you are spamming the main thread with invalidate() repetitively. If you are still getting lag when using postInvalidateOnAnimation(), then what you are doing on each animation frame is too heavy, and you'll have to find a way to make it cheaper / faster.
EDIT: A few other tips.
1. You are creating the Paint every animation frame. That's bad. I recommend you move the val paint = Paint(Paint.ANTI_ALIAS_FLAG) into a property at the top of the class, i.e. with the y1 and x1 properties.
2. bitmap1 can be val instead of var.

Hamburger menu icon to cross animation in Android

I'm trying to replace menu icon with cross icon, but I don't know better solution than replacing source in ImageView and futher more I cannot find done libraries which converts images.
Any help will be appreciateg.
Android has a drawable for animating between hamburger and arrow: android.support.v7.graphics.drawable.DrawerArrowDrawable
This drawable uses very generic approach with canvas drawing. If you have some spare time and ready for some tedious work, you can animate pretty much anything by looking at this example.
For instance, here is "hamburger" to cross drawable:
/**
* Simple animating drawable between the "hamburger" icon and cross icon
*
* Based on [android.support.v7.graphics.drawable.DrawerArrowDrawable]
*/
class HamburgerCrossDrawable(
/** Width and height of the drawable (the drawable is always square) */
private val size: Int,
/** Thickness of each individual line */
private val barThickness: Float,
/** The space between bars when they are parallel */
private val barGap: Float
) : Drawable() {
private val paint = Paint()
private val thick2 = barThickness / 2.0f
init {
paint.style = Paint.Style.STROKE
paint.strokeJoin = Paint.Join.MITER
paint.strokeCap = Paint.Cap.BUTT
paint.isAntiAlias = true
paint.strokeWidth = barThickness
}
override fun draw(canvas: Canvas) {
if (progress < 0.5) {
drawHamburger(canvas)
} else {
drawCross(canvas)
}
}
private fun drawHamburger(canvas: Canvas) {
val bounds = bounds
val centerY = bounds.exactCenterY()
val left = bounds.left.toFloat() + thick2
val right = bounds.right.toFloat() - thick2
// Draw middle line
canvas.drawLine(
left, centerY,
right, centerY,
paint)
// Calculate Y offset to top and bottom lines
val offsetY = barGap * (2 * (0.5f - progress))
// Draw top & bottom lines
canvas.drawLine(
left, centerY - offsetY,
right, centerY - offsetY,
paint)
canvas.drawLine(
left, centerY + offsetY,
right, centerY + offsetY,
paint)
}
private fun drawCross(canvas: Canvas) {
val bounds = bounds
val centerX = bounds.exactCenterX()
val centerY = bounds.exactCenterY()
val crossHeight = barGap * 2 + barThickness * 3
val crossHeight2 = crossHeight / 2
// Calculate current cross position
val distanceY = crossHeight2 * (2 * (progress - 0.5f))
val top = centerY - distanceY
val bottom = centerY + distanceY
val left = centerX - crossHeight2
val right = centerX + crossHeight2
// Draw cross
canvas.drawLine(
left, top,
right, bottom,
paint)
canvas.drawLine(
left, bottom,
right, top,
paint)
}
override fun setAlpha(alpha: Int) {
if (alpha != paint.alpha) {
paint.alpha = alpha
invalidateSelf()
}
}
override fun setColorFilter(colorFilter: ColorFilter?) {
paint.colorFilter = colorFilter
invalidateSelf()
}
override fun getIntrinsicWidth(): Int {
return size
}
override fun getIntrinsicHeight(): Int {
return size
}
override fun getOpacity(): Int {
return PixelFormat.TRANSLUCENT
}
/**
* Drawable color
* Can be animated
*/
var color: Int = 0xFFFFFFFF.toInt()
set(value) {
field = value
paint.color = value
invalidateSelf()
}
/**
* Animate this property to transition from hamburger to cross
* 0 = hamburger
* 1 = cross
*/
var progress: Float = 0.0f
set(value) {
field = value.coerceIn(0.0f, 1.0f)
invalidateSelf()
}
}
You can use this drawable like any other drawable, for example by setting ImageView src to this drawable:
imageView = AppCompatImageView(context)
addView(imageView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
hamburgerDrawable = HamburgerCrossDrawable(
size = dpToPx(20).toInt(),
barThickness = dpToPx(2),
barGap = dpToPx(5)
)
hamburgerDrawable.color = hamburgerColor
imageView.setImageDrawable(hamburgerDrawable)
To make the drawable actually change from hamburger to cross and back you'll need to change HamburgerCrossDrawable.progress (0 stands for hamburger and 1 stands for cross):
val animator = ValueAnimator.ofFloat(0.0f, 1.0f)
animator.interpolator = AccelerateDecelerateInterpolator()
animator.duration = 300
animator.addUpdateListener {
val progress = it.animatedValue as Float
val color = interpolateColor(hamburgerColor, crossColor, progress)
hamburgerDrawable.color = color
hamburgerDrawable.progress = progress
}
animator.start()
I seem to be a little late to the show, but I prefer a declarative approach with an animated selector for icon animations. It seems a lot clearer and the only thing you need to care about are the View or Button's states.
TL;DR: I've created a gist with all of the classes needed to achieve the animation.
Here's an example of the selector which you use as a drawable:
<?xml version="1.0" encoding="utf-8"?>
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="#+id/open"
android:drawable="#drawable/ic_drawer_closed"
android:state_selected="true"/>
<item
android:id="#+id/closed"
android:drawable="#drawable/ic_drawer"/>
<transition
android:drawable="#drawable/avd_drawer_close"
android:fromId="#id/open"
android:toId="#id/closed"/>
<transition
android:drawable="#drawable/avd_drawer_open"
android:fromId="#id/closed"
android:toId="#id/open"/>
</animated-selector>
And here's the animation itself:
You shoud use existing AnimatedVectorDrawable on the internet. Or you should create it with your icons on Shape Shifter web site. I recommend you to watch a tutrial on youtube for this process: ShapeShifter Tutorial.
Handle the state change of the button by setting a state value in the tag of the button and change the Animatable vector drawable resource according to the value of the current solution may also work.
btnMenuView.setOnClickListener {
val state = it.getTag(R.string.meta_tag_menu_button_state).toString().trim()
context?.let {
var animDrawable =
AnimatedVectorDrawableCompat.create(it, R.drawable.avd_drawer_open)
if (state != "open") {
animDrawable =
AnimatedVectorDrawableCompat.create(it, R.drawable.avd_drawer_close)
btnMenuView.setTag(R.string.meta_tag_menu_button_state, "open")
} else {
btnMenuView.setTag(R.string.meta_tag_menu_button_state, "close")
}
btnMenuView.setImageDrawable(animDrawable)
val animatable: Drawable? = btnMenuView.drawable
if (animatable is Animatable) {
animatable.start()
}
}
---Rest of your code
}

Android LinearGradient Preallocate and reuse

I have been updating some old code in an app that has a lot of re-allocations in onDraw that complain with message:
Avoid object allocations during draw/layout operations (preallocate and reuse instead).
So I have got all of it updated properly with no more warning, except one. LinearGradient. It seems there is no method to set values on an instance of the object. And the properties are not public so you can't do linLayout.x = value;
This is my code and it complains with the warning described above (underlines LinearGradient):
myPaintGradient.setShader(new LinearGradient(deviation,6,halfwidth,LinearGradientSize,barColorGreen, barColorRed, android.graphics.Shader.TileMode.CLAMP));
I have just solved the same problem, albeit with a RadialGradient.
If you want to update location data of a shader on each draw call you should pre-allocate the shader (as the linter hints at) and you should also pre-allocate a Matrix. The only functionality a shader instance exposes is getLocalMatrix and setLocalMatrix.
To do what you want here, you will make use of setLocalMatrix, passing in a Matrix instance which you have performed some appropriate transforms on. In your case, I think a simple translation transform will do the trick.
As I touched on earlier, you will need to pre-allocate a Matrix and you will modify this pre-allocated one on each draw loop, before passing it into the shader.setLocalMatrix.
Here is an example of how I did this to update the centerX and centerY of a RadialGradient shader. The use case for this was a user can drag a circle around and I needed to keep the radial gradient centered on the circle.
The code below shows you a full working solution as a custom view which you could copy and paste into your code base, and use in some XML.
The interesting bit is in the onDraw override, where I am doing the matrix transforms and updating the shader:
class MoveableGradientCircle #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
// Struct for all the data which needs pre-allocating
private data class Circle(
val radius: Float,
val centerX: Float,
val centerY: Float,
val paint: Paint,
val shaderMatrix: Matrix
)
// Pre-allocate the data
private var circle: Circle = Circle(
radius = 10f,
centerX = x,
centerY = y,
paint = Paint().apply {
isAntiAlias = true
style = Paint.Style.FILL_AND_STROKE
shader = RadialGradient(
centerX = 0f,
centerY = 0f,
radius = 10f,
startColor = Color.RED,
edgeColor = Color.GREEN,
tileMode = Shader.TileMode.CLAMP
)
},
shaderMatrix = Matrix()
)
// Setup a touch listener to update the circles x/y positions on user touch location
init {
setOnTouchListener { view, event ->
view.performClick()
when (event.action) {
MotionEvent.ACTION_DOWN -> {
circle = circle.copy(
centerX = event.x,
centerY = event.y
)
invalidate()
true
}
MotionEvent.ACTION_MOVE -> {
circle = circle.copy(
centerX = event.x,
centerY = event.y
)
invalidate()
true
}
MotionEvent.ACTION_UP -> {
circle = circle.copy(
centerX = event.x,
centerY = event.y
)
invalidate()
true
}
else -> false
}
}
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
// No need to keep re-allocating shader
// Instead, we update the pre-allocated matrix
// In this case, we'll just translate it to the circles current center x,y
// which happens to be the users touch location
circle.shaderMatrix.reset()
circle.shaderMatrix.setTranslate(
circle.centerX,
circle.centerY
)
// Update the matrix on the shader
circle.paint.shader.setLocalMatrix(circle.shaderMatrix)
// Draw the arc with the updated paint
canvas?.drawArc(
circle.centerX - circle.radius,
circle.centerY - circle.radius,
circle.centerX + circle.radius,
circle.centerY + circle.radius,
0f,
360f,
true,
circle.paint
)
}
}
I hope this helps you or someone else with the same problem in the future!

Categories

Resources