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"
Related
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.
Actually I am using recycler view and adding a layout in the rows and I am using flip animation on cardviews(when clicked on it). The problem is when I add multiple items in the recycler the flip animation works only with the first item. I used toast to make sure that click function is working with other items or not, turns out it's working but flip animation is not working with any other items.Can any one help me out here
This is my code
override fun onCardClick(item: PacketModel, position: Int) {
val scale = this.resources.displayMetrics.density
frontCard.cameraDistance= 8000 * scale
backCard.cameraDistance = 8000 * scale
front_anim = AnimatorInflater.loadAnimator(context, R.animator.front_animator) as AnimatorSet
back_anim = AnimatorInflater.loadAnimator(context, R.animator.back_animator) as AnimatorSet
if (isFront){
front_anim.setTarget(frontCard)
back_anim.setTarget(backCard)
front_anim.start()
back_anim.start()
isFront = false
}else
{
front_anim.setTarget(backCard)
back_anim.setTarget(frontCard)
back_anim.start()
front_anim.start()
isFront = true
}
Toast.makeText(context, item.Name , Toast.LENGTH_SHORT).show()
}
}
This is the adapter Class
class PacketAdapter (val packetList: ArrayList<PacketModel> , var clickListener2: onPacketItemClickListener): RecyclerView.Adapter<PacketAdapter.ViewHolder>(){
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val a = LayoutInflater.from(parent?.context).inflate(R.layout.packet, parent, false)
return ViewHolder(a)
}
override fun getItemCount(): Int {
return packetList.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val packet : PacketModel = packetList[position]
holder.intialize(packet, clickListener2)
}
class ViewHolder(itemView : View) : RecyclerView.ViewHolder(itemView)
{
val packetTime = itemView.findViewById<TextView>(R.id.packetTime)
val timeMessage = itemView.findViewById<TextView>(R.id.timeMessage)
fun intialize(item: PacketModel, action: onPacketItemClickListener){
packetTime.text = item.Name
timeMessage.text = item.Age
itemView.setOnClickListener {
action.onCardClick(item, adapterPosition)
}
}
}
interface onPacketItemClickListener{
fun onCardClick (item: PacketModel, position: Int)
}
}
You should place your card flipping code inside your recyclerview adapter so that recyclerview can recycle it as it should be. You can place your card flipping code inside itemview onClicklistener:
itemView.setOnClickListener {
// Place your flipping code here
action.onCardClick(item, adapterPosition)
}
Remove flipping code from onCardClick callback. Let me know if it works fine.
I want to animate a view's width on the swipe of ViewPager (Scale a button on the screen).
I'm using the following function to animate the width:
fun View.animateWidth(targetWidth: Int, duration) {
val prevWidth = width
visibility = View.VISIBLE
val valueAnimator = ValueAnimator.ofInt(prevWidth, targetWidth)
valueAnimator.addUpdateListener { animation ->
layoutParams.width = animation.animatedValue as Int
this#animateWidth.requestLayout()
}
valueAnimator.duration = duration
valueAnimator.start()
}
I'm calling this function on every update that I'm getting from onPageScrollListener of my ViewPager. The problem is that the UI is not getting updated until either I stop touching/ moving my fingers or I get to the next page of the screen. It seems that the UI is blocking my animation until I finish my touch moves.
Any idea?
Edit: This is where I call the animateWidth function:
viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrollStateChanged(state: Int) {}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
button.animateWidth(
(fullWidth * positionOffset).toInt(), 1)
}
override fun onPageSelected(position: Int) { }
})
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 have a TranslateAnimation for a view inside an item of a RecyclerView.Adapter. The animation should be applied to a specific list item when it initially appears, but it only works when you scroll back and the item is recycled again.
My guess is that it has something to do with the RecyclerView's lifecycle, but I couldn't figure out what's causing the animation to not start.
class mAdapter(items: List<String>): RecyclerView.Adapter<mAdapter.ViewHolder>(){
private var mPosition = 0
// The animation will be applied to the first item
override fun getItemViewType(position: Int): Int {
if (position == mPosition){
return 1
} else {
return 0
}
}
override fun onViewRecycled(holder: ViewHolder) {
super.onViewRecycled(holder)
// Animate the view inside the item
if (holder.itemViewType == 1){
holder.animateItem()
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
super.onBindViewHolder(holder, position, payloads)
// Animate the view inside the item
if (holder.itemViewType == 1){
holder.animateItem()
}
}
inner class ViewHolder(view: View): RecyclerView.ViewHolder(view){
val picture: ImageView? = view.findViewById(R.id.picture)
fun animateItem(){
val itemWidth: Float = itemView.width.toFloat()
val animator = TranslateAnimation(-itemWidth, 0f, 0f, 0f)
animator.repeatCount = 0
animator.interpolator = AccelerateInterpolator(1.0f)
animator.duration = 700
animator.fillAfter = true
background?.animation = animator
background?.startAnimation(animator)
}
}
}
If I log a message inside animateItem it will appear when the RecycleView loads but it will not animate until I scroll down and up.
Answer
As NSimon pointed out, the solution is to write a addOnGlobalLayoutListener
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
holder.itemView.viewTreeObserver.addOnGlobalLayoutListener{
holder.animateItem()
}
}
As discussed in the comments, your first animation was actually playing properly.
However, the first time it was invoked, the View had not been fully layered on screen. Therefore, itemView.width.toFloat() was returning 0, and you were animating from 0 to 0.
The quick solution is to encapsulate the launch of the animation inside a GlobalLayoutListener (which is the system telling you that the view has been layered).
Something like this:
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
holder.itemView.viewTreeObserver.addOnGlobalLayoutListener{
holder.animateItem()
}
}
Bare in mind though, that you should remove the globalLayoutListener once you've started the animation (otherwise it stays there forever and will keep triggering if/when something happens to the view). So a better approach would be to create a helper function like this one:
inline fun <T: View> T.afterMeasured(crossinline f: T.() -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (measuredWidth > 0 && measuredHeight > 0) {
viewTreeObserver.removeOnGlobalLayoutListener(this)
f()
}
}
})
}
and call it inside onBindViewHolder like so:
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
holder.itemView.afterMeasured{
holder.animateItem()
}
}
You're correct. The ViewHolder is only recycled sometime after its initial use. Don't do the animation in onViewRecycled. You might try onBindViewHolder instead, although I'm not sure the exact timing you're looking for.