I'm in the process of learning Animation in android and I have several questions -
I have view that I animate based on some bool:
mainFab.setOnClickListener {
isOpen = ViewAnimations.rotate(binding.mainFab, !isOpen)
if (isOpen) {
ViewAnimations.apply {
showMenu(binding.shareFab)
}
} else {
ViewAnimations.apply {
hideMenu(binding.shareFab)
}}
}
ViewAnimations methods:
fun rotateFab(view: View, isFabOpen: Boolean): Boolean {
view.animate()
.rotation(if (isFabOpen) 1440f else 0f)
.setDuration(2000)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation)
Log.d(TAG, "onAnimationEnd: ")
}
})
return isFabOpen
}
fun showMenu(view: View) {
view.visibility = View.VISIBLE
view.alpha = 0f
view.animate()
.setDuration(2000)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation)
}
})
.alpha(1f)
.start()
}
fun hideMenu(view: View) {
view.animate()
.setDuration(2000)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
view.visibility = View.GONE
super.onAnimationEnd(animation)
}
}).alpha(0f)
.start()
My questions are:
In showMenu function, why my animation doesn't work properly without the empty listener?
It's working fine at the first time, but from the second time and on it does animate the view, but then set the alpha to 0/ view to gone.
Why the animation still working without .start()? Is it mandatory to use it?
if I start animation by calling showMenu and then at the half way I'm calling hideMenu it just hide the view in very ugly way, there is a way to "reverse" the animation in more elegant way?
All yours problem is from over engineering. What is doing the object ViewAnimations?
About question :
There is can not be problem in empty listener. There is a problem somewhere in your logic.
Without .start() animation can not be launched. If animation is starting so there again a problem somewhere in your logic.
ViewPropertyAnimator animation can perfect animate from semi-state to new state. I recommend you to start write animation from the beginning to be confident in all your steps.
Related
I'm using a custom animator to animate two recycler in two different fragments with custom animations. Everything works, except a minor and a major issue. First thing first, here's the custom animator:
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.util.Log
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
// A custom animator to animate the items in recycler view
class RecyclerAnimator : SimpleItemAnimator() {
// Never called
override fun animateRemove(viewHolder: RecyclerView.ViewHolder): Boolean {
viewHolder.itemView.animate()
.alpha(0F)
.setInterpolator(FastOutSlowInInterpolator())
.setStartDelay(200)
.setDuration(300)
.scaleY(0F)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
dispatchRemoveFinished(viewHolder)
}
})
.start()
Log.d("recycler_animation", "animate remove")
return false
}
// Called when the items appear in the list (launch, fragment change, it was created)
override fun animateAdd(viewHolder: RecyclerView.ViewHolder): Boolean {
val height: Int = viewHolder.itemView.measuredHeight / 3
val view = viewHolder.itemView
view.translationY = height.toFloat()
view.alpha = 0F
view.scaleY = 1F
view.animate()
.translationY(0F)
.alpha(1F)
.setInterpolator(FastOutSlowInInterpolator())
.setDuration(400)
.setStartDelay(viewHolder.bindingAdapterPosition * 50L)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
dispatchAddFinished(viewHolder)
}
})
.start()
Log.d("recycler_animation", "animate add")
return false
}
// Called when an item is being moved to another position in the adapter
override fun animateMove(
viewHolder: RecyclerView.ViewHolder,
fromX: Int, fromY: Int,
toX: Int, toY: Int
): Boolean {
val item = viewHolder.itemView
item.y = fromY.toFloat()
val verticalMovement = if (toY > fromY)
(toY - fromY).toFloat() - (item.measuredHeight)
else (fromY - toY).toFloat() - (item.measuredHeight)
item.animate()
.translationY(verticalMovement)
.setDuration(300)
.setInterpolator(FastOutSlowInInterpolator())
.setStartDelay(200)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
dispatchMoveFinished(viewHolder)
}
})
.start()
Log.d("recycler_animation", "animate move")
return false
}
// Called when an item changes its data
override fun animateChange(
oldHolder: RecyclerView.ViewHolder,
newHolder: RecyclerView.ViewHolder,
fromLeft: Int, fromTop: Int,
toLeft: Int, toTop: Int
): Boolean {
newHolder.itemView.alpha = 0F
val oldAnimation = oldHolder.itemView.animate()
.alpha(0F)
.setInterpolator(FastOutSlowInInterpolator())
.setDuration(300)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
dispatchChangeFinished(oldHolder, true)
}
})
val newAnimation = newHolder.itemView.animate()
.alpha(1F)
.setInterpolator(FastOutSlowInInterpolator())
.setDuration(300)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
dispatchChangeFinished(newHolder, false)
}
})
oldAnimation.start()
newAnimation.start()
Log.d("recycler_animation", "animate change")
return false
}
// Called when an item is deleted from the adapter
override fun animateDisappearance(
viewHolder: RecyclerView.ViewHolder,
preLayoutInfo: ItemHolderInfo,
postLayoutInfo: ItemHolderInfo?
): Boolean {
viewHolder.itemView.animate()
.alpha(0F)
.setInterpolator(FastOutSlowInInterpolator())
.setDuration(400)
.setStartDelay(100)
.scaleY(0F)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
dispatchRemoveFinished(viewHolder)
viewHolder.itemView.alpha = 1F
viewHolder.itemView.scaleY = 1F
}
})
.start()
Log.d("recycler_animation", "animate disappearance")
return false
}
override fun runPendingAnimations() {
Log.d("recycler_animation", "pending animations")
}
override fun endAnimation(viewHolder: RecyclerView.ViewHolder) {
Log.d("recycler_animation", "end animation")
val item = viewHolder.itemView
item.alpha = 1F
item.translationY = 0F
item.scaleY = 1F
}
override fun endAnimations() {
Log.d("recycler_animation", "end animation no arg")
}
override fun isRunning(): Boolean {
return false
}
}
First, major issue: i use two different viewholder layouts in one of the recycler views, and when the elements are moved (animateMove) the shorter elements are moved in the wrong position both when they move up or down. My guess is that the "measured height" is wrong, but i have no idea why. (This issue can be observed in the first part of the gif)
Second, minor issue: since i use a delay to make the items appear gradually, when adding an item back in the list, if the item is in the upper part the animation has no delay, while if the item is in the lower part, the animation starts with a delay based on its position in the list. (This issue can be observed in the second part of the gif)
Any help is well appreciated, since i'm close to the desired effect
On the major issue, may be due to measuredHeight vs height.
From the View page. "A view actually possess two pairs of width and height values." "measured width and measured height. These dimensions define how big a view wants to be" "width and height. These dimensions define the actual size of the view on screen" Because you are calling measuredHeight, the actual height of the item in the final layout may not agree.
On the minor issue, have you tried returning "true" from the animate functions? The wording in the doc is really confusing, yet could be interpreted as "true if a call to runPendingAnimations is requested, false if start all animations together in a later call to runPendingAnimations"
I am using shared element transition between two activities. The second activity is comprised of view pager containing fragments. I want to make further changes when shared element transition ends. This is the callback:
setEnterSharedElementCallback(
object : SharedElementCallback() {
override fun onMapSharedElements(
names: MutableList<String>,
sharedElements: MutableMap<String, View>
) {
val keySharedElementView = sharedElements[videoPath[currentPosition]]
if (keySharedElementView != null) {
Log.i("KSEV", "Not Null")
ViewCompat.animate(keySharedElementView)
.setListener(object : ViewPropertyAnimatorListenerAdapter() {
override fun onAnimationEnd(view: View?) {
super.onAnimationEnd(view)
Log.i("KSEV","Ended")
runOnUiThread { k
videoThumb.visibility = GONE
if (videoThumb.visibility == GONE) {
Log.i("SEV", "GONE")
}
}
}
}).start()
}
}
})
Last Log statement GONE is printed. But view is still visible on screen. How to change its visibility?
Your code seems good except that you haven't started the animation yet, hence no onAnimationEnd() callback.
Modify your code as follows:
ViewCompat.animate(keySharedElementView)
.setListener(object : ViewPropertyAnimatorListenerAdapter() {
override fun onAnimationEnd(view: View?) {
super.onAnimationEnd(view)
Log.i("KSEV","Ended")
}
}).setDuration(250).start()
val anim = swipe.animate()
.rotationBy((-30).toFloat())
.setDuration(1000)
.translationX((-swipe.left).toFloat())
.setInterpolator(AccelerateDecelerateInterpolator())
anim.start()
I need an animation finish listener, I tried:
anim.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(p0: Animation?) {
}
override fun onAnimationRepeat(p0: Animation?) {
}
override fun onAnimationEnd(p0: Animation?) {
}
})
but get this error
Unresolved reference: setAnimationListener
How to do this right?
Root cause
In ViewPropertyAnimator class, there is no method named setAnimationListener.
Solution 1
anim.withEndAction {
// Your code logic goes here.
}
Solution 2
anim.setListener(object : Animator.AnimatorListener {
override fun onAnimationRepeat(animation: Animator?) {}
override fun onAnimationCancel(animation: Animator?) {}
override fun onAnimationStart(animation: Animator?) {}
override fun onAnimationEnd(animation: Animator?) {
// Your code logic goes here.
}
})
Note: Remember to cancel the anim if users leave the screen while animation.
swipe.animate().cancel()
I've recently found that depending on your android sdk, when changing visibility of a view, it behaves differently.
Before Oreo, when setting from VISIBLE to GONE, the view first fade from 1 to 0 in terms of alpha, then the place that the view used collapse.
For Oreo and later (I suppose I haven't tested on Android Q), when setting from VISIBLE to GONE, the view collapse while fading, doing clipping with other views, since the alpha hasn't been set to zero yet.
I haven't found anything on this particular case, only that I had to do myself back the animation on some post by customizing my views.
I will answer myself below.
So I created my custom view and after lots of debugging, I found that I needed to override the setAlpha method :
class CustomViewAnimation(context: Context, attributeSet: AttributeSet) : ConstraintLayout(context, attributeSet) {
private fun callToSuper(alpha: Float) {
super.setAlpha(alpha)
}
override fun setAlpha(alpha: Float) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
super.setAlpha(alpha)
else {
if (this.visibility == View.GONE)
super.setAlpha(0f)
else if (this.visibility == View.VISIBLE && alpha != 1f)
super.setAlpha(0f)
else if (this.visibility == View.VISIBLE && alpha == 1f)
this.animate()
.alpha(1f)
.setListener(object : Animator.AnimatorListener {
override fun onAnimationRepeat(animation: Animator?) {}
override fun onAnimationEnd(animation: Animator?) {
callToSuper(1f)
}
override fun onAnimationCancel(animation: Animator?) {}
override fun onAnimationStart(animation: Animator?) {}
})
}
}
}
You also have to do the other part of the animation in your code where you want to change the visibility :
if (view.visibility == View.VISIBLE) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
view.animate()
.alpha(0.0f)
.setListener(object : Animator.AnimatorListener {
override fun onAnimationRepeat(animation: Animator?) {}
override fun onAnimationEnd(animation: Animator?) {
view.alpha = 0.0f
view.visibility = View.GONE
}
override fun onAnimationCancel(animation: Animator?) {}
override fun onAnimationStart(animation: Animator?) {}
})
} else
view.visibility = View.GONE
} else
view.visibility = View.VISIBLE
This may not be the best way due to the lots of conditions, but again, I haven't found anything else and this works very well.
I am working on Animating my view where i animate the translation and scaling of View.
Problem:
If my animation duration is for 2000 ms (2 Sec) i don't want any user event's to interfere in between animation.
Example if Double Tap on View Trigger's the Scaling Animation and scrolling trigger's Translation Animation.Both animation duration is 2 seconds,But if i double tap and scroll one after another it create's weird result.
So i want to stop event's when animation is going on.
Is there any easy solution without maintaining the state of OnGoing animation and overriding the onTouchEvent to disable events?
Lock UI from events:
private void lockUI() {
getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
}
Unlock UI:
private void unlockUI() {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
}
Solution that i used:
Created a State of Animation
private var isAnimationOnGoing: Boolean = false
Setting the State in Animation Listener
translationAnimation.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationRepeat(animation: Animation?) {
}
override fun onAnimationEnd(animation: Animation?) {
isAnimationOnGoing = false
}
override fun onAnimationStart(animation: Animation?) {
isAnimationOnGoing = true
}
})
Use dispatchTouchEvent(ev: MotionEvent?) . to prevent event's to be received by the ViewGroup or by children's of ViewGroup
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
val dispatchTouchEvent = super.dispatchTouchEvent(ev)
if (isAnimationOnGoing) {
return false
}
return dispatchTouchEvent
}