This is something weird and unexpected that is happening. While creating a custom view, I am animating a padding value which is later displaying on Canvas. But other objects like RectF are changing its values too. Using kotlin, I have this helper function:
operator fun Pair<Float, Float>.invoke(callback: (Float) -> Unit) {
with(ValueAnimator.ofFloat(first, second)) {
duration = 500
addUpdateListener {
callback(it.animatedValue as Float)
}
start()
}
}
In my Custom View, I have this code:
private val bgCardBounds = RectF(0f, 0f, 0f, 0f)
private var expandedCard = 0
set(value) {
field = value
when (value) {
1 -> {
(0f to 32f) {
bitmapPadding = it
invalidate()
}
}
}
}
On an touch event, when the value of expandedCard changes from 0 to 1, then even the values left, top, right, bottom values of bgCardBounds also changes. Can someone help me point out why this is happening and how to stop it?
Related
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
I'm trying to get two views to move to the middle of the screen and bounce back again x number of times.
This code does that but it runs only once.
` val view = findViewById(R.id.imageView2)
val animation = SpringAnimation(view, DynamicAnimation.TRANSLATION_Y, 0f)
val view2 = findViewById<View>(R.id.imageView3)
val animation2 = SpringAnimation(view2, DynamicAnimation.TRANSLATION_Y, 0f)
findViewById<View>(R.id.imageView2).also { img ->
SpringAnimation(img, DynamicAnimation.TRANSLATION_Y).apply {
animation.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_HIGH_BOUNCY)
animation.spring.stiffness = SpringForce.STIFFNESS_VERY_LOW
animation.animateToFinalPosition(50f)
}
}
findViewById<View>(R.id.imageView3).also { img ->
SpringAnimation(img, DynamicAnimation.TRANSLATION_Y).apply {
animation2.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_HIGH_BOUNCY)
animation2.spring.stiffness = SpringForce.STIFFNESS_VERY_LOW
animation2.animateToFinalPosition(-100f)
}
}`
So how do I get it to run x number of times?
This is obviously Spring Animation, but I'm not married to it. If there is another animation that would accomplish this I'd be totally open to changing.
You can run multiple SpringAnimations on the same View by repeatedly calling animateToFinalPosition(translation) with a sequence of translation values.
For example:
startSpringAnimations(findViewById<View>(R.id.imageView1), 300f, 6)
startSpringAnimations(findViewById<View>(R.id.imageView2), -600f, 6)
with a function
/**
* [view] will be moved using [times] SpringAnimations over a distance of abs([totalTranslation])
* If [totalTranslation] is negative, direction will be up, else down
*/
private fun startSpringAnimations(view: View, totalTranslation: Float, times: Int ) {
if(times <= 0){
return
}
val translation = totalTranslation/ times.toFloat()
SpringAnimation(view, DynamicAnimation.TRANSLATION_Y, 0f).apply{
spring.dampingRatio = SpringForce.DAMPING_RATIO_HIGH_BOUNCY
spring.stiffness = SpringForce.STIFFNESS_VERY_LOW
addEndListener(object: DynamicAnimation.OnAnimationEndListener{
private var count = 1
override fun onAnimationEnd(animation1: DynamicAnimation<*>?, canceled: Boolean, value: Float, velocity: Float) {
Log.d("SpringAnimation", "onAnimationEnd: animation $animation1 canceled $canceled value $value velocity $velocity count $count")
if (canceled) return
count++
if(count <= times){
animateToFinalPosition(translation * count)
}
}
})
animateToFinalPosition(translation)
}
}
Set android:repeatCount="infinite" in anim folder
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()
}
}
I have a custom view. In this view, I am drawing 4 circles. This view has drawCirclesWithAnim() function in which I would like to draw circles with animation one by one with sequence.
For example, first circle is drawn at first, then second, then third ... so on. How to achieve this with custom views?
Since it is custom view, I think my only option is ValueAnimator. Currently, to achieve what I need, I am creating 4 ValueAnimators. Check out the code below:
CustomView.kt
private var mAnimValCircle1 = 0f //These values are used in "onDraw()".
private var mAnimValCircle2 = 0f
private var mAnimValCircle3 = 0f
private var mAnimValCircle4 = 0f
...
fun drawCirclesWithAnim(): {
ValueAnimator().apply {
duration = 200
addUpdateListener {
mAnimValCircle1 = it.animatedValue as Float
invalidate()
}
setFloatValues(0f, 1f)
start()
}
ValueAnimator().apply {
startDelay = 200
duration = 150
addUpdateListener {
mAnimValCircle2 = it.animatedValue as Float
invalidate()
}
setFloatValues(0f, 1f)
start()
}
ValueAnimator().apply {
startDelay = 350
duration = 150
addUpdateListener {
mAnimValCircle3 = it.animatedValue as Float
invalidate()
}
setFloatValues(0f, 1f)
start()
}
ValueAnimator().apply {
startDelay = 500
duration = 150
addUpdateListener {
mAnimValCircle4 = it.animatedValue as Float
invalidate()
}
setFloatValues(0f, 1f)
start()
}
}
As you can see, I am manualy calculating the delays and making sure that each animation starts after the previous one finishes. Not only that but also I am creating ValueAnimator for each of the circles. It is working fine. But I reckon this is not perfect solution.
Is there a way to create AnimationSet from ValueAnimator? Or, are there any other solutions available to tackle such cases?
I don't know of a way to use AnimationSet and ValueAnimator and don't believe it is possible out-of-the-box, but what you could do is use parameters in your function and write one method to handle it all, and with that get rid of lots of unneccessary code, something like :
fun drawCircleWithAnim(mAnimValCircle: float, duration: Int, startDelay: Int) {
ValueAnimator().apply {
startDelay = startDelay
duration = duration
addUpdateListener {
mAnimValCircle = it.animatedValue as Float
invalidate()
}
setFloatValues(0f, 1f)
start()
}
}
And then call 4 times like this :
drawCircleWithAnim(mAnimValCircle1, 200, 0)
drawCircleWithAnim(mAnimValCircle2, 150, 200)
drawCircleWithAnim(mAnimValCircle3, 350, 150)
drawCircleWithAnim(mAnimValCircle4, 500, 150)
Creating 4 ValueAnimators should not be a problem unless you are looking for ultra-micro optimization.
I am working on a paint tool for android and have attempted to implement a 'fill with color' tool. Where a user is able to fill a particular area with color depending on what they have selected. I have found examples of Flood Fill for Java but cant see anything similar for Kotlin.
I have tried the following - https://developer.android.com/reference/kotlin/android/graphics/Path.FillType
But have not had too much luck since it instead uses the surrounding colour and not what colour the user has selected.
EDIT:
A snippet of the code I am working with:
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>()
private val mUndoPaths = ArrayList<CustomPath>()
private val mRedoPaths = ArrayList<CustomPath>()
private var counterUndo = 0
init {
setUpDrawing()
}
private fun 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)
}
//Change Canvas to Canvas? if fails
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawBitmap(mCanvasBitMap!!, 0f,0f, mCanvasPaint)
for(path in mPaths){
mDrawPaint!!.strokeWidth = path.brushThickness
mDrawPaint!!.color = path.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 {
val touchX = event?.x
val touchY = event?.y
when(event?.action){
MotionEvent.ACTION_DOWN -> {
mDrawPath!!.color = color
mDrawPath!!.brushThickness = mBrushSize
mDrawPath!!.reset()
mDrawPath!!.moveTo(touchX!!, touchY!!)
}
MotionEvent.ACTION_MOVE -> {
mDrawPath!!.lineTo(touchX!!, touchY!!)
}
MotionEvent.ACTION_UP -> {
mPaths.add(mDrawPath!!)
mDrawPath = CustomPath(color, mBrushSize)
}
else -> return false
}
invalidate()
return true
}
fun setFill(){
mDrawPaint!!.style = Paint.Style.FILL
}
}
This is kind of a complicated subject - if you want to draw a Path with a particular fill and colour, you need to create a Paint and use setColor (and setStyle to make it Paint.Style.FILL.
But Paths are for vector graphics, they're all defined lines and curves. Unless you're creating the shapes that the user can fill in as paths in the first place, you'll have trouble defining one to fit an arbitrary area.
I'm guessing you're actually using bitmaps, like a normal paint program, and you want to tap on a pixel and have that change colour, and also recursively change the colour of surrounding pixels if they meet a certain threshold. So you'll have to get the Canvas's Bitmap, and work out how to move through the pixels, changing them as you go (e.g. with Bitmap.setPixel())
There are a lot of algorithms for doing this, you'll just need to pick one and implement it. You probably don't want to use Paths to do it though!