Android TranslateAnimation在动画结束后重置

30

我正在创建一个类似SlideDrawer的东西,但具有更多的自定义选项。基本上它是可以工作的,但在动画结束时会出现闪烁。

进一步解释一下,我使用了TranslateAnimation,然后在这个动画完成后它返回到原始位置,如果我设置了setFillAfter,则布局中的按钮会停止工作。如果我侦听onAnimationEnd并将其他布局设置为View.GONE,那么布局会闪烁。根据判断,在动画结束时,视图返回到调用View.GONE之前的原始位置。

任何建议都将非常棒。谢谢

6个回答

47

这里是与此问题相关的实际bug。

基本上,这意味着当将AnimationListener附加到动画时,onAnimationEnd(...) 方法并不是真正有效的。

解决方法是监听您将动画应用于的视图中的动画事件。例如,如果最初您像这样将动画侦听器附加到动画:

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

然后将此动画应用于 ImageView 上,如下所示

mImageView.startAnimation(mAnimation);
为了解决这个问题,现在必须创建一个自定义的ImageView
public Class myImageView extends ImageView {

然后重写View类的onAnimationEnd方法,并在其中提供所有功能。

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

这是解决此问题的正确方法:在重写的View -> onAnimationEnd(...)方法中提供功能,而不是在附加到动画的AnimationListener的onAnimationEnd(...)方法中提供功能。

这样做可以正常工作,不再出现动画末尾的闪烁。希望这可以帮到你。


看起来这个方法无法解决 Jelly Bean 设备上的问题(尽管在 JB 之前的版本上完美运行!) - cottonBallPaws

27

从API 11开始,您可以使用ObjectAnimator,实际上更改视图属性,例如翻译的情况下,视图将保持在动画结束后到达的位置。

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

更多内容在这里


FYI,您可以使用9oldAndroids库在旧版本的Android上使用相同的API。http://nineoldandroids.com - Patrick Jackson

4

上面的Soham的回答对我有用,尽管当我第一次阅读这个线程时,它并不是立即显而易见的,但值得指出的是,您仍然可以通过在视图上设置单独的监听器,在您的View的onAnimationStart()onAnimationEnd()结束时运行。

例如,如果您的代码需要在动画期间禁用按钮:

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);

目前,someView并不知道myButton的存在,我希望保持这种状态。你可以在自定义视图类中创建一些监听器,以相同的方式调用它:

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();
      }
    }
}

然后在您的其他代码中(可能是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);

这样做可以使你的组件保持干净分离,同时仍然获得一个有效的AnimationListener。


2

使用Soham的答案,这里有一个专门用于淡入淡出动画的ImageView:

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

/*
 * Custom view to prevent flickering on animation end
 * 
 * https://dev59.com/8HE85IYBdhLWcg3wtVxT
 */
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);
}
}

这是我的动画代码:

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

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

1
出现了java.lang.ClassCastException: android.widget.ImageView无法强制转换为com.animate.FadeableImageView的异常。可能的原因是什么? - Joyson

1

摆脱setFillAfter,只需在onAnimationEnd()中使用View.GONE。请参见此处,其中提供了一个示例自定义视图,该视图使用TranslateAnimation实现滑动面板。


@ahmet emrah:你所声称的证据是什么?可以具体说明一下吗? - CommonsWare
当我们需要该视图用于类似连续事件时,“将视图设为不可见”不是一种选择。例如,再次单击动画按钮,再次使用动画滑块等情况。 - radhoo
@radhoo:当然可以。"Gone"表示一个可见性状态(View.GONE);你随时都可以将其切换为可见状态。 - CommonsWare
我不会争论这个问题,因为有很多种做同一件事的方法 - 我只是更喜欢简单的方法。可以通过处理正在被动画化的imageview中的onAnimationEnd来轻松解决这个问题。由于Soham提出的原因或者我们会遇到闪烁问题,不能直接将回调钩子连接到动画本身。在这里没有太多需要改变可见状态的地方。 - radhoo

0

所以,我在寻找我的Xamarin项目的答案,但我想它也适用于Java。我意识到的是,被动画化的LinearLayout始终具有相同的位置(例如,它位于x = 100,y == 100),您的动画应该相对于此位置。

ObjectAnimator绝对是正确的选择,这是我的解决方案:

首先,一个简单的布局,顶部有一些文本,下面是一个LinearLayout,它是动画的目标...

<?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>

我的活动看起来像下面这样:

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);
            }
        }
    }
}

最重要的一点是,_distanceToTravel(在这种情况下沿Y轴翻译)相对于我们正在动画化的LinearLayout的Top属性。假设每个包含文本的LinearLayout(始终显示,隐藏第一行,隐藏第二行)的高度为20(总高度为60)。滑块,比如说,具有2100的Top属性。因为它位于底部,隐藏两行意味着我们必须将LinearLayout linMySlider向下移动40以隐藏两行,只留下第一个可见。如果您认为LinearLayout始终为2100,则在向下滑动时将其加上40就有意义了(好吧,不是我们,而是Animator为我们执行此操作),这在第一个OfFloat行中很明显,其中开始Y位置为0(即相对于2100的0,因此等于2100),其结束Y位置为_distanceToTravel(即40,但再次相对,因此实际上等于2140)。在相反的方向上,我们从Y的_distanceToTravel开始(再次为40,但实际上为2140),并以0结束(您猜到了,距离2100为0,因此为2100)。
希望这一切都说得通——我花了一点时间才明白,但它非常有效,没有闪烁,也没有重置回原始位置(以前总是会出现这种情况)。希望Java代码和这个C#示例一样适用。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接