3

I have a ViewPager which I need to move as a whole on button press. I use an animation for this.

When I press it, I translate the 'x' for it. I use setFillAfter(true) to keep the new position. But when I change the page of the ViewPager, it jumps back to the original x-position!

I only saw this issue on Android 4.1, with Android 4.0 there is no problem! So it looks like some kind of regression in Android.

I attached a testproject where I could reproduce the issue without all my other stuff around it. I think it is best if you want to help me figure this out to import the project in your Eclipse and see it for yourself.

I also added to video's, one on my HTC One X where I see the issue, and the other on a tablet with Android 4.0, where the issue is not there.

I have been desperately looking to fix this ugly side effect, but no luck till now...

(Sorry for the big movie files...)

Video of Android 4.0 without the side effect

Video Android 4.1 with the side effect

the project where you can reproduce the issue with

Edit:

I added the following:

@Override
public void onAnimationEnd(Animation animation) {
    RelativeLayout.LayoutParams lp = (android.widget.RelativeLayout.LayoutParams) myViewPager.getLayoutParams();
    if (!i)
        lp.setMargins(300,0,0,0);
    else
        lp.setMargins(0,0,0,0);

    myViewPager.setLayoutParams(lp);
}

After that it stays at the correct position, but it 'flickers' quickly, like the animation is still showing at the end and when I change the margin, it still shows the offset it had after animation. Then it jumps to the correct position.

Boy
  • 7,010
  • 4
  • 54
  • 68
  • Could you, please, change the first sentence from "move" to, say, "move as a whole" to make it clear for readers, what you want to achieve. – alexei burmistrov May 23 '13 at 10:07

4 Answers4

3

The main problem seems to be incorrect choice of animation type. You see, View Animation as a tool is not intended to be used with complex interactive objects like ViewPager. It offers only low-cost animation of the drawing place of views. The visual behaivior of the animated ViewPager in response to user-actions is undefined and should not be relied on. Ugly flicks, when you substitute a "gost" with the real object are only natural.

The mechanism, that is intended to use in your case since API 11 is specialized property animator built in Views for optimized performance: ViewPropertyAnimator, or not specialized, but more versatile ObjectAnimator and AnimatorSet.

Property animation makes the View to really change its place and function normally there.

To make project, to use, say, ViewPropertyAnimator, change your listener setting to this:

btn.setOnClickListener(new OnClickListener() {
    boolean b = false;
    @Override
    public void onClick(View v) {
        if(b) {
            myViewPager.animate().translationX(0f).setDuration(700);
        }
        else {
            myViewPager.animate().translationX(300f).setDuration(700);
        }
        b=!b;
    }
});

If you want to use xml configuration only, stick to |ObjectAnimator and AnimatorSet. Read through the above link for further information.

In case, you are anxious to support pre-Honeycomb devices, you can use Jake Warton's NineOldAndroids project. Hope that helps.

alexei burmistrov
  • 1,417
  • 10
  • 13
  • That is awesome man! Thanks a lot, this does exactly what I need!! Even while the tranlation is being performed now, you can flick through the pages. Thanks for taking the time to import the project. Did you receive the bounty? I didn't have the time before to try this and give the points before the deadline... – Boy May 27 '13 at 14:26
1

That's because the Animation's setFillAfter(true) doesn't actually change the position or any attributes of the View; all it does is create a Bitmap of the view's drawing cache and leaves it where the animation ends. Once the screen is invalidated again (ie. changing the page in the ViewPager), the bitmap will be removed and it will appear as if the View is returning to it's original position, when in fact it was already there.

If you want the View to retain it's position after the animation has finished, you need to actually adjust the View's LayoutParams to match your desired effect. To achieve this, you can override the onAnimationEnd method of the Animation, and adjust the LayoutParams of the View inside there.

Once you adjust the LayoutParams, you can remove your call to setFillAfter(true) and your View will actually stay where you expect it to.

Cruceo
  • 6,763
  • 2
  • 31
  • 52
  • Aren't layoutparams only for width and height, not for x/y positioning? – Boy May 13 '13 at 14:38
  • Depends on your layout. If you're doing a RelativeLayout, you can set something like the left margin to the width of the screen if you're trying to keep it outside the boundaries. If AbsoluteLayout (deprecated), you can set the x/y. If you just don't want to see it, you may even just want to hide the View by setting it's visibility in onAnimationEnd to View.GONE, and if you ever need it again you can just set the visibility back to View.VISIBLE in onAnimationStart and then animate it in appropriately. There are lots of ways to do it, but setFillAfter(true) is definitely not the way to go – Cruceo May 13 '13 at 14:43
  • Then why does etFillAfter(true) work perfectly on 4.0 and not on 4.1...frustrating...it is some kind of regression...and see my edit, I added layoutparams and now I have this flickering / jumpy at the end of the animation and setting the margin... – Boy May 13 '13 at 14:52
  • Hmmm, I think you should actually use the setFillAfter(true) to make sure it doesn't jump, in conjunction with the altered LayoutParams to make sure the position stays. My mistake for saying to take it out – Cruceo May 13 '13 at 14:56
  • Maybe to get rid of the jump, try setting its visibility to GONE in onAnimationEnd and then you can set its visibility back after you've adjusted the layout params – Cruceo May 14 '13 at 13:34
  • I already did set it to invisible, then set the new position and then visible, still same problem – Boy May 27 '13 at 13:56
1

Regarding the flicker issue:

I have encountered this issue before, and it stems from the possibility of the onAnimationEnd() call not syncing up with the next layout pass. Animation works by applying a transformation to a View, drawing it relative to its current position.

However, it is possible for a View to be rendered after you have moved it in your onAnimationEnd() method. In this case, the Animation's transformation is still being applied correctly, but the Animation thinks the View has not changed its original position, which means it will be drawn relative to its ENDING position instead of its STARTING position.

My solution was to create a custom subclass of Animation and add a method, changeYOffset(int change), which modifies the y translation that is applied during the Animation's applyTransformation method. I call this new method in my View's onLayout() method, and pass the new y-offset.

Here is some of my code from my Animation, MenuAnimation:

/**
* Signal to this animation that a layout pass has caused the View on which this animation is
* running to have its "top" coordinate changed.
*
* @param change
* the difference in pixels
*/
public void changeYOffset(int change) {
    fromY -= change;
    toY -= change;
}

@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
    float reverseTime = 1f - interpolatedTime;

    float dy = (interpolatedTime * toY) + (reverseTime * fromY);
    float alpha = (interpolatedTime * toAlpha) + (reverseTime * fromAlpha);

    if (alpha > 1f) {
        alpha = 1f;
    }
    else if (alpha < 0f) {
        alpha = 0f;
    }

    t.setAlpha(alpha);
    t.getMatrix().setTranslate(0f, dy);
}

And from the View class:

private int lastTop;

// ...

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    // the animation is expecting that its View will not be moved by the container
    // during its time period. if this does happen, we need to inform it of the change.
    Animation anim = getAnimation();
    if (anim != null && anim instanceof MenuAnimation) {
        MenuAnimation animation = (MenuAnimation) anim;
        animation.changeYOffset(top - lastTop);
    }

    // ...

    lastTop = top;

    super.onLayout(changed, left, top, right, bottom);
}
Jschools
  • 2,698
  • 1
  • 17
  • 18
1

Crucero has it right about setFillAfter not adjusting params post invalidation. When the view is re-layed out (which'll happen the pass after it's invalidated), its layout params will be the ones that always applied, so it should go back to the original position.

And Jschools is right about onAnimationEnd. Strongly encourage you to step through the source code with a debugger, where you'll instructively discover that an update is made that affects the drawn position of the view after onAnimationEnd is fired, at which point you've actually applied the layout params, hence the flicker caused by doubled up offset.

But this can be solved quite simply by making sure you relayout at the right time. You want to put your re-positioning logic at the end of the ui message queue at the time of animation end so that it is polled after the animation but before laying out. There's nowhere that suggests doing this, annoyingly, but I've yet find a reason in any release of the SDK reason why (when doing this just once and not incorrectly using ui thread) this shouldn't work.

Also clear the animation due to another issue we found on some older devices.

So, try:

@Override
public void onAnimationEnd(final Animation animation) {
   myViewPager.post(new Runnable() {
       @Override
       public public void run() {
           final RelativeLayout.LayoutParams lp = (android.widget.RelativeLayout.LayoutParams) myViewPager.getLayoutParams();
           if (!someBooleanIPresume)
               lp.setMargins(300,0,0,0);
           else
               lp.setMargins(0,0,0,0);

           myViewPager.setLayoutParams(lp);
           myViewPager.clearAnimation();
       }
}
Tom
  • 1,773
  • 15
  • 23