130

TL;DR:我正在寻找我称之为“Gmail 三片段动画”场景的完整工作示例。具体来说,我们希望从两个片段开始,如下所示:

两个片段

对于一些 UI 事件(例如,点击片段 B 中的某些内容),我们希望:

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

而且,在按下 BACK 按钮时,我们希望反转这组操作。

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


@Reto Meier 为同一个基本问题提供了这个流行的答案,表明您将setCustomAnimations()使用FragmentTransaction. 对于两个片段的场景(例如,您最初只看到片段 A,并希望将其替换为使用动画效果的新片段 B),我完全同意。然而:

  • 由于您只能指定一个“进”和一个“出”动画,我看不出您将如何处理三片段场景所需的所有不同动画
  • <objectAnimator>他的示例代码中使用以像素为单位的硬连线位置,考虑到不同的屏幕尺寸,这似乎不切实际,但setCustomAnimations()需要动画资源,排除了在 Java 中定义这些东西的可能性
  • 我不知道缩放的对象动画师如何与按百分比分配空间之类android:layout_weight的事情联系起来LinearLayout
  • 我不知道一开始是如何处理片段 C 的(GONEandroid:layout_weight0?预先动画到 0 的比例?还有别的吗?)

@Roman Nurik 指出您可以为任何属性设置动画,包括您自己定义的属性。这可以帮助解决硬连线位置的问题,代价是发明您自己的自定义布局管理器子类。这对一些人有帮助,但我仍然对 Reto 的其他解决方案感到困惑。


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


这篇博文的作者表示,Gmail 根本没有使用setCustomAnimations(),而是直接使用对象动画器(“你只需更改根视图的左边距 + 更改右视图的宽度”)。然而,这仍然是一个两片段解决方案 AFAICT,并且再次显示的实现以像素为单位硬线尺寸。


我将继续解决这个问题,所以有一天我可能会自己回答这个问题,但我真的希望有人已经为这个动画场景制定了三片段解决方案并可以发布代码(或链接)。Android 中的动画让我想把头发拔掉,而那些看过我的人都知道,这在很大程度上是徒劳的。

4

6 回答 6

67

在github 上上传了我的建议 (正在使用所有 android 版本,尽管强烈建议对此类动画使用视图硬件加速。对于非硬件加速设备,位图缓存实现应该更适合)

带有动画的演示视频在这里(屏幕投射的帧速率慢。实际性能非常快)


用法:

layout = new ThreeLayout(this, 3);
layout.setAnimationDuration(1000);
setContentView(layout);
layout.getLeftView();   //<---inflate FragmentA here
layout.getMiddleView(); //<---inflate FragmentB here
layout.getRightView();  //<---inflate FragmentC here

//Left Animation set
layout.startLeftAnimation();

//Right Animation set
layout.startRightAnimation();

//You can even set interpolators

说明

创建了一个新的自定义RelativeLayout(ThreeLayout)和 2 个自定义动画(MyScalAnimation, MyTranslateAnimation)

ThreeLayout假设其他可见视图具有,将左窗格的权重作为参数weight=1

因此,new ThreeLayout(context,3)创建一个包含 3 个子项的新视图,左窗格占总屏幕的 1/3。另一个视图占据了所有可用空间。

它在运行时计算宽度,一个更安全的实现是在draw()中第一次计算尺寸。而不是在 post()

缩放和平移动画实际上会调整视图大小和移动视图,而不是伪 [scale,move]。请注意,fillAfter(true)它不在任何地方使用。

View2 是 View1 的 right_of

View3 是 View2 的 right_of

设置这些规则之后,RelativeLayout 会处理其他所有事情。动画改变margins(移动)和[width,height]规模

要访问每个孩子(这样你就可以用你的 Fragment 给它充气,你可以打电话

public FrameLayout getLeftLayout() {}

public FrameLayout getMiddleLayout() {}

public FrameLayout getRightLayout() {}

下面展示了2个动画


阶段1

---IN 屏幕----------!-----OUT----

[查看1][_____查看2____][_____查看3_____]

第二阶段

--OUT-!--------IN 屏幕------

[查看1][查看2][_____查看3____]

于 2012-09-07T00:38:30.787 回答
31

好的,这是我自己的解决方案,根据@Christopher 在问题评论中的建议,源自电子邮件 AOSP 应用程序。

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

@weakwire 的解决方案让我想起了我,尽管他使用的是经典Animation而不是动画师,并且他使用RelativeLayout规则来强制定位。从赏金的角度来看,他可能会得到赏金,除非其他人有更巧妙的解决方案但发布了答案。


简而言之,该ThreePaneLayout项目中的 是一个LinearLayout子类,旨在与三个孩子一起在景观中工作。这些孩子的宽度可以通过任何所需的方式在布局 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;
  }
}

但是,在运行时,无论您最初如何设置宽度,宽度管理都会在ThreePaneLayout您第一次用于hideLeft()从显示问题称为片段 A 和 B 切换到片段 B 和 C 时接管。ThreePaneLayout- 与片段没有特定联系 - 这三个部分是left,middleright. 在您致电 时hideLeft(),我们会记录这三者中使用的任何权重的大小left并将其归零,因此我们可以完全控制大小。middle在 的时间点hideLeft(),我们将 的大小设置right为 的原始大小middle

动画分为两部分:

  • 使用 aViewPropertyAnimator将三个小部件向左平移 的宽度left,使用硬件层
  • ObjectAnimator在自定义伪属性上使用amiddleWidthmiddle宽度从它开始的任何位置更改为原始宽度left

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

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

绝对有可能我对动画的理解仍然存在差距,而且我喜欢括号内的陈述)

最终效果是left滑出屏幕,middle滑到 的原始位置和大小left,并right在 的正后方平移middle

showLeft()只需反转过程,使用相同的动画师组合,只是方向相反。

该活动使用 aThreePaneLayout来保存一对ListFragment小部件和一个Button. 选择左侧片段中的某些内容会添加(或更新)中间片段的内容。选择中间片段中的内容设置 的标题Button,并hideLeft()ThreePaneLayout. 按 BACK,如果我们隐藏左侧,将执行showLeft();否则,BACK 退出活动。由于这不FragmentTransactions用于影响动画,我们被困在自己管理那个“回栈”中。

上面链接到的项目使用原生片段和原生动画框架。我有另一个版本的同一项目,它使用 Android 支持片段 backport 和NineOldAndroids动画:

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

backport 在第一代 Kindle Fire 上运行良好,但由于硬件规格较低且缺乏硬件加速支持,动画有点生涩。在 Nexus 7 和其他当代平板电脑上,这两种实现似乎都很顺利。

我当然愿意接受有关如何改进此解决方案的想法,或者其他与我在这里所做的(或@weakwire 使用的)相比具有明显优势的解决方案。

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

于 2012-09-07T12:45:52.943 回答
13

我们构建了一个名为 PanesLibrary 的库来解决这个问题。它比以前提供的更灵活,因为:

  • 每个窗格都可以动态调整大小
  • 它允许任意数量的窗格(不仅仅是 2 个或 3 个)
  • 窗格内的片段在方向更改时正确保留。

你可以在这里查看:https ://github.com/Mapsaurus/Android-PanesLibrary

这是一个演示:http ://www.youtube.com/watch?v=UA-lAGVXoLU&feature=youtu.be

它基本上允许您轻松添加任意数量的动态大小的窗格并将片段附加到这些窗格。希望你觉得它有用!:)

于 2013-02-19T16:51:07.507 回答
6

基于您链接到的示例之一 ( http://android.amberfog.com/?p=758 ),如何为layout_weight属性设置动画?这样,您可以将 3 个片段的重量变化动画在一起,并且您会获得它们都很好地滑动在一起的奖励:

从简单的布局开始。由于我们要制作动画layout_weight,我们需要一个LinearLayout作为 3 个面板的根视图:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">

<LinearLayout
    android:id="@+id/panel1"
    android:layout_width="0dip"
    android:layout_weight="1"
    android:layout_height="match_parent"/>

<LinearLayout
    android:id="@+id/panel2"
    android:layout_width="0dip"
    android:layout_weight="2"
    android:layout_height="match_parent"/>

<LinearLayout
    android:id="@+id/panel3"
    android:layout_width="0dip"
    android:layout_weight="0"
    android:layout_height="match_parent"/>
</LinearLayout>

然后是演示类:

public class DemoActivity extends Activity implements View.OnClickListener {
    public static final int ANIM_DURATION = 500;
    private static final Interpolator interpolator = new DecelerateInterpolator();

    boolean isCollapsed = false;

    private Fragment frag1, frag2, frag3;
    private ViewGroup panel1, panel2, panel3;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        panel1 = (ViewGroup) findViewById(R.id.panel1);
        panel2 = (ViewGroup) findViewById(R.id.panel2);
        panel3 = (ViewGroup) findViewById(R.id.panel3);

        frag1 = new ColorFrag(Color.BLUE);
        frag2 = new InfoFrag();
        frag3 = new ColorFrag(Color.RED);

        final FragmentManager fm = getFragmentManager();
        final FragmentTransaction trans = fm.beginTransaction();

        trans.replace(R.id.panel1, frag1);
        trans.replace(R.id.panel2, frag2);
        trans.replace(R.id.panel3, frag3);

        trans.commit();
    }

    @Override
    public void onClick(View view) {
        toggleCollapseState();
    }

    private void toggleCollapseState() {
        //Most of the magic here can be attributed to: http://android.amberfog.com/?p=758

        if (isCollapsed) {
            PropertyValuesHolder[] arrayOfPropertyValuesHolder = new PropertyValuesHolder[3];
            arrayOfPropertyValuesHolder[0] = PropertyValuesHolder.ofFloat("Panel1Weight", 0.0f, 1.0f);
            arrayOfPropertyValuesHolder[1] = PropertyValuesHolder.ofFloat("Panel2Weight", 1.0f, 2.0f);
            arrayOfPropertyValuesHolder[2] = PropertyValuesHolder.ofFloat("Panel3Weight", 2.0f, 0.0f);
            ObjectAnimator localObjectAnimator = ObjectAnimator.ofPropertyValuesHolder(this, arrayOfPropertyValuesHolder).setDuration(ANIM_DURATION);
            localObjectAnimator.setInterpolator(interpolator);
            localObjectAnimator.start();
        } else {
            PropertyValuesHolder[] arrayOfPropertyValuesHolder = new PropertyValuesHolder[3];
            arrayOfPropertyValuesHolder[0] = PropertyValuesHolder.ofFloat("Panel1Weight", 1.0f, 0.0f);
            arrayOfPropertyValuesHolder[1] = PropertyValuesHolder.ofFloat("Panel2Weight", 2.0f, 1.0f);
            arrayOfPropertyValuesHolder[2] = PropertyValuesHolder.ofFloat("Panel3Weight", 0.0f, 2.0f);
            ObjectAnimator localObjectAnimator = ObjectAnimator.ofPropertyValuesHolder(this, arrayOfPropertyValuesHolder).setDuration(ANIM_DURATION);
            localObjectAnimator.setInterpolator(interpolator);
            localObjectAnimator.start();
        }
        isCollapsed = !isCollapsed;
    }

    @Override
    public void onBackPressed() {
        //TODO: Very basic stack handling. Would probably want to do something relating to fragments here..
        if(isCollapsed) {
            toggleCollapseState();
        } else {
            super.onBackPressed();
        }
    }

    /*
     * Our magic getters/setters below!
     */

    public float getPanel1Weight() {
        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)     panel1.getLayoutParams();
        return params.weight;
    }

    public void setPanel1Weight(float newWeight) {
        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) panel1.getLayoutParams();
        params.weight = newWeight;
        panel1.setLayoutParams(params);
    }

    public float getPanel2Weight() {
        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) panel2.getLayoutParams();
        return params.weight;
    }

    public void setPanel2Weight(float newWeight) {
        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) panel2.getLayoutParams();
        params.weight = newWeight;
        panel2.setLayoutParams(params);
    }

    public float getPanel3Weight() {
        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) panel3.getLayoutParams();
        return params.weight;
    }

    public void setPanel3Weight(float newWeight) {
        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) panel3.getLayoutParams();
        params.weight = newWeight;
        panel3.setLayoutParams(params);
    }


    /**
     * Crappy fragment which displays a toggle button
     */
    public static class InfoFrag extends Fragment {
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle     savedInstanceState) {
            LinearLayout layout = new LinearLayout(getActivity());
            layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
            layout.setBackgroundColor(Color.DKGRAY);

            Button b = new Button(getActivity());
            b.setOnClickListener((DemoActivity) getActivity());
            b.setText("Toggle Me!");

            layout.addView(b);

            return layout;
        }
    }

    /**
     * Crappy fragment which just fills the screen with a color
     */
    public static class ColorFrag extends Fragment {
        private int mColor;

        public ColorFrag() {
            mColor = Color.BLUE; //Default
        }

        public ColorFrag(int color) {
            mColor = color;
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            FrameLayout layout = new FrameLayout(getActivity());
            layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));

            layout.setBackgroundColor(mColor);
            return layout;
        }
    }
}

此外,此示例不使用 FragmentTransactions 来实现动画(相反,它为片段附加到的视图设置动画),因此您需要自己完成所有 backstack/fragment 事务,但与让动画工作的努力相比很好,这似乎不是一个糟糕的权衡:)

可怕的低分辨率视频:http: //youtu.be/Zm517j3bFCo

于 2012-09-06T15:44:01.687 回答
5

这不是使用片段......这是一个有 3 个孩子的自定义布局。当你点击一条消息时,你抵消了 3 个孩子的使用offsetLeftAndRight()和一个动画师。

您可以在JellyBean“开发人员选项”设置中启用“显示布局边界”。幻灯片动画完成后,您仍然可以看到左侧菜单仍然存在,但位于中间面板下方。

它类似于Cyril Mottier 的 Fly-in 应用程序菜单,但有 3 个元素而不是 2 个。

此外,ViewPager第三个孩子的 是这种行为的另一个迹象:ViewPager通常使用片段(我知道他们不必这样做,但我从未见过其他的实现Fragment),并且由于您不能Fragments在另一个内部使用Fragment3 个孩子可能不是碎片....

于 2012-09-06T15:26:52.787 回答
2

我目前正在尝试做类似的事情,除了 Fragment B 缩放以占用可用空间并且如果有足够的空间可以同时打开 3 窗格。到目前为止,这是我的解决方案,但我不确定我是否会坚持下去。我希望有人会提供一个显示正确方式的答案。

我没有使用 LinearLayout 并为权重设置动画,而是使用 RelativeLayout 并为边距设置动画。我不确定这是不是最好的方法,因为它需要在每次更新时调用 requestLayout()。不过在我所有的设备上都很流畅。

所以,我为布局设置动画,我没有使用片段事务。如果片段 C 打开,我手动处理后退按钮以关闭片段 C。

FragmentB 使用 layout_toLeftOf/ToRightOf 保持它与片段 A 和 C 对齐。

当我的应用程序触发显示片段 C 的事件时,我同时滑入片段 C 和滑出片段 A。(2个单独的动画)。相反,当片段 A 打开时,我同时关闭 C。

在纵向模式或较小的屏幕上,我使用稍微不同的布局并在屏幕上滑动片段 C。

要将百分比用于片段 A 和 C 的宽度,我认为您必须在运行时计算它...(?)

这是活动的布局:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/rootpane"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <!-- FRAGMENT A -->
    <fragment
        android:id="@+id/fragment_A"
        android:layout_width="300dp"
        android:layout_height="fill_parent"
        class="com.xyz.fragA" />

    <!-- FRAGMENT C -->
    <fragment
        android:id="@+id/fragment_C"
        android:layout_width="600dp"
        android:layout_height="match_parent"
        android:layout_alignParentRight="true"
        class="com.xyz.fragC"/>

    <!-- FRAGMENT B -->
    <fragment
        android:id="@+id/fragment_B"
        android:layout_width="match_parent"
        android:layout_height="fill_parent"
        android:layout_marginLeft="0dip"
        android:layout_marginRight="0dip"
        android:layout_toLeftOf="@id/fragment_C"
        android:layout_toRightOf="@id/fragment_A"
        class="com.xyz.fragB" />

</RelativeLayout>

将 FragmentC 滑入或滑出的动画:

private ValueAnimator createFragmentCAnimation(final View fragmentCRootView, boolean slideIn) {

    ValueAnimator anim = null;

    final RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) fragmentCRootView.getLayoutParams();
    if (slideIn) {
        // set the rightMargin so the view is just outside the right edge of the screen.
        lp.rightMargin = -(lp.width);
        // the view's visibility was GONE, make it VISIBLE
        fragmentCRootView.setVisibility(View.VISIBLE);
        anim = ValueAnimator.ofInt(lp.rightMargin, 0);
    } else
        // slide out: animate rightMargin until the view is outside the screen
        anim = ValueAnimator.ofInt(0, -(lp.width));

    anim.setInterpolator(new DecelerateInterpolator(5));
    anim.setDuration(300);
    anim.addUpdateListener(new AnimatorUpdateListener() {

        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            Integer rightMargin = (Integer) animation.getAnimatedValue();
            lp.rightMargin = rightMargin;
            fragmentCRootView.requestLayout();
        }
    });

    if (!slideIn) {
        // if the view was sliding out, set visibility to GONE once the animation is done
        anim.addListener(new AnimatorListenerAdapter() {

            @Override
            public void onAnimationEnd(Animator animation) {
                fragmentCRootView.setVisibility(View.GONE);
            }
        });
    }
    return anim;
}
于 2012-09-06T19:25:47.963 回答