Android animation with KTX - android

I have some animations in my Android app and I'd like to change my code in order to take advantage of Android KTX. Sadly, I don't really understand the documentation about it. Could someone tell me how I can improve this code with the help of Android KTX?
view
.animate()
.translationY(view.height.toFloat())
.setDuration(3000)
.setInterpolator(AccelerateInterpolator())
.setListener(object : Animator.AnimatorListener {
override fun onAnimationStart(animation: Animator?) {}
override fun onAnimationRepeat(animation: Animator?) {}
override fun onAnimationCancel(animation: Animator?) {}
override fun onAnimationEnd(animation: Animator?) {
// Do whatever I want
}
})
Of course, I have already added the dependency in my Gradle file:
implementation 'androidx.core:core-ktx:1.0.2'
Thank you very much in advance :)

Android KTX has doOnEnd { /** Do whatever you want **/ } and other extension functions for android.animation.Animator. When you call view.animate(), it returns ViewPropertyAnimator which is not inherited from android.animation.Animator class. Therefore you cannot view.animate().doOnEnd { /** Do whatever you want **/ } provided by Android KTX.
However, you can use withEndAction to do anything when animation ends. withEndAction is part of default ViewPropertyAnimator, so, you do not need to have Android KTX to use that:
view
.animate()
.translationY(image.height.toFloat())
.setDuration(3000)
.setInterpolator(AccelerateInterpolator())
.withEndAction {
// Do whatever you want
}

Android KTX provides some animation event listener functions for Animator instances. As view.animate() returns an instance of ViewPropertyAnimator, you should use a subclass of Animator (like ObjectAnimator) to be able to use KTX capabilities. for example:
ObjectAnimator.ofFloat(view, "translationY", view.height.toFloat()).apply {
duration = 3000
interpolator = AccelerateInterpolator()
// Using KTX extension function:
doOnEnd { /** Do whatever you want **/ }
start()
}

Related

How to wait and continue execution in Kotlin

basically, I am trying to continue execution right after the ad is finished.
Currently, I am using delay which is not really convenient. Some user won't wait or simply dismiss the activity while the delays happen.
My current approach:
override fun onRequestStarted() {
showAd()
}
private fun Context.showAd(): Boolean {
mInterstitialAd?.fullScreenContentCallback = object : FullScreenContentCallback() {
/** Some other function **/
override fun onAdShowedFullScreenContent() {
isAdWatched = true
}
}
override fun onRequestIntent() {
GlobalScope.launch {
delay(10000L)
if (isAdWatched) {
isAdWatched = false
super.onRequestIntent(intent)
}
}
}
You can convert callback-based APIs to suspend functions.
To create a suspend function that returns when the callback fires. For this example, I'm assuming you only want to await the return from the ad, whether or not it was watched. I haven't tested this, but the documentation implies that this particular function onAdDismissedFullScreenContent is the only one you need to respond to to know when the ad is finished (or never loaded).
/** Show the ad and suspend until it is dismissed. */
suspend fun InterstitialAd.showAndAwait(activity: Activity) = suspendCoroutine<Unit> { cont ->
fullScreenContentCallback = object : FullScreenContentCallback() {
override onAdDismissedFullScreenContent() {
cont.resume(Unit)
}
}
showAd(activity)
}
You will have to change your design so the functionality isn't split between these two functions that you overrode. I don't know how you're calling these two functions, so I can't exactly suggest what you need to change. But ultimately, to do what you described, you would have a single function to call that launches a coroutine, and in the coroutine calls the above suspend function to show the ad and then does whatever you want to do next. Something like this:
fun foo() {
lifecycleScope.launch {
mInterstitialAd?.showAndAwait()
doSomethingAfterReturnedFromAd()
}
}
Never use GlobalScope. In the latest version of Kotlin Coroutines, it shows a compiler warning when you use it, although it's not quite deprecated because there are a few very specific cases where it might have useful applications. You should use lifecycleScope for this.

How can I make a TextView visible for a few seconds and stop other Views from showing?

What I am trying to do is retrieve data from the server when I click a button. When I click the button, I want to show my "Loading..." TextView for 2 seconds, and only then show the data I got from the server. How can I do this?
For now my animation is working, but the data is showing almost instantly. I want to delay that. Using Thread.sleep(2000) causes both the data and Loading to be delayed.
val loadingAnimation :TextView = findViewById<TextView>(R.id.loadingAnimationTextView)
val alphaAnim = AlphaAnimation(1.0f, 0.0f)
alphaAnim.startOffset = 0
alphaAnim.duration = 2000
alphaAnim.setAnimationListener(object : AnimationListener {
override fun onAnimationRepeat(animation: Animation?) {
//not sure what code to put here
}
override fun onAnimationEnd(animation: Animation) {
// make invisible when animation completes, you could also remove the view from the layout
loadingAnimation.setVisibility(View.INVISIBLE)
}
override fun onAnimationStart(animation: Animation?) {
loadingAnimation.setVisibility(View.VISIBLE)
}
})
loadingAnimation.setAnimation(alphaAnim)
Thread.sleep(2000)
You can use the handler for this task.
Handler(Looper.getMainLooper()).postDelayed({
// Show you data here
loadingAnimation.setVisibility(View.INVISIBLE)
}, 2000)
Here, 2000 = 2 seconds
It's probably easier to use the ViewPropertyAnimator stuff:
loadingAnimation.animate()
.alpha(0)
.duration(2000)
.withEndAction {
// data displaying code goes here
}.start()
but honestly I don't think there's anything wrong with populating an invisible list, and just making it visible when you want to display it. But that up there is a way to chain runnable code and animations, one after the other

Fade out animation doesn't stay faded out

I have a simple fade out animation function for my android app, it works, but the issue I am having is that, after the fade out animation has run, the view (TextView in this case) does not stay faded out, the alpha value becomes 1 again.
Here is the fadeOut function code:
fun fadeOut(duration: Long = 100) : AlphaAnimation{
val fadeOut = AlphaAnimation(1f, 0f)
fadeOut.interpolator = DecelerateInterpolator()
fadeOut.duration = duration
return fadeOut
}
And I use it like this:
myTextView.startAnimation(fadeOut(500))
Any help or advice will be highly appreciated.
I think Animation::setFillAfter function would do the trick for you, the code would be like this:
val animation = fadeOut(500)
animation.fillAfter = true
myTextView.startAnimation(animation)
Although, this solution only preserves the alpha value of the view after the animation ends, if you want to change the visibility of the view, you need to change it when the animation ends using Animation.AnimationListener interface, the code would be like this:
myTextView.startAnimation(fadeOut(500).apply {
setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation?) {
}
override fun onAnimationEnd(animation: Animation?) {
myTextView.visibility = View.GONE
}
override fun onAnimationRepeat(animation: Animation?) {
}
})
})
You can set visibility in handler after running animation
myTextView.startAnimation(fadeOut(500))
Handler().postDelayed({
myTextView.visibility = View.GONE
},500)

Android Espresso testing SwipeRefreshLayout OnRefresh not been triggered on swipeDown

I'm trying to write simple test for pull to refresh as a part of integration testing. I'm using the newest androidX testing components and Robolectric. I'm testing isolated fragment in which one I'm injecting mocked presenter.
XML layout part
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="#+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerTasks"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
Fragment part
binding.refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
#Override
public void onRefresh() {
presenter.onRefresh();
}
});
Test:
onView(withId(R.id.refreshLayout)).perform(swipeDown());
verify(presenter).onRefresh();
but test doesn't pass, message:
Wanted but not invoked: presenter.onRefresh();
The app works perfectly fine and pull to refresh calls presenter.onRefresh(). I did also debugging of the test and setOnRefreshListener been called and it's not a null. If I do testing with custom matcher to check the status of SwipeRefreshLayout test passes.
onView(withId(R.id.refreshLayout)).check(matches(isRefreshing()));
I did some minor investigation over last weekend since I was facing the same issue and it was bothering me. I also did some comparing with what happens on a device to spot the differences.
Internally androidx.swiperefreshlayout.widget.SwipeRefreshLayout has an mRefreshListener that will run when onAnimationEnd is called. The AnimationEnd will trigger then OnRefreshListener.onRefresh method.
That animation listener (mRefreshListener) is passed to the mCircleView (CircleImageView) and the circle animation start is called.
On a device when the view draw method is called it will call the applyLegacyAnimation method that will, in turn, call the AnimationStart method. At the AnimationEnd, the onRefresh method will be called.
On Robolectric the draw method of the View is never called since the items are not actually drawn. This means that the animation will never run and thus neither will the onRefresh method.
My conclusion is that with the current version of Robolectric is not possible to verify that the onRefresh called due to implementation limitations. It seems though that it is planned to have a realistic rendering in the future.
I'm finally able to solve this using a hacky way :
fun swipeToRefresh(): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View>? {
return object : BaseMatcher<View>() {
override fun matches(item: Any): Boolean {
return isA(SwipeRefreshLayout::class.java).matches(item)
}
override fun describeMismatch(item: Any, mismatchDescription: Description) {
mismatchDescription.appendText(
"Expected SwipeRefreshLayout or its Descendant, but got other View"
)
}
override fun describeTo(description: Description) {
description.appendText(
"Action SwipeToRefresh to view SwipeRefreshLayout or its descendant"
)
}
}
}
override fun getDescription(): String {
return "Perform swipeToRefresh on the SwipeRefreshLayout"
}
override fun perform(uiController: UiController, view: View) {
val swipeRefreshLayout = view as SwipeRefreshLayout
swipeRefreshLayout.run {
isRefreshing = true
// set mNotify to true
val notify = SwipeRefreshLayout::class.memberProperties.find {
it.name == "mNotify"
}
notify?.isAccessible = true
if (notify is KMutableProperty<*>) {
notify.setter.call(this, true)
}
// mockk mRefreshListener onAnimationEnd
val refreshListener = SwipeRefreshLayout::class.memberProperties.find {
it.name == "mRefreshListener"
}
refreshListener?.isAccessible = true
val animatorListener = refreshListener?.get(this) as Animation.AnimationListener
animatorListener.onAnimationEnd(mockk())
}
}
}
}

BottomSheetBehaviour setstate without animation

I have tried the new BottomSheetBehaviour with design library 23.0.2 but i think it too limited. When I change state with setState() method, the bottomsheet use ad animation to move to the new state.
How can I change state immediately, without animation? I don't see a public method to do that.
Unfortunately it looks like you can't. Invocation of BottomSheetBehavior's setState ends with synchronous or asynchronous call of startSettlingAnimation(child, state). And there is no way to override these methods behavior cause setState is final and startSettlingAnimation has package visible modifier. Check the sources for more information.
I have problems with the same, but in a bit different way - my UI state changes setHideable to false before that settling animation invokes, so I'm getting IllegalStateException there. I will consider usage of BottomSheetCallback to manage this properly.
If you want to remove the show/close animation you can use dialog.window?.setWindowAnimations(-1). For instance:
class MyDialog(): BottomSheetDialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
dialog.window?.setDimAmount(0f) // for removing the dimm
dialog.window?.setWindowAnimations(-1) // for removing the animation
return dialog
}
}
If you really need it, then you can resort to reflection:
fun BottomSheetBehavior.getViewDragHelper(): ViewDragHelper? = BottomSheetBehavior::class.java
.getDeclaredField("viewDragHelper")
.apply { isAccessible = true }
.let { field -> field.get(this) as? ViewDragHelper? }
fun ViewDragHelper.getScroller(): OverScroller? = ViewDragHelper::class.java
.getDeclaredField("mScroller")
.apply { isAccessible = true }
.let { field -> field.get(this) as? OverScroller? }
Then you can use these extension methods when the state changes:
bottomSheetBehavior.setBottomSheetCallback(object : BottomSheetCallback() {
override fun onSlide(view: View, offset: Float) {}
override fun onStateChanged(view: View, state: Int) {
if (state == STATE_SETTLING) {
try {
bottomSheetBehavior.getViewDragHelper()?.getScroller()?.abortAnimation()
} catch(e: Throwable) {}
}
}
})
I will add that the code is not perfect, getting fields every time the state changes is not efficient, and this is done for the sake of simplicity.

Categories

Resources