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.
Related
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'm currently working in a custom view, basically its a draggable view that has a pulse effect. In fact I achieved the effect that I want but I it has performance issues.
I don't know if it can be seen well in the gif but at first the effect runs without problems and then it shows performance issues.
Here is my class
class BalanceAndFadeView #JvmOverloads constructor(...) : FrameLayout(...) {
// The thumb view is the draggable view
private var thumb: ImageView
// These are for the pulsing effect
private var pulseView: ImageView
private var pulseView2: ImageView
private var selectedPoint: Point = Point()
private val animators = arrayListOf<Animator>()
private var animatorsSet = AnimatorSet()
private var centerX = 0f
private var centerY = 0f
init {
// Here I create and add the views to the root view
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
centerX = w * 0.5f
centerY = h * 0.5f
animationStart()
}
override fun onTouchEvent(event: MotionEvent): Boolean {
return when (event.actionMasked) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_MOVE,
MotionEvent.ACTION_UP -> {
val snapPoint = Point(event.x.toInt(), event.y.toInt())
selectedPoint = snapPoint
setCoordinate(snapPoint.x, snapPoint.y)
true
}
else -> {
false
}
}
}
fun setCoordinate(x: Int, y: Int) {
// Sets the thumb view coordinates to create the draggable view effect
thumb.x = x - thumb.measuredWidth * 0.5f
thumb.y = y - thumb.measuredHeight * 0.5f
// These animators are for the pulsing effect to translate to the center of the view
// I want the start point of the translation to update with the position
// of the thumb view, so I need it to be a dynamic animation
val translationXAnimator =
ObjectAnimator.ofFloat(pulseView, "TranslationX", x.toFloat() - centerX, 0f)
.apply {
repeatCount = ObjectAnimator.INFINITE
repeatMode = ObjectAnimator.RESTART
startDelay = 0
}
val translationYAnimator =
ObjectAnimator.ofFloat(pulseView, "TranslationY", y.toFloat() - centerY, 0f)
.apply {
repeatCount = ObjectAnimator.INFINITE
repeatMode = ObjectAnimator.RESTART
startDelay = 0
}
val translationXAnimator2 = ObjectAnimator.ofFloat(...).apply { ... }
val translationYAnimator2 = ObjectAnimator.ofFloat(...).apply { ... }
animatorsSet.playTogether(
... // adds new animators
)
// Since I run this method every time the touch event it's troggered,
// the performance gets worse every time I touch the screen
animatorsSet.start()
// But if I call animatorsSet.end() and then animatorsSet.start()
// the pulsing effect does not work as expected since the animation stops
// but in terms of performance it's just normal
}
// These animators are for the pulsing effect
private fun animationStart() {
val scaleXAnimator = ObjectAnimator.ofFloat(pulseView, "ScaleX", 1f, 6f).apply {
repeatCount = ObjectAnimator.INFINITE
repeatMode = ObjectAnimator.RESTART
startDelay = 0
}
animators.add(scaleXAnimator)
val scaleYAnimator = ObjectAnimator.ofFloat(pulseView, "ScaleY", 1f, 6f).apply {
repeatCount = ObjectAnimator.INFINITE
repeatMode = ObjectAnimator.RESTART
startDelay = 0
}
animators.add(scaleYAnimator)
val alphaAnimator = ObjectAnimator.ofFloat(pulseView, "Alpha", 1f, 0f).apply {
repeatCount = ObjectAnimator.INFINITE
repeatMode = ObjectAnimator.RESTART
startDelay = 0
}
animators.add(alphaAnimator)
val scaleXAnimator2 = ObjectAnimator.ofFloat( ... ).apply { ... }
animators.add(scaleXAnimator2)
val scaleYAnimator2 = ObjectAnimator.ofFloat( ... ).apply { ... }
animators.add(scaleYAnimator2)
val alphaAnimator2 = ObjectAnimator.ofFloat( ... ).apply { ... }
animators.add(alphaAnimator2)
animatorsSet.playTogether(animators)
animatorsSet.interpolator = LinearInterpolator()
animatorsSet.duration = 1000
animatorsSet.start()
}
}
I hope you can help me or recommend me a better approach
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?
I have a translate animation like this
fun createTranslateAnimation(fromX: Float, toX: Float, fromY: Float, toY: Float):
ObjectAnimator {
val propertyValuesX = PropertyValuesHolder.ofFloat(View.TRANSLATION_X, fromX, toX)
val propertyValuesY = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, fromY, toY)
return ObjectAnimator.ofPropertyValuesHolder(imageView,
propertyValuesX,
propertyValuesY)
.apply {
duration = 3000
}
}
I specified the duration 3000ms. But when I run the app, the animation is executing 2 times faster. So I've put some logs like this.
val objectAnim = createTranslateAnimation(xValueFrom, xValueTo, yValueFrom, yValueTo)
objectAnim.apply {
doOnStart {
time = System.currentTimeMillis()
}
doOnEnd {
val elapsedTime = System.currentTimeMillis() - time
Log.d("$elapsedTime seconds")
}
start()
}
At the end the result is - 1518 seconds
What am I doing wrong? If I specify the duration of 3000ms, why is it executing two times faster?
It sounds like Animation duration scale setting in device's Developer options section is changed.
As far as I know once the Developer options and some setting or value is changed there is nothing you can do so the system doesn't take it in consideration.
I am delevolping an android app in which I have an infinitely repeating animation that is causing a StackOverflowError. It does this, when another animation on the same object is started.
private fun pulse() {
val randomGenerator = Random()
val durationx = randomGenerator.nextInt(4000) + 1500
val inflateX = ObjectAnimator.ofFloat(mContentView, "scaleX", 1.3f).apply {
duration = durationx.toLong()
}
val inflateY = ObjectAnimator.ofFloat(mContentView, "scaleY", 1.3f).apply {
duration = durationx.toLong()
}
val deflateX = ObjectAnimator.ofFloat(mContentView, "scaleX", 1.0f).apply {
duration = durationx.toLong()
}
val deflateY = ObjectAnimator.ofFloat(mContentView, "scaleY", 1.0f).apply {
duration = durationx.toLong()
}
val rotate = ObjectAnimator.ofFloat(mContentView, "rotation", 1.0f).apply {
duration = durationx.toLong()
}
val soulToButton = AnimatorSet().apply {
play(inflateX).with(inflateY)
play(rotate).with(inflateX)
play(deflateX).after(inflateX)
play(deflateY).after(inflateY)
start()
}
soulToButton.addListener(object: AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
soulToButton.start() // stacktrace points to this line as cause for the error.
}
})
soulToButton.start()
AnimatorSet().apply {
play(soulToButton)
start()
}
}
I tried changing the function to fun pulse(stop: boolean), calling pulse(false) for starting the pulse and pulse(true) before the other animation starts, and adding if (stop) {soulToButton.cancel()} in several places. I also tried wrapping the line causing the error like this: while(stop == false){soultoButton.start()}
All of this didnĀ“t help.
Try out this version: its short and concise and basically doing the same thing.
val randomGenerator = Random()
val durationx = randomGenerator.nextInt(4000) + 1500
val animator = animator#{ property : String, value : Float ->
return#animator ObjectAnimator.ofFloat(mContextView, property, value).apply {
duration = durationx.toLong()
repeatMode = REVERSE
repeatCount = INFINITE
}
}
AnimatorSet().apply {
playTogether(animator("scaleX", 1.3f),animator("scaleY", 1.3f),animator("rotation", 45.0f))
start()
}
It is Kotlin but it is still Java and all the JVM limitations are relevant here. Thus you cannot call one function indefinitely without causing StackOverflowError since the JVM stack is limited.
There are special functions that will help you to run the animation indefinitely. For example
objectAnimator.setRepeatCount(ObjectAnimator.INFINITE);
Or in your case
ObjectAnimator.ofFloat(mContentView, "scaleX", 1.3f).apply {
duration = durationx.toLong()
repeatCount = ObjectAnimator.INFINITE
}
After you modify all your animations the same way and chain them in AnimatorSet its start function will play this animation indefinite time without StackOverflowError.
Hope it helps.
You are trying to start animation again after it has completed in here ,
soulToButton.addListener(object: AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
soulToButton.start() // This will start the animation again without an end to the animation hence the StackOverflow error.
}
})
In order to run an animation for infinite time use the following piece of code,
val inflateX = ObjectAnimator.ofFloat(mContentView, "scaleX", 1.3f).apply {
duration = durationx.toLong()
repeatCount = ObjectAnimator.INFINITE
}
val soulToButton = AnimatorSet().apply {
play(inflateX).with(inflateY)
play(rotate).with(inflateX)
play(deflateX).after(inflateX)
play(deflateY).after(inflateY)
}
soulToButton.start()