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.
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"
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'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.
Is there a way in Espresso to check the state of a BottomSheetBehavior? For example, I would like to be able to write the following code to check that the BottomSheetBehavior attached to myLayout has state BottomSheetBehavior.STATE_COLLAPSED:
onView(withId(R.id.myLayout)).check(matches(hasBottomSheetBehaviorState(BottomSheetBehavior.STATE_COLLAPSED)))
Are there any Espresso matchers for a Layout's BottomSheetBehavior?
Adding onto Michael's answer, one thing that might help with a more friendly error message if your test fails is a simple function converting the Int into a user friendly message.
private fun getFriendlyBottomSheetBehaviorStateDescription(state: Int): String = when (state) {
BottomSheetBehavior.STATE_DRAGGING -> "dragging"
BottomSheetBehavior.STATE_SETTLING -> "settling"
BottomSheetBehavior.STATE_EXPANDED -> "expanded"
BottomSheetBehavior.STATE_COLLAPSED -> "collapsed"
BottomSheetBehavior.STATE_HIDDEN -> "hidden"
BottomSheetBehavior.STATE_HALF_EXPANDED -> "half-expanded"
else -> "unknown but the value was $state"
}
fun hasBottomSheetBehaviorState(expectedState: Int): Matcher<in View>? {
return object : BoundedMatcher<View, View>(View::class.java) {
override fun describeTo(description: Description) {
description.appendText("has BottomSheetBehavior state: ${getFriendlyBottomSheetBehaviorStateDescription(expectedState)}")
}
override fun matchesSafely(view: View): Boolean {
val bottomSheetBehavior = BottomSheetBehavior.from(view)
return expectedState == bottomSheetBehavior.state
}
}
}
Then if you were to run the test with the original sample.
onView(withId(R.id.myLayout)).check(matches(hasBottomSheetBehaviorState(BottomSheetBehavior.STATE_COLLAPSED)))
It would output the following instead of just a number.
Caused by: junit.framework.AssertionFailedError: 'has BottomSheetBehavior state: collapsed' doesn't match the selected view.
Also worth noting using the following approach does require creating an idling resource to avoid espresso trying to achieve this assertion while the bottom sheet behavior is not settled. There might be a better way, but I made use of the CountingIdlingResource to increment when the bottom sheet was settling and decrementing for everything else which worked in my use case of testing.
bottomSheetBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_SETTLING) {
countingIdlingResource.increment()
} else {
countingIdlingResource.decrement()
}
}
override fun onSlide(bottomSheet: View, slideOffset: Float) {
}
})
I couldn't find an existing Matcher but was able to write one which seems to work for this case. Here is hasBottomSheetBehaviorState in Kotlin:
fun hasBottomSheetBehaviorState(expectedState: Int): Matcher<in View>? {
return object : BoundedMatcher<View, View>(View::class.java) {
override fun describeTo(description: Description) {
description.appendText("has BottomSheetBehavior state $expectedState")
}
override fun matchesSafely(view: View): Boolean {
val bottomSheetBehavior = BottomSheetBehavior.from(view)
return expectedState == bottomSheetBehavior.state
}
}
}
I have a seekbar that goes from 0-12, but I'd like to make it not possible to select one of the values based on some other criteria. Is that possible? i.e. they can slide through 0-12 but it skips 5? Is there a better selection method maybe? I like how minimal the seekbar is to perform this function, but open to other ideas, if removing a selection is not an option.
Easy way to do it
bar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener{
var previousProgress = 0
val disabled = intArrayOf(3,5,7)
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
if (disabled.contains(progress)) seekBar?.progress = previousProgress
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
previousProgress = seekBar?.progress ?: 0
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
}
})