Gmail三片段animation场景的完整工作示例?
TL; DR:我正在寻找一个我将称之为“Gmail三片段animation”场景的完整工作示例 。 具体来说,我们要从两个片段开始,像这样:
在某些UI事件(例如,点击片段B中的某些内容)时,我们希望:
- 片段A滑出屏幕左侧
- 片段B滑动到屏幕的左边缘并缩小以占据由片段A腾出的点
- 碎片C从屏幕右侧滑入,并占据碎片B腾出的空间
而且,在后退button上,我们希望这组操作被颠倒。
现在,我已经看到了很多部分的实现; 我会在下面回顾其中的四个。 除了不完整之外,他们都有自己的问题。
@Reto Meier对同样的基本问题贡献了这个stream行的答案 ,表明你将使用setCustomAnimations()
和FragmentTransaction
。 对于一个双片段的情况(例如,你最初只能看到片段A,并且想用一个新的使用animation效果的片段Breplace它),我完全同意。 然而:
- 既然你只能指定一个“in”和一个“out”animation,我看不到你将如何处理三片段场景所需的所有不同的animation
- 他的示例代码中的
<objectAnimator>
使用像素中的硬连线位置,在屏幕尺寸不同的情况下,这似乎是不切实际的,但setCustomAnimations()
需要animation资源,不能在Java中定义这些东西 - 我不知道如何使用
LinearLayout
android:layout_weight
类的东西来dynamic缩放对象animation,以便按百分比分配空间 - 我不知道如何处理碎片C(
GONE
?android:layout_weight
of0
?pre-animated to scale of 0?else else?)
@Roman Nurik指出, 你可以animation任何属性 ,包括你自己定义的属性 。 这可以帮助解决硬连接位置的问题,这是以创build您自己的自定义布局pipe理器子类为代价的。 这有助于一些,但我仍然对Reto的其他解决scheme感到困惑。
这个pastebin条目的作者显示了一些诱人的伪代码,基本上说,所有三个片段最初将驻留在容器中,通过hide()
事务操作,片段C隐藏在一开始。 然后我们show()
C并hide()
A发生UI事件。 但是,我不知道如何处理B更改大小的事实。 它也依赖于你显然可以添加多个片段到同一个容器的事实,我不知道这是否是长期可靠的行为(更不用说它应该打破findFragmentById()
,虽然我可以生活那)。
这篇博文的作者指出,Gmail根本不使用setCustomAnimations()
,而是直接使用对象animation(“你只需改变根视图的左边距+改变右视图的宽度”)。 但是,这仍然是AFAICT的两部分解决scheme,并且再一次显示了以像素为单位的硬线尺寸。
我会继续阻止这个,所以我有可能在某天将自己回答,但是我真的希望有人为这个animation场景制定了三段解决scheme,并且可以发布代码(或者链接)。 Android中的animation让我想把我的头发拉出来,而那些看到我的人都知道这是一个毫无结果的努力。
在github上传了我的build议(正在使用所有的android版本,但强烈build议这种animation使用硬件加速,对于非硬件加速的设备,位图caching实现应该更好)
演示video与animation是这里 (屏幕播放的慢帧率原因,实际performance非常快)
用法:
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
解释 :
创build了一个新的自定义RelativeLayout(ThreeLayout)和2个自定义animation(MyScalAnimation,MyTranslateAnimation)
ThreeLayout
将左窗格的权重作为参数,假设其他可见视图的weight=1
。
所以new ThreeLayout(context,3)
创build一个新的视图与3个孩子和左窗格中有1/3的总屏幕。 另一种观点占据了所有可用的空间。
它在运行时计算宽度,一个更安全的实现是尺寸首次在draw()中计算。 而不是在后()
缩放和翻译animation实际上resize和移动视图,而不是伪 – [缩放,移动]。 注意fillAfter(true)
不会在任何地方使用。
View2是Right1的View1
和
View3是Right2的View2
设置这些规则RelativeLayout负责其他的一切。 animation会根据比例改变margins
(移动时)和[width,height]
要访问每个孩子(这样你就可以用你的Fragment给它充气
public FrameLayout getLeftLayout() {} public FrameLayout getMiddleLayout() {} public FrameLayout getRightLayout() {}
下面演示了2个animation
阶段1
— IN屏幕———-!—– OUT —-
[视图1] [_____视图2 _____] [_____ View3_____]
2阶段
–OUT – !——– IN屏幕——
[视图1] [视图2] [_____ View3_____]
好的,这里是我自己的解决scheme,来源于电子邮件AOSP应用程序,根据@克里斯托弗在问题评论中的build议。
https://github.com/commonsguy/cw-omnibus/tree/master/Animation/ThreePane
@ weakwire的解决scheme让人想起我的,虽然他使用经典Animation
而不是animation师,他使用RelativeLayout
规则来强制定位。 从赏金的angular度来看,他可能会得到赏金,除非有其他人提供了一个很好的解决scheme,但没有答案。
简而言之,该项目中的ThreePaneLayout
是一个LinearLayout
子类,devise用于在三个孩子的环境中工作。 这些儿童的宽度可以通过任何想要的方法在布局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 hideLeft()
切换到碎片B和C, ThreePaneLayout
接pipe宽度pipe理。在ThreePaneLayout
的术语中 – 与片段没有特定的联系 – 三个片断是left
, middle
, right
。 当你调用hideLeft()
,我们loggingleft
和middle
的大小,并且把任何三个上面使用的权重都归零,所以我们可以完全控制大小。 在hideLeft()
,我们把right
的大小设置为middle
的原始大小。
animation是双重的:
- 使用
ViewPropertyAnimator
将三个小部件向左移动左边的宽度,使用硬件层 - 在
middle
宽度的自定义伪属性上使用middleWidth
将middle
宽度从其开始的宽度更改为left
的原始宽度
(对于所有这些,使用AnimatorSet
和ObjectAnimators
是更好的ObjectAnimators
,尽pipe现在这个方法ObjectAnimators
)
( middleWidth
ObjectAnimator
也可能会否定硬件层的值,因为这需要相当连续的无效)
(我在animation理解方面仍然存在差距,而且我喜欢括号声明)
最终的效果是, left
幻灯片离开屏幕, middle
滑到left
的原始位置和大小, right
翻到右边middle
。
showLeft()
简单地反转过程,使用相同的animation师混合,只是方向相反。
该活动使用ThreePaneLayout
来保存一对ListFragment
小部件和一个Button
。 select左侧片段中的某些内容会添加(或更新内容)中间片段。 select中间片段中的某些内容设置Button
的标题,并在hideLeft()
上执行hideLeft()
。 按BACK,如果我们隐藏左侧,将执行showLeft()
; 否则,BACK退出活动。 由于这不会使用FragmentTransactions
来影响animation,所以我们自己就不能pipe理这个“后退堆栈”。
上面链接的项目使用本地片段和本地animation构架。 我有另一个版本的相同的项目,使用Android支持片段backport和NineOldAndroids的animation:
https://github.com/commonsguy/cw-omnibus/tree/master/Animation/ThreePaneBC
第一代Kindle Fire上的backport工作正常,但由于硬件规格较低,并且缺less硬件加速支持,所以animation有点生涩。 在Nexus 7和其他当代平板电脑上,这两种实现看起来都很顺利。
对于如何改进这个解决scheme,或者其他解决scheme,我提供了比我在这里(或者使用什么@weakwire)更明显的优势的想法,我当然是开放的。
再次感谢所有贡献的人!
我们build立了一个名为PanesLibrary的库来解决这个问题。 它比以前提供的更灵活,因为:
- 每个窗格可以dynamicresize
- 它允许任何数量的窗格(不只是2或3)
- 窗格中的碎片正确保留在方向更改上。
你可以看看这里: https : //github.com/Mapsaurus/Android-PanesLibrary
这是一个演示: http : //www.youtube.com/watch?v= UA-lAGVXoLU&feature= youtu.be
它基本上允许您轻松添加任意数量的dynamic大小的窗格,并将碎片附加到这些窗格。 希望你觉得它有用! 🙂
build立一个你链接的例子( http://android.amberfog.com/?p=758 ),如何animationlayout_weight
属性? 通过这种方式,您可以将3个片段的重量变化一起进行animation处理,并且您可以获得他们都很好地融合在一起的奖励:
从简单的布局开始。 由于我们要animationlayout_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来实现animation(相反,它animation片段被连接到的视图),所以你需要自己做所有的backstack / fragment事务,但是与获得animation的工作相比,很好,这似乎不是一个坏的折衷:)
它在行动中的可怕的低分辨率video:http: //youtu.be/Zm517j3bFCo
这不是使用片段….这是一个3个孩子的自定义布局。 当你点击一条消息时,你使用offsetLeftAndRight()
和一个animation工具来抵消这三个孩子。
在JellyBean
您可以在“开发者选项”设置中启用“显示布局边界”。 当幻灯片animation完成后,您仍然可以看到左侧菜单仍然存在,但在中间面板下方。
这和Cyril Mottier的Fly-in应用菜单很相似,但是有3个元素,而不是2个。
另外,第三个孩子的ViewPager
也是这种行为的另一种performance: ViewPager
通常使用片段(我知道他们不需要,但是我从来没有见过其他Fragment
的实现),并且因为你不能在里面使用Fragments
另一个Fragment
3个孩子可能不是片段….
我目前正在尝试做类似的事情,除了片段B缩放以获得可用空间,并且如果有足够的空间,则3个窗格可以同时打开。 到目前为止,这是我的解决scheme,但我不确定是否要坚持下去。 我希望有人会提供一个正确的答案。
我使用了一个RelativeLayout并为边距设置了animation效果,而不是使用LinearLayout和animation权重。 我不确定这是最好的方法,因为它需要在每次更新时调用requestLayout()。 虽然我所有的设备都很stream畅
所以,我animation的布局,我不使用片段交易。 我手动处理后退button以closures片段C(如果它是打开的)。
FragmentB使用layout_toLeftOf / ToRightOf保持它与片段A和Calignment。
当我的应用程序触发一个事件显示片段C时,我滑入片段C,并同时滑出片段A. (2个独立的animation)。 相反,当片段A开放时,我同时closuresC。
在纵向模式或较小的屏幕上,我使用一个稍微不同的布局,并在屏幕上滑动片段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滑入或滑出的animation:
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; }