Gmail 三段式动画场景的完整工作样本?

DR: 我正在寻找一个我称之为“ Gmail 三片段动画”场景的 完整的工作样本。具体来说,我们想从两个片段开始,像这样:

two fragments

对于某些 UI 事件(例如,在片段 B 中点击某些东西) ,我们需要:

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

并且,在后退按钮上,我们希望这组操作被反转。

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


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

  • 由于您只能指定一个“ in”和一个“ out”动画,我看不出您将如何处理三个片段场景所需的所有不同动画
  • 在他的示例代码中,<objectAnimator>使用像素的硬连接位置,这似乎是不切实际的,因为屏幕大小不同,然而 setCustomAnimations()需要动画资源,排除了在 Java 中定义这些东西的可能性
  • 我不知道如何对象动画师的比例配合的东西,如 LinearLayout中的 android:layout_weight分配空间的百分比为基础
  • 我不知道在一开始如何处理片段 C (GONE0android:layout_weight?预动画到0?还有别的吗?)

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


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


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


我将继续深入研究这个问题,所以有一天我可能会自己回答这个问题,但是我真的希望有人已经为这个动画场景找到了三个片段的解决方案,并且可以发布代码(或者与之相关的链接)。Android 中的动画让我很想揪头发,你们中那些见过我的人都知道这是一个很大程度上徒劳无功的尝试。

30032 次浏览

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

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

它类似于 Cyril Mottier 的飞入应用菜单,但是使用3个元素而不是2个。

另外,第三个孩子的 ViewPager是这种行为的另一个标志: ViewPager通常使用片段(我知道他们不必,但我从来没有看到其他实现除了 Fragment) ,而且因为你不能在另一个 Fragment中使用 Fragments,所以3个孩子可能不是片段..。

构建您链接到的一个示例(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;
}
}
}

此外,这个例子并没有使用片段事务来实现动画(相反,它使片段附加到的视图具有动画效果) ,所以你需要自己完成所有的回栈/片段事务,但是相对于让动画工作得很好的努力,这似乎并不是一个糟糕的权衡:)

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

我目前正在尝试做一些类似的事情,除了片段 B 规模,以采取可用的空间和3窗格可以在同一时间打开,如果有足够的空间。这是我目前的解决方案,但我不确定我是否会坚持下去。我希望有人会提供一个答案显示正确的方式。

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

所以,我动画的布局,我没有使用片段处理。我手动处理后退按钮,以关闭片段 C,如果它是打开的。

FragmentB 使用 lay _ 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;
}

把我的提案上传到了 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 (Three Layout)和2个自定义动画(MyScalAnimation,MyTranateAnimation)

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

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

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

缩放和翻译动画实际上是调整和移动视图,而不是伪[缩放,移动]。请注意,fillAfter(true)并没有在任何地方使用。

View2是 View1的右边

还有

View3是 View2的右边

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

访问每个子级(这样就可以使用可以调用的片段对其进行充气)

public FrameLayout getLeftLayout() {}


public FrameLayout getMiddleLayout() {}


public FrameLayout getRightLayout() {}

下面是演示的2个动画


第一阶段

———— ! ————出——

[视图1][ _ _ _ _ _ _ _ _ ][ _ _ _ _ _ _ _ _ _ _ _ _ _ ]

第二阶段

——输出—— ! ——在屏幕——

[视图1][视图2][ _ _ _ _ _ _ 视图3 _ _ _ _ ]

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

Https://github.com/commonsguy/cw-omnibus/tree/master/animation/threepane

虽然他使用的是经典的 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;
}
}

然而,在运行时,无论您最初如何设置宽度,当您第一次使用 hideLeft()时,宽度管理将由 ThreePaneLayout接管,从显示被称为片段 A 和 B 的问题切换到片段 B 和 C。在 ThreePaneLayout的术语中——它与片段没有特定的联系——这三个片段是 leftmiddleright。在您调用 hideLeft()的时候,我们记录了 leftmiddle的大小,并且剔除了三者中任何一个所使用的重量,因此我们可以完全控制大小。在 hideLeft()时刻,我们将 right的大小设置为 middle的原始大小。

这些动画有两个方面:

  • 使用一个硬件层,使用 ViewPropertyAnimator将三个小部件向左转换为 left的宽度
  • middleWidth的自定义伪属性上使用 ObjectAnimator,将 middle的宽度从最初的宽度改为 left的原始宽度

(可能对所有这些都使用 AnimatorSetObjectAnimators是一个更好的主意,尽管目前这样做是可行的)

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

(当然可能是我在动画理解方面还有差距,而且我喜欢插入语句)

最终的效果是 left从屏幕上滑落,middle滑落到 left的原始位置和大小,而 rightmiddle后面。

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

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

上面链接到的项目使用了本地片段和本地动画框架。我有另一个版本的同样的项目,使用 Android 支持片段背端口和 九代机器人的动画:

Https://github.com/commonsguy/cw-omnibus/tree/master/animation/threepanebc

后端口在第一代 Kindle Fire 上运行良好,不过由于硬件规格较低且缺乏硬件加速支持,动画有点不稳定。在 Nexus7和其他当代平板电脑上,这两种实现看起来都很平稳。

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

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

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

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

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

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

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