TL;DR:我正在寻找complete working sample个我称之为"Gmail三段动画"的场景.具体来说,我们想从两个片段开始,如下所示:

两个片段

在发生某些UI事件时(例如,点击片段B中的某些内容),我们需要:

  • 片段A从屏幕上滑到左边
  • 片段B滑动到屏幕的左边缘并收缩以占据片段A腾出的位置
  • 片段C从屏幕右侧滑入,占据片段B腾出的位置

而且,在按下后退按钮时,我们希望这组操作被反转.

现在,我已经看到了很多部分实现;下面我将回顾其中的四个.除了不完整之外,它们都有自己的问题.


@Reto Meier在同一个基本问题上贡献了this popular answer分,这表明你会使用setCustomAnimations()FragmentTransaction.对于两个片段的场景(例如,您最初只看到片段A,并且希望使用动画效果将其替换为新的片段B),我完全同意.但是:

  • 由于您只能指定一个"in"和一个"out"动画,我看不出您将如何处理三片段场景所需的所有不同动画
  • 他的示例代码中的<objectAnimator>使用以像素为单位的硬连线位置,考虑到不同的屏幕大小,这似乎是不切实际的,然而setCustomAnimations()需要动画资源,排除了用Java定义这些内容的可能性
  • 我对zoom 的对象动画师如何与LinearLayout中的android:layout_weight这样的东西按百分比分配空间感到困惑
  • 我不知道片段C在一开始是如何处理的(GONEandroid:layout_weight/0?预设置为0?其他什么?)

@Roman Nurik指出,you can animate any property个,包括你自己定义的那些.这可以帮助解决硬连接位置的问题,但代价是要发明您自己的自定义布局管理器子类.这对一些人有帮助,但我仍然对雷托的睡觉解决方案感到困惑.


this pastebin entry的作者展示了一些诱人的伪代码,基本上是说所有三个片段最初都驻留在容器中,片段C通过hide()事务操作在一开始就隐藏起来.然后,当UI事件发生时,我们使用show()摄氏度和hide()摄氏度.然而,我看不出这是如何处理B改变大小的事实的.它还依赖于这样一个事实,即您显然可以将多个片段添加到同一个容器中,我不确定这是否是长期可靠的行为(更不用说它应该突破findFragmentById(),尽管我可以接受).


this blog post》的作者表示,Gmail根本没有使用setCustomAnimations(),而是直接使用对象动画("只需更改根视图的左边距+更改右视图的宽度").然而,这仍然是一个两段式的AFAICT解决方案,实现再次以像素为单位显示硬线尺寸.


我会继续努力解决这个问题,所以也许有一天我会自己回答这个问题,但我真的希望有人已经为这个动画场景设计出了三段式的解决方案,并可以发布代码(或链接).安卓系统中的动画让我想把头发拔出来,你们这些见过我的人都知道,这在很大程度上是徒劳的.

推荐答案

好的,这是我自己的解决方案,根据@Christopher在问题 comments 中的建议,来自邮箱AOSP应用程序.

https://github.com/commonsguy/cw-omnibus/tree/master/Animation/ThreePane

@weakwire的解决方案让人想起了我的解决方案,尽管他使用的是classic Animation而不是动画师,他使用RelativeLayout条规则来强制定位.从赏金的Angular 来看,他很可能会得到赏金,除非其他有更巧妙解决方案的人给出答案.


简而言之,该项目中的ThreePaneLayout个是LinearLayout个子类,设计用于与三个子元素一起从事景观工作.这些子 node 的宽度可以在布局XML中通过任何想要的方式来设置--我显示的是使用权重,但是您可以通过维度资源或其他方式来设置特定的宽度.第三个子元素--问题中的片段C--的宽度应该为零.

package com.commonsware.android.anim.threepane;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;

public class ThreePaneLayout extends LinearLayout {
  private static final int ANIM_DURATION=500;
  private View left=null;
  private View middle=null;
  private View right=null;
  private int leftWidth=-1;
  private int middleWidthNormal=-1;

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

  void initSelf() {
    setOrientation(HORIZONTAL);
  }

  @Override
  public void onFinishInflate() {
    super.onFinishInflate();

    left=getChildAt(0);
    middle=getChildAt(1);
    right=getChildAt(2);
  }

  public View getLeftView() {
    return(left);
  }

  public View getMiddleView() {
    return(middle);
  }

  public View getRightView() {
    return(right);
  }

  public void hideLeft() {
    if (leftWidth == -1) {
      leftWidth=left.getWidth();
      middleWidthNormal=middle.getWidth();
      resetWidget(left, leftWidth);
      resetWidget(middle, middleWidthNormal);
      resetWidget(right, middleWidthNormal);
      requestLayout();
    }

    translateWidgets(-1 * leftWidth, left, middle, right);

    ObjectAnimator.ofInt(this, "middleWidth", middleWidthNormal,
                         leftWidth).setDuration(ANIM_DURATION).start();
  }

  public void showLeft() {
    translateWidgets(leftWidth, left, middle, right);

    ObjectAnimator.ofInt(this, "middleWidth", leftWidth,
                         middleWidthNormal).setDuration(ANIM_DURATION)
                  .start();
  }

  public void setMiddleWidth(int value) {
    middle.getLayoutParams().width=value;
    requestLayout();
  }

  private void translateWidgets(int deltaX, View... views) {
    for (final View v : views) {
      v.setLayerType(View.LAYER_TYPE_HARDWARE, null);

      v.animate().translationXBy(deltaX).setDuration(ANIM_DURATION)
       .setListener(new AnimatorListenerAdapter() {
         @Override
         public void onAnimationEnd(Animator animation) {
           v.setLayerType(View.LAYER_TYPE_NONE, null);
         }
       });
    }
  }

  private void resetWidget(View v, int width) {
    LinearLayout.LayoutParams p=
        (LinearLayout.LayoutParams)v.getLayoutParams();

    p.width=width;
    p.weight=0;
  }
}

然而,在运行时,无论您最初如何设置宽度,当您第一次使用hideLeft()从显示问题所指的片段A和B切换到片段B和C时,宽度管理将由ThreePaneLayout接管.在ThreePaneLayout的术语中——它与片段没有特定的联系——这三个片段是leftmiddleright.当你拨打hideLeft()时,我们会记录leftmiddle的尺寸,并将三者中任何一个使用的任何重量归零,这样我们就可以完全控制尺寸.在时间点hideLeft()时,我们将right的大小设置为原来的middle.

动画有两个方面:

  • 使用硬件层,使用ViewPropertyAnimator向左转换宽度为left的三个小部件
  • 在自定义伪属性middleWidth上使用ObjectAnimatormiddle宽度从最初的宽度更改为left

(对于所有这些,使用AnimatorSetObjectAnimators可能是一个更好的主意,尽管这目前还有效)

(也有可能middleWidthObjectAnimator否定硬件层的价值,因为这需要相当连续的无效)

(我对动画的理解可能仍有差距,而且我喜欢附加说明)

最终的效果是,left张幻灯片从屏幕上滑下,middle张幻灯片滑到left的原始位置和大小,right张幻灯片在middle的正后方平移.

showLeft()只是简单地颠倒过程,使用相同的动画师组合,只是方向颠倒.

该活动使用一个ThreePaneLayout来容纳一对ListFragment小部件和一个Button. Select 左侧片段中的某个内容会添加(或更新)中间片段.在中间片段中 Select 某物设置Button的标题,再加上ThreePaneLayout的执行hideLeft().如果我们把左边藏起来,按一下,就会执行showLeft();否则,返回将退出活动.由于这并没有使用FragmentTransactions来影响动画,所以我们只能自己管理"后堆栈".

上面链接的项目使用本机片段和本机animator框架.我有同一个项目的另一个版本,使用Android Support fragments backport和NineOldAndroids来制作动画:

https://github.com/commonsguy/cw-omnibus/tree/master/Animation/ThreePaneBC

后端口在第一代Kindle Fire上运行良好,尽管考虑到较低的硬件规格和缺乏硬件加速支持,动画有点不稳定.在Nexus7和其他当前一代的平板电脑上,这两种实现似乎都很顺利.

我当然愿意接受关于如何改进这个解决方案的 idea ,或者其他比我在这里所做的工作(或者使用的@WINKWIRE)提供明显优势的解决方案.

再次感谢所有做出贡献的人!

Android相关问答推荐

更新Jetpack Compose打破了动态色彩

无法从API访问项目详细信息

如何从Android 12的来电中获取电话号码?

在Jetpack Compose中,如何判断屏幕是否已重新组合?

我正在创建一个简单的连接四个游戏,我需要一个弹出式窗口当你赢了

安卓喷气背包组成倒计时动画

从片段导航回来

SDK 33 的问题 - 面向 S+(版本 31 及更高版本)要求在创建 PendingIntent 时指定 FLAG_IMMUTABLE 或 FLAG_MUTABLE 之一

Jetpack Compose X.dp 性能问题?

ComposeView 抢走了 AndroidTV 内容的焦点

在 kotlin 协同 routine 中,如何将数据范围限定为请求路径(以 MDC 为例)?

Koin Android-KMM:我有嵌套范围但注入不起作用

状态值更改时屏幕未重新组合 - Jetpack Compose

如何在 TextButton 中分隔文本和图标

TextField 溢出和软包装不适用于 Compose 约束布局

如何将房间数据库导出到 .CSV

关于launchWhenX和repeatOnLifecycle的问题

使用 Jetpack Compose 的深层链接导航到可组合项

在 android-billing-5.0 中获取 ProductDetails 价格

在delphi中将Jnet_uri转换为Tbitmap