Android TranslateAnimation resets after animation

2019-01-10 07:25发布

问题:

I'm creating something like a SlideDrawer but with most customization, basically the thing is working but the animation is flickering at the end.

To further explain, I got an TranslateAnimation then after this animation it returns back to the original position, if i set setFillAfter then the buttons inside the layout stops working. If i listen to onAnimationEnd and set other's layout to View.GONE the layout fickers. Judging from it is that on animation end, the view goes back to original position before the View.GONE is called.

Any advice would be awesome. Thanks

回答1:

Here is the actual bug related to this issue

This basically states that the onAnimationEnd(...) method doesn't really work well when an AnimationListener is attached to an Animation

The workaround is to listen for the animation events in the view to which you were applying the animation to For example if initially you were attaching the animation listener to the animation like this

mAnimation.setAnimationListener(new AnimationListener() {
    @Override
    public void onAnimationEnd(Animation arg0) {
                       //Functionality here
    }

and then applying to the animation to a ImageView like this

mImageView.startAnimation(mAnimation);

To work around this issue, you must now create a custom ImageView

public Class myImageView extends ImageView {

and then override the onAnimationEnd method of the View class and provide all the functionality there

@Override
protected void onAnimationEnd() {
    super.onAnimationEnd();
    //Functionality here
}

This is the proper workaround for this issue, provide the functionality in the over-riden View -> onAnimationEnd(...) method as opposed to the onAnimationEnd(...) method of the AnimationListener attached to the Animation.

This works properly and there is no longer any flicker towards the end of the animation. Hope this helps



回答2:

From API 11, you can use the ObjectAnimator, which actually changes the view properties, i.e. in the case of a translation, the view will remain at the position it reaches after the animation.

ObjectAnimator objectAnimator= ObjectAnimator.ofFloat(mContent_container, "translationX", startX, endX);
objectAnimator.setDuration(1000);
objectAnimator.start();

More here.



回答3:

Soham's answer above works for me, although it's worth pointing out (since it wasn't immediately obvious to me when first reading this thread) that you can still get very nearly the same behavior as an animation listener by setting a separate listener on the view to be run at the end of your View's onAnimationStart() and onAnimationEnd().

For instance, if your code needs to disable a button for the duration of an animation:

Animation a = getAnimation(/* your code */);
a.setDuration(1000);
a.setAnimationListener(new AnimationListener() {
   @Override
   public void onAnimationStart(Animation arg0) {
     myButton.setEnabled(false);
   }

   @Override
   public void onAnimationEnd(Animation arg0) {
     myButton.setEnabled(true);
   }
});
someView.startAnimation(a);

Currently, someView doesn't know about myButton, and I'd like to keep it that way. You can just create some listener on your custom view class that gets called in the same fashion:

public final class SomeView extends View {
    // other code

    public interface RealAnimationListener {
      public void onAnimationStart();
      public void onAnimationEnd();
    }

    private RealAnimationListener mRealAnimationListener;

    public void setRealAnimationListener(final RealAnimationListener listener) {
      mRealAnimationListener = listener;
    }

    @Override
    protected void onAnimationStart() {
      super.onAnimationStart();
      if (mRealAnimationListener != null) {
         mRealAnimationListener.onAnimationStart();
      }
    }

    @Override
    protected void onAnimationEnd() {
      super.onAnimationEnd();
      if (mRealAnimationListener != null) {
         mRealAnimationListener.onAnimationEnd();
      }
    }
}

And then back in your other code (probably an Activity):

Animation a = getAnimation(/* your code */);
a.setDuration(1000);
someView.setRealAnimationListener(new RealAnimationListener() {
   @Override
   public void onAnimationStart() {
     myButton.setEnabled(false);
   }

   @Override
   public void onAnimationEnd() {
     myButton.setEnabled(true);
   }
});
someView.startAnimation(a);

This way you keep your components separated cleanly while still getting an AnimationListener that works.



回答4:

Using Soham's answer, here is an ImageView specific to fade animations:

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;

/*
 * Custom view to prevent flickering on animation end
 * 
 * http://stackoverflow.com/questions/2650351/android-translateanimation-resets-after-animation
 */
public class FadeableImageView extends ImageView {

public FadeableImageView(Context context) {
    super(context);
}

public FadeableImageView(Context context, AttributeSet attrs) {
    super(context, attrs);
}

public FadeableImageView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
}

@Override
protected void onAnimationEnd() {
    super.onAnimationEnd();
    this.setVisibility(View.GONE);
}
}

And here is my animation code:

protected void startSplash() {
    final FadeableImageView splash = (FadeableImageView) findViewById(R.id.splash);

    Animation fadeOut = new AlphaAnimation(1, 0);
    fadeOut.setDuration(2000);
    splash.startAnimation(fadeOut);
}


回答5:

Get rid of setFillAfter and just use View.GONE in onAnimationEnd(). See here for a sample custom View that implements a sliding panel using a TranslateAnimation.



回答6:

So, I was looking for the answer to this for my Xamarin project, but I guess it should also apply to Java. The realization I had was that the LinearLayout being animated ALWAYS had the same position (say, it was at x=100, y==100) and your animations should be RELATIVE to this position. The ObjectAnimator was definitely the way to go, and here is my solution:

First off, a simple layout with some text at the top and a LinearLayout below that which is the target for the animation....

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:p1="http://schemas.android.com/apk/res/android"
    p1:minWidth="25px"
    p1:minHeight="25px"
    p1:layout_width="match_parent"
    p1:layout_height="match_parent"
    p1:id="@+id/frameLayout1">
    <TextView
        p1:text="Some text at the top"
        p1:textAppearance="?android:attr/textAppearanceLarge"
        p1:id="@+id/txtSomeTextAtTheTop"
        p1:layout_width="wrap_content"
        p1:layout_height="wrap_content"
        p1:layout_gravity="center_horizontal" />
    <LinearLayout
        p1:orientation="vertical"
        p1:minWidth="25px"
        p1:minHeight="25px"
        p1:layout_width="wrap_content"
        p1:layout_height="wrap_content"
        p1:id="@+id/linMySlider"
        p1:layout_gravity="center_horizontal|bottom">
        <LinearLayout
            p1:orientation="horizontal"
            p1:minWidth="25px"
            p1:minHeight="25px"
            p1:layout_width="match_parent"
            p1:layout_height="wrap_content"
            p1:id="@+id/linAlwaysDisplay"
            p1:layout_marginBottom="10px">
            <TextView
                p1:text="ALWAYS ON DISPLAY"
                p1:textAppearance="?android:attr/textAppearanceLarge"
                p1:id="@+id/txtAlwaysDisplay"
                p1:layout_width="wrap_content"
                p1:layout_height="wrap_content"
                p1:layout_gravity="center_horizontal" />
        </LinearLayout>
        <LinearLayout
            p1:orientation="horizontal"
            p1:minWidth="25px"
            p1:minHeight="25px"
            p1:layout_width="match_parent"
            p1:layout_height="wrap_content"
            p1:id="@+id/linToHideLineOne">
            <TextView
                p1:text="To Hide Line One"
                p1:textAppearance="?android:attr/textAppearanceLarge"
                p1:id="@+id/txtHideLineOne"
                p1:layout_width="wrap_content"
                p1:layout_height="wrap_content"
                p1:layout_gravity="center_horizontal" />
        </LinearLayout>
        <LinearLayout
            p1:orientation="horizontal"
            p1:minWidth="25px"
            p1:minHeight="25px"
            p1:layout_width="match_parent"
            p1:layout_height="wrap_content"
            p1:id="@+id/linHideLineTwo">
            <TextView
                p1:text="To Hide Line Two"
                p1:textAppearance="?android:attr/textAppearanceLarge"
                p1:id="@+id/txtHideLineTwo"
                p1:layout_width="wrap_content"
                p1:layout_height="match_parent" />
        </LinearLayout>
    </LinearLayout>
</FrameLayout>

My activity, then, looked like the following:

using System;

using Android.App;
using Android.OS;
using Android.Views;
using Android.Widget;
using Android.Animation;
using Android.Views.Animations;
using Android.Util;

namespace MyNamespace
{
    [Activity(Label = "testActivity")]
    public class testActivity : Activity
    {
        public static string TAG = "M:testActivity";


        //by default we want the slider to be closed, which is why
        // _sliderOpen has been set to true and we animate it into position when
        //the window gets first focus
        private bool _sliderOpen = true;

        private ViewGroup _linMySlider;
        private LinearLayout _linAlwaysDisplays;

        private int _distanceToTravel;

        protected override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);

            SetContentView(Resource.Layout.testLayout);

            _linMySlider = FindViewById<ViewGroup>(Resource.Id.linMySlider);
            _linAlwaysDisplays = FindViewById<LinearLayout>(Resource.Id.linAlwaysDisplay);

            TextView alwaysDisplayText = FindViewById<TextView>(Resource.Id.txtAlwaysDisplay);
            alwaysDisplayText.Click += AlwaysDisplayText_Click;
        }

        private void AlwaysDisplayText_Click(object sender, EventArgs e)
        {
            DoAnimation(500);
        }

        public override void OnWindowFocusChanged(bool hasFocus)
        {
            base.OnWindowFocusChanged(hasFocus);

            if (hasFocus)
            {
                if (_sliderOpen)
                {
                    //we store this one time as it remains constant throught our sliding animations
                    _distanceToTravel = _linMySlider.Height - _linAlwaysDisplays.Height;
                    DoAnimation(1);
                }
            }
        }

        private void DoAnimation(long duration)
        {
            ObjectAnimator slideMe = null;

            try
            {
                switch (_sliderOpen)
                {
                    case true:
                        slideMe = ObjectAnimator.OfFloat(_linMySlider, "translationY", 0, _distanceToTravel);
                        _sliderOpen = false;
                        break;
                    case false:
                        slideMe = ObjectAnimator.OfFloat(_linMySlider, "translationY", _distanceToTravel, 0);
                        _sliderOpen = true;
                        break;
                }
                slideMe.SetInterpolator(new OvershootInterpolator());
                slideMe.SetDuration(duration);
                slideMe.Start();
            }
            catch (Exception e)
            {
                Log.Error(TAG, "DoAnimation: Exception - " + e.Message);
            }
        }
    }
}

The most important point to note is that the _distanceToTravel (in this case translating on the Y axis) is relative to the Top property of the LinearLayout we are animating. Assume that each of the LinearLayouts that hold the text (ALWAYS ON DISPLAY, To Hide Line One, To Hide Line Two) have a height of 20 (making the total height 60). The slider, say, has a Top property of 2100. Because it is located at the bottom, to hide the two lines means we have to move the LinearLayout linMySlider down by 40 in order to hide the two lines, leaving just the first visible. If you think of the LinearLayout as ALWAYS being 2100 it then makes sense that on the slide down we add 40 to it (well, not us, the Animator does this for us), evident in the first OfFloat line, where the start Y position is 0 (ie 0 relative to 2100, so equals 2100) and its end Y position is _distanceToTravel (which is 40, but again relative so equals, in fact, 2140). In the opposite direction we start with the _distanceToTravel for the Y (again 40, but in fact 2140) and we end at 0 (you guessed it 0 away from 2100 and, therefore, 2100).

Hope this all makes sense - it took me a little while for the penny to drop, but it works really well with no flicker and no reset back to the original position (which it always had lol). Hopefully the same applies to Java code as it does in this C# example.