Gmail三片段动画场景的完整工作示例?

Com*_*are 124 android android-animation android-fragments

TL; DR:我正在寻找一个完整的工作样本,我将其称为"Gmail三片段动画"场景.具体来说,我们想从两个片段开始,如下所示:

两个片段

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

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

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

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


@Reto迈尔促成这种普遍的回答相同的基本问题,表明你会使用setCustomAnimations()一个FragmentTransaction.对于双片段场景(例如,您最初只看到片段A,并希望使用动画效果将其替换为新的片段B),我完全同意.然而:

  • 由于您只能指定一个"in"和一个"out"动画,我无法看到如何处理三片段场景所需的所有不同动画
  • <objectAnimator>他的代码示例使用像素的硬连线的位置,这似乎是不切实际给出不同的屏幕尺寸,但setCustomAnimations()需要动画资源,排除在Java中定义这些事情的可能性
  • 我很茫然以对比例的对象动画师如何配合之类的东西android:layout_weightLinearLayout为按百分比分配空间
  • 我很茫然,以C片段是如何在开始处理(GONEandroid:layout_weight0?预动画为0?别的一个规模?)

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


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


此博客文章的作者表示Gmail根本不使用setCustomAnimations(),而是直接使用对象动画师("您只需更改根视图的左边距+更改右视图的宽度").然而,这仍然是一个双片段解决方案AFAICT,并且再次显示的实现硬连线尺寸以像素为单位.


我将继续关注此问题,所以有一天我可能会自己回答这个问题,但我真的希望有人为这个动画场景制定了三片段解决方案,并且可以发布代码(或链接).Android中的动画让我想要脱掉头发,而那些见过我的人知道这是一个毫无结果的努力.

wea*_*ire 66

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
Run Code Online (Sandbox Code Playgroud)

解释:

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

ThreeLayout假设另一个可见视图具有,则将左窗格的权重作为参数获取weight=1.

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

它在运行时计算宽度,更安全的实现是在draw()中第一次计算维数.而不是在post()

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

View2是View1的右侧

View3是View2的右侧

设置这些规则后,RelativeLayout会处理其他所有事情.动画改变margins(在移动中)和[width,height]按比例

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

public FrameLayout getLeftLayout() {}

public FrameLayout getMiddleLayout() {}

public FrameLayout getRightLayout() {}
Run Code Online (Sandbox Code Playgroud)

下面展示了2个动画


阶段1

---在屏幕----------!----- OUT ----

[视图1] [_____视图2 _____] [_____ View3_____]

2阶段

--OUT - !--------在屏幕------

[视图1] [视图2] [_____ View3_____]

  • 您的视频显示了良好的帧速率,但您的示例中使用的视图非常简单.在动画期间执行requestLayout()非常昂贵.特别是如果孩子们很复杂(例如ListView).这可能会导致动画不稳定.GMail应用程序不会调用requestLayout.相反,在动画开始之前,将另一个较小的视图放入中间面板. (2认同)

Com*_*are 31

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

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;
  }
}
Run Code Online (Sandbox Code Playgroud)

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

动画有两个:

  • 使用a 使用硬件层ViewPropertyAnimator执行左边三个小部件的宽度转换left
  • 使用ObjectAnimator自定义伪属性middleWidth来将middle宽度从开头的宽度更改为原始宽度left

(有可能最好使用一个AnimatorSetObjectAnimators所有这些,虽然这适用于现在)

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

(这是绝对可能的,我仍然在我的动画的理解的差距,而且我喜欢括号语句)

净效果是left滑出屏幕,middle滑动到原始位置和大小left,并right在右后方翻译middle.

showLeft() 简单地使用相同的动画师组合来反转过程,只需反转方向.

该活动使用a ThreePaneLayout来保存一对ListFragment小部件和一个Button.选择左侧片段中的内容会添加(或更新中间片段的内容).选择中间片段中的某些内容会设置标题Button,并hideLeft()在其上执行ThreePaneLayout.按下BACK,如果我们隐藏左侧,将执行showLeft(); 否则,BACK退出活动.由于这不会FragmentTransactions影响动画效果,因此我们无法自行管理"后台".

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

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

在第一代Kindle Fire上,后端工作正常,但由于硬件规格较低且缺乏硬件加速支持,动画有点不稳定.这两种实现在Nexus 7和其他当代平板电脑上看起来都很流畅.

我当然对如何改进这个解决方案或其他解决方案持开放态度,这些解决方案比我在这里做的(或者@weakwire使用的)提供了明显的优势.

再次感谢所有贡献者!

  • @CommonsWare:我用4.2编译你的代码,很棒的工作.当我点击中间的"ListView"并快速按下按钮然后重复然后整个布局向右移动.它开始在左侧显示额外的空间列.引入此行为是因为我不会让第一个动画(通过点击中间的`ListView`开始)完成并按下后退按钮.我通过在`translateWidgets`方法中用`translationX`替换`translationXBy`和`translateWidgets(leftWidth,left,middle,right);```translateWidgets(0,left,middle,right);``showLeft() `方法. (2认同)
  • 我还在`setMiddleWidth(int value);`方法中用`middle.requestLayout();`替换`requestLayout();`.它可能不会影响性能,因为我猜GPU仍会进行完整的布局传递,任何评论? (2认同)

cod*_*oda 13

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

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

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

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

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


Dan*_*ech 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>
Run Code Online (Sandbox Code Playgroud)

那么演示类:

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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


Thi*_*Roy 5

这不是使用片段....这是一个有3个孩子的自定义布局.当您单击某条消息时,您将使用offsetLeftAndRight()动画师和动画师来抵消3个孩子.

JellyBean您可以启用"显示布局边界",在"Developper选项"设置.幻灯片动画完成后,您仍然可以看到左侧菜单仍在那里,但在中间面板下方.

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

另外,ViewPager第三个孩子的另一个表现就是这种行为:ViewPager通常使用片段(我知道他们没有,但我从未见过其他的实现Fragment),并且因为你不能Fragments在另外Fragment3个孩子里面使用可能不是碎片......