Variables change before onAnimationEnd runs - java

My problem is most likely because of bad design, but anyway :
I have an ArrayList that holds a bunch of ImageViews (terrain blocks).
Then, I iterated through to create the ImageViews programatically, and then set their animation to an animation called moveLeft which just makes the ImageView move left for 10 seconds.
Right after the animation ends, I need to set the ImageView's visibility to GONE because it will be off-screen by then.
The problem is that I used an int count to hold the total number of ImageViews.
moveLeft.setAnimationListener(new Animation.AnimationListener() {
#Override
public void onAnimationStart(Animation animation) {
}
#Override
public void onAnimationEnd(Animation animation) {
terrainArray.get(count - 1).setVisibility(View.GONE);
}
#Override
public void onAnimationRepeat(Animation animation) {
}
});
I'm constantly making new terrain blocks, which means my count variable is constantly changing.
However, onAnimationEnd doesn't run with the same value of count as it does when the animation starts. As a result, it ends up setting some othhe block's visibility to GONE instead.
This is because onAnimationEnd didn't actually run until 10 seconds AFTER the block was created, because the animation lasts 10 seconds.
As a result, I'm left with the wrong block number when I want to set its visibility because MORE blocks are being created AS the animation is running. (therefore increasing count's value)
Is there some sort of way I could store count's current value on the onAnimationStart method
and then pass that same value to the onAnimationEnd method? (And make it work for up to infinite numbers of terrain blocks? Also, there's only one listener, and multiple blocks can possibly end their animation at the same time.)

This is easy. If you are sure that you need the exact value of count that is at the onAnimationStart(), then create a variable in the anonymous class. Save count value in onAnimationStart() and use it in onAnimationEnd()
You can copy the following snippet.
moveLeft.setAnimationListener(new Animation.AnimationListener() {
private int mCount = 0;
#Override
public void onAnimationStart(Animation animation) {
mCount = count;
}
#Override
public void onAnimationEnd(Animation animation) {
terrainArray.get(mCount - 1).setVisibility(View.GONE);
}
#Override
public void onAnimationRepeat(Animation animation) {
}
});
Hope this helps. But, I am afraid there could be bigger design problems. Be careful boss :)

Related

How reliable is onAnimationEndListener?

I made an mediaplayer app with animations when the users plays the next song, the album art fades out and the album art for the next song fades in.
I first start the fade out animation and if this animation ends i send a broadcast to start the second song.
I would like to know if onAnimationEnd is reliable and will always called because if not then my app basically wouldn't work anymore for playing next/prev song.
private void nextSong(){
mAlbumArtLarge.startAnimation(fadeOutAnimation);
mAlbumIvBottom.startAnimation(fadeOutAnimation);
mAlbumArt.startAnimation(fadeOutAnimation);
tvSongTitle.startAnimation(fadeOutAnimation);
tvArtistName.startAnimation(fadeOutAnimation);
tvSongListSize.startAnimation(fadeOutAnimation);
fadeOutAnimation.setAnimationListener(new Animation.AnimationListener() {
#Override
public void onAnimationStart(Animation animation) {
}
#Override
public void onAnimationEnd(Animation animation) {
if (Main.getInstance().mServiceIsBound) {
Main.getInstance().mediaPlayerService.startActionNextSong(getApplication(), songList, songIndex);
}
}
#Override
public void onAnimationRepeat(Animation animation) {
}
});
}
From my experience it wasn't called in case you run an animation during the start onCreate()of activity in view.post{ startAnimation() } and by going to background during beginning of animation it didn't called the listener. Eventually the problem for me was usage of post(Runnable) on queue to execute on main thread, as it was called in onCreate(). I had resolved it by using onWindowFocusChanged() + post{}. Will update my answer after sometime of using it in production. For now tested it on different devices/APIs no problem occurred.
To think of : you can add some (safety workaround) Handler with timer (like 5 sec if animation doesn't ends you do next step there after 5s)

Android - View.animate skipping

I'm working on animating a View from within a GridLayout. When I access the view from the View.OnClickListener onClick(View view) method the animation runs smoothly.
However, when I get Views via gridLayout.getChildAt(i) the animation skips straight to the ending position.
view.animate()
.translationX(viewAnimation.xTransform)
.translationY(viewAnimation.yTransform)
.setDuration(200)
.setListener(new Animator.AnimatorListener() {
#Override
public void onAnimationStart(Animator animator) {
//Code
}
#Override
public void onAnimationEnd(Animator animator) {
if (reloadGrid)
//Code
else
animate(viewAnimations);
}
//Other methods here
});
I'm really unsure as to why when the View is passed as a parameter it animates fine, however otherwise it skips.
Any advice would be great!
Edit
I have also checked and the difference between the 2 'View' objects is one has mDrawable set and one doesn't. I believe this could be the cause?
I'm not really sure why this solves the problem, however accessing each View within the GridLayout via findViewById(i) seems to work, and then allow the animation to run smoothly.
This took way too long to figure out.

How can I shake ViewPager to show the user that there are more pages available?

I have a ViewPager in my app, and I'd like it to shake when it is loaded for the first time. This is to show the user that there are more pages available. How can I achieve that programmatically?
However, I don't want the pager to automatically scroll to the next page.
ViewPager has a method for swiping programmatically: fakeDragBy
https://developer.android.com/reference/android/support/v4/view/ViewPager.html#fakeDragBy(float)
Here's how you can achieve this:
Call viewPager.beginFakeDrag()
Start an animator. In the AnimatorUpdateListener override onAnimationUpdate so it calls viewPager.fakeDragBy()
In the AnimatorListener override onAnimationEnd so it calls viewPager.endFakeDrag() After this call, the ViewPager should settle back to the initial page.
vpaliyX's answer using translation won't show the next page. You need to use fakeDragBy so that the user can briefly see the edge of the next page.
As requested, here is a code snippet:
private float drag;
...
#Override
protected void onResume() {
super.onResume();
mViewPager.postDelayed(new Runnable() {
#Override
public void run() {
mViewPager.beginFakeDrag();
drag = 0;
float density = getResources().getDisplayMetrics().density;
// 120 is the number of dps you want the page to move in total
ValueAnimator animator = ValueAnimator.ofFloat(0, 120 * density);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
#Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
float progress = (float) valueAnimator.getAnimatedValue();
mViewPager.fakeDragBy(drag - progress);
drag = progress;
}
});
animator.addListener(new Animator.AnimatorListener() {
#Override
public void onAnimationStart(Animator animator) { }
#Override
public void onAnimationEnd(Animator animator) {
mViewPager.endFakeDrag();
}
#Override
public void onAnimationCancel(Animator animator) { }
#Override
public void onAnimationRepeat(Animator animator) { }
});
animator.setDuration(400);
animator.start();
}
}, 300);
}
I put in a 300 ms start delay. I don't really know if that's necessary or not.
Note the expression drag - progress which gives a negative number. To get a leftward swipe, you have to use a negative value.
Also note that I didn't include any code to determine if this was the first time the app was opened.
You can play with the start delay, the animation duration, and the number of dps to swipe until you get the effect you are looking for.
If you want to shake it, you can use animation for it.
I had done the same stuff for FloatingActionButton, and it worked perfectly.
For ViewPager check out this code:
ObjectAnimator shakeAnimation=ObjectAnimator.ofFloat(yourViewPager,View.TRANSLATION_X,-25f,25f);
shakeAnimation.setDuration(100);
shakeAnimation.setRepeatCount(2);
shakeAnimation.addListener(new AnimatorListenerAdapter() {
#Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
yourViewPager.setTranslationY(0f);
}
});
shakeAnimation.start();
You can adjust the translation as convenient for, in my case it's just [-25,25].Also you can set a duration of animation, and the repeat count.
You can use this code for your ViewPager when the data has been loaded for the first time, however, you need to determine when the data has been loaded for the first time. But I don't think you will have problems with that.
Good luck!

Android animation only shows the last ArrayList element

I have 5 TextView fields that are meant to display a String and change the text every 10 seconds.
In order to make the change I have an AsyncTask that grabs the next 5 messages (doInBackground) and passes them to onPostExecute to be displayed in the UI.
When I display the messages without an animation, everything works perfectly fine and the messages rotate every 10 seconds via a new AsyncTask every time.
Now, when I attempt to use a simple fade-in/out animation on the TextViews, I can see all of them fading out and then back in with a placeholder string however the actual text messages I want to display are only displayed in the last TextView.
The AsyncTask is an inner-class of my MainActivity.java.
I have implemented an AnimationListener in MainActivity.java and due to that the objects below in onPostExecute ((TextView) mNextTextView & (String) mNextMessageToDisplay) are also declared as global variables in the main class.
Here's AsyncTask's onPostExecute:
protected void onPostExecute(List<String> mFiveMessagesToDisplay) {
super.onPostExecute(mFiveMessagesToDisplay);
for (int mTextViewCounter = 0; mTextViewCounter < 5; mTextViewCounter++) {
// Prepare the next TextView and message to use
mNextTextViewToUse = mTextViewArrayList.get(mTextViewCounter);
mNextMessageToDisplay = mFiveMessagesToDisplay.get(mTextViewCounter);
// Post and animate the message changes in the TextView
mNextTextViewToUse.startAnimation(mFadeOutAnimation);
}
}
And my AnimationListener:
private AnimationListener mFadeOutAnimListener = new AnimationListener() {
public void onAnimationStart(Animation animation) {}
public void onAnimationRepeat(Animation animation) {}
public void onAnimationEnd(Animation animation) {
mNextTextViewToUse.setVisibility(View.INVISIBLE);
mNextTextViewToUse.setText(mNextMessageToDisplay);
mNextTextViewToUse.setVisibility(View.VISIBLE);
mNextTextViewToUse.startAnimation(mFadeInAnimation);
}
}
As you can see, my setText() method is applied on the mNextTextViewToUse object with the contents of mNextMessageToDisplay, which are as mentioned before both global variables in the main class.
Both ArrayLists, the one containing the messages and the other containing the correct, I have iterated over both and printed the contents - everything is in its place.
Reminding again that if I use setText() with the same TextView and String objects under onPostExecute, everything works perfectly fine just without the animation.
Many thanks in advance!
Your issue is that the loops finishes by the time the animation ends, that's why you see only the last message.
Lets say that mNextTextViewToUse points to TextView1. You start a fade out animation for this TextView. By the time the animation ends, mNextTextViewToUse points to a different textView object (because the loop continue to run while the fade out animation is playing) so when onAnimationEnd is being called (by the fade out animation of TextView1) mNextTextView points to TextView5, and basically you are setting the text to this text view only.
There are several ways to solve this / implement what you need, personally I would probably create my own TextView class (extending TextView of course) and I'll add a public method called setTextAnimated (String newText). It will look something like:
public void setTextAnimated (String newText) {
mNextText = newText;
startAnimation(mFadeOutAnim);
}
where your onAnimationEnd callback will simple call setText(mNextText) and then will start fade-in animation.
Using this you will avoid another issue - that if you use the same animation object on different views - they will share the same timeline and interpolator. The result is that if View1 start a fade out animation that should take 200ms, and View 5 starts the same animation after 140ms, View5 will immediately "jump" to the same alpha value that View1 is.
I found the solution!
I ended up completely removing the FOR loop logic and using recursion which raises a Boolean flag once it has used all 5 available TextViews.
The method that gets the next TextView and String message and execute the animated setText():
public void updateTextView(List<String> mMessages) {
mNextTextViewToUse = mTextViewArrayList.get(mCurrentMessageIndex);
mNextMessageToDisplay = mMessages.get(mCurrentMessageIndex);
mNextTextViewToUse.startAnimation(mFadeOutAnimation);
if (mCurrentMessageIndex == 4) {
mStopMessageDisplayRepeat = true;
} else {
mCurrentMessageIndex++;
}
}
The Runnable that calls the method above, while the recursion stop Boolean is false:
public Runnable mUpdateTextViewRunnable = new Runnable() {
final static int POSTDELAY_INTERVAL = 600;
#Override
public void run() {
updateTextView(mFiveMessages);
if (! mStopMessageDisplayRepeat) {
mMessageDisplayRepeatHandler.postDelayed(mUpdateTextViewRunnable, POSTDELAY_INTERVAL);
}
}
};
Execute the Runnable:
public void startTextViewRunnableRepeat() {
mUpdateTextViewRunnable.run();
}
This is how my AsyncTask's onPostExecute() looks like:
#Override
protected void onPostExecute(List<String> mFiveMessagesToDisplay) {
super.onPostExecute(mFiveMessagesToDisplay);
mFiveMessages = mFiveMessagesToDisplay;
startTextViewRunnableRepeat();
}

Android: Two ScaleAnimations performed sequentially

I want to animate a scale an ImageView vertically, wait, then perform another scale on the same ImageView. I've simplified the code below but it's essentially what I want to do:
ScaleAnimation animate = new ScaleAnimation(1,1,1,2);
animate.setDuration(1000);
animate.fillAfter(true);
ScaleAnimation animateAgain = new ScaleAnimation(1,1,2,1);
animate.setDuration(1000);
animate.fillAfter(true);
view.startAnimation(animate);
view.startAnimation(animateAgain);
I've tried multiple ways of waiting for the first animation to finish (Thread.sleep etc.) but it doesn't seem to make any difference. I assume the animation is rendered in the onDraw method or something? I'm not entirely sure how scale animations work so I can't really grasp how to solve my problem.
What's the simplest way of doing what I want to accomplish?
Thanks guys :)
There are a couple of ways:
Attach an AnimationListener to your animations.
ScaleAnimation animate = new ScaleAnimation(1,1,1,2);
animate.setDuration(1000);
animate.fillAfter(true);
animate.setAnimationListener(new AnimationListener() {
#Override
public void onAnimationEnd(Animation animation) {
ScaleAnimation animateAgain = new ScaleAnimation(1,1,2,1);
animateAgain.setDuration(1000);
animateAgain.fillAfter(true);
view.startAnimation(animateAgain);
}
#Override
public void onAnimationRepeat(Animation animation) {
}
#Override
public void onAnimationStart(Animation animation) {
}});
view.startAnimation(animate);
Alternately, you can set an animation delay for your second animation.
ScaleAnimation animateAgain = new ScaleAnimation(1,1,2,1);
animateAgain.setDuration(1000);
animateAgain.fillAfter(true);
animateAgain.setStartOffset(firstAnimationDuration + delay);

Categories

Resources