如何在XML中定义特定于屏幕大小的动画

时间:2013-04-08 14:19:17

标签: android android-animation

我在我的应用中为Animation创建了FragmentsAnimation动画在标签之间移动,为此目的,我需要设置当前Fragment的动画,以便在需要Fragment从另一侧滑入时完全移出屏幕。有点像Animation使用的ViewPager

我需要指定View的绝对开始和结束位置,并且因为不同的设备具有不同的维度,所以我无法在XML中定义适合所有设备的Animation。在较大的设备上,View可能无法完全滑动屏幕,而在较小的设备上,View移动到很远,并且当他已经在屏幕外时继续移动。

所以我想我的问题是:如何在XML中定义一个动画,它会在屏幕上滑动Fragment,同时适合所有设备?

我的动画:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">      

  <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/linear_interpolator"
    android:propertyName="x" 
    android:valueType="floatType"
    android:valueTo="720" 
    android:valueFrom="0"
    android:duration="300"/>  

</set>

3 个答案:

答案 0 :(得分:21)

1)关于赏金:

  

Existing answers on SO建议继承FrameLayout ......

如果你想使用ObjectAnimator,你别无选择,只能继承你想要动画的View。您需要为ObjectAnimator提供必要的getter和setter方法才能发挥其魔力,因为它实际上只是调用那些getter和setter方法来执行Animation

您要关联的问题( Animate the transition between fragments )是子类化FrameLayout以添加setXFraction()getXFraction()方法。它们的实现方式是相对于FrameLayout的宽度设置x值。只有通过这样做,ObjectAnimator除了在绝对值之间制作动画外,还可以做任何其他事情。

总而言之,ObjectAnimator本身实际上并没有做太多动画,它只是通过反射来调用getter和setter方法。

  

真的没有办法获得实际的屏幕像素尺寸(不是   只是dp)进入xml文件?

使用ObjectAnimator无法实现这一目标。 ObjectAnimators只是从起始值到结束值进行插值。如上所述,setter方法定义了实际发生的情况。

  

例如,调用返回宽度的自定义函数将是   很好,或定义代码可以设置的常量,代码集说   常量等于屏幕宽度,然后从中访问该常量   xml同样有用。

您无法将代码中的任何值插入任何xml资源。 xml文件中包含的所有内容在构建时都会编译到APK中,并且无法在运行时更改。这也是你的另一个问题的答案:没有资源或常量或任何可以在xml中访问的包含当前屏幕大小的内容。安装应用程序的设备的屏幕大小等动态值不能成为这些资源的一部分,因为构建它们时,其中的所有内容都会编译到您的应用程序中。


2)可能的解决方案:查看动画

一种可能的解决方案是使用视图动画而不是ObjectAnimator。使用视图动画,您可以指定分数而不仅仅是绝对值:

<set xmlns:android="http://schemas.android.com/apk/res/android">  
    <translate android:fromYDelta="-100%" android:toYDelta="0%" android:duration="1000"/>
</set>

这会使屏幕高度从View -100%的{​​{1}}动画显示为0%。换句话说,从完全从屏幕到View的位置。您可以像这样使用它:

Animation animation = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.slide_down);
view.startAnimation(animation);

我意识到这可能对你没有任何帮助。在某些情况下,必须使用ObjectAnimators并且无法使用视图动画。但只要你能够使用视图动画,这几乎可以解决你的问题。


3)最佳实践

我们在这里应该解决的是我认为你的误解。正如您已经注意到,您在xml中定义的每个动画都具有绝对的起始值和结束值。这些值是静态的,在编译应用程序后无法更改。

如果你看看资源如何与许多可用的选择器以及dp,sp ...一起工作,那么你会发现它只是为了做一件事: 例如,您可以定义将View移动100dp的动画。 Dp是物理尺寸的测量值,100dp将是具有任何像素密度的任何屏幕的完全相同的物理尺寸。通过选择器,您可以为具有更小或更大屏幕的设备更改此动画,其中动画可能会移动View太多或太少。但是你只能填写静态值。除了使用选择器之外,您无法为每个设备自定义动画。

所以你看,资源实际上只是为那些静态和不变的东西而设计的。它适用于尺寸或字符串翻译,但有时使用动画可能会有点痛苦。如上所述,通过提供指定分数而非绝对值的选项,只有视图动画提供了一种绕静态特性的方法。但一般来说,你无法在xml中定义任何动态。

为了进一步证实这一论点,请查看Google关于动画的优秀DevBytes视频:

然后您会注意到,这些示例中没有一个动画在xml中定义过。当然有人可能会说他们没有在xml中定义它们,因为他们希望能够更容易地解释和显示代码,但在我看来,这又证明了一点:

您无法在依赖纯动态值的静态资源中定义动画

由于屏幕宽度/高度因设备而异,因此每台设备都需要不同的动画。只有查看动画提供了解决方法,因为它们允许您定义分数而不是绝对值。在任何其他情况下,您将需要以编程方式定义动画。幸运的是ObjectAnimators

并不困难
Animator animator = ObjectAnimator.ofFloat(view, View.X, startValue, endValue);
animator.start(); 

这将从开始到结束值的View的x位置设置动画,以像素为单位。您可以传递View的宽度来为其设置动画,如下所示:

Animator animator = ObjectAnimator.ofFloat(view, View.X, -view.getWidth(), 0.0f);
animator.start();

这会使View动画从左侧滑入。它从屏幕开始完全停止并停在最终位置。你也可以这样做:

view.animate().x(targetValue);

这会将View从其当前x位置设置为目标x值。

但是请不要误解我,你仍然应该尽可能地在资源中定义 - 无论是动画还是其他任何东西。应尽可能避免硬编码动画或任何其他值,除非在您的情况下是必要的。


4)摘要

总结一下:

  • 如果不添加您想要设置动画的ObjectAnimators所需的getter和setter方法,则无法使用View执行操作。
  • 如果可能,请使用view animations。使用它们,您可以根据View的宽度或高度的分数定义动画。
  • 如果上述内容不起作用或不适用于您的情况,请定义动画programmatically,这在任何情况下都有效。

我希望我可以帮助您,如果您有任何其他问题,请随时提出来!

答案 1 :(得分:3)

根据屏幕密度创建不同的文件夹。

因此,假设您的动画xml位于名为anim的文件夹中(在res父文件夹中)。

然后在res中创建anim-ldpi,anim-mdpi等。将各自的动画放在代表屏幕密度的每个文件夹中,android将选择正确的动画。

答案 2 :(得分:0)

有人要求我的工作解决方案。这里是。这是通过Mono的C#代码。希望Java作者能够弄清楚该语言应该是什么。

public class MyFragment: Fragment  {

    public int TransitDuration {
      get {
        return 500;
      }
    }

    public override Animator OnCreateAnimator(FragmentTransit transit, bool enter, int nextAnim) {
      switch (transit) {
        case FragmentTransit.FragmentOpen:
          {
            if (enter) {
              return this.EnterFromLeftAnimator;
            } else {
              return this.ExitToRightAnimator;
            }
          }
        case FragmentTransit.FragmentClose:
          {
            if (enter) {
              return this.EnterFromRightAnimator;
            } else {
              return this.ExitToLeftAnimator;
            }
          }
        default:
          Animator r = base.OnCreateAnimator(transit, enter, nextAnim);
          if (r == null) {
            if (!this.IsAdded) {
              this.OnContextHidden();
            }
          }
          return r;
      }
    }


    public Animator EnterFromLeftAnimator {
      get {
        float width = Screen.MainScreen.PixelSize.Width; // this is an object of mine; other code cached the width there long ago. It would actually be better to use the window width.
        ObjectAnimator animator = ObjectAnimator.OfFloat(this, "X", width, 0);
        animator.SetDuration(this.TransitDuration);
        Animator r = animator as Animator;
        return r;
      }
    }
    public Animator ExitToRightAnimator {
      get {
        float width = Screen.MainScreen.PixelSize.Width;
        ObjectAnimator animator = ObjectAnimator.OfFloat(this, "X", 0, -width);
        animator.SetDuration(this.TransitDuration);
        Animator r = animator as Animator;
        r.AddListener(new AnimatorEndListenerAdapter(() => this.OnContextHidden()));
        return r;
      }
    }
    public Animator EnterFromRightAnimator {
      get {
        float width = this.ScreenWidth;
        ObjectAnimator animator = ObjectAnimator.OfFloat(this, "X", -width, 0);
        animator.SetDuration(this.TransitDuration);
        Animator r = animator as Animator;
        return r;
      }
    }
    public Animator ExitToLeftAnimator {
      get {
        float width = this.ScreenWidth;
        ObjectAnimator animator = ObjectAnimator.OfFloat(this, "X", 0, width);
        animator.SetDuration(this.TransitDuration);
        Animator r = animator as Animator;
        r.AddListener(new AnimatorEndListenerAdapter(() => this.OnContextHidden()));

        return r;
      }
    }

    public override void OnActivityCreated(Bundle savedInstanceState) {
      base.OnActivityCreated(savedInstanceState);
      this.RetainInstance = true;
    }

}