TL; DR:「Gmail 3フラグメントアニメーション」シナリオと呼ぶものの完全な作業サンプルを探しています。具体的には、次のような2つのフラグメントから始めます。
UIイベント(フラグメントBの何かをタップするなど)が発生すると、次のようになります。
そして、BACKボタンを押すと、その一連の操作が逆になります。
今、私は多くの部分的な実装を見てきました。それらのうちの4つを以下でレビューします。不完全であることに加えて、彼らはすべて問題を抱えています。
@Reto Meierは、同じ基本的な質問に この一般的な回答 を提供し、FragmentTransaction
でsetCustomAnimations()
を使用することを示しました。 2フラグメントシナリオの場合(たとえば、最初はフラグメントAのみが表示され、アニメーション効果を使用して新しいフラグメントBに置き換えたい場合)、私は完全に同意します。しかしながら:
<objectAnimator>
_は、ピクセル単位のハードワイヤード位置を使用しており、さまざまな画面サイズを考えると非現実的と思われますが、setCustomAnimations()
はアニメーションリソースを必要とするため、 JavaLinearLayout
のような_Android:layout_weight
_のようなものとどのように結び付けられているかについて、私は途方に暮れています。GONE
?_Android:layout_weight
_ of _0
_ ??スケール0に事前にアニメーション化されていますか?)@Roman Nurikは、自分で定義したものも含めて、 任意のプロパティをアニメーション化できる と指摘しています。それは、独自のカスタムレイアウトマネージャーサブクラスを発明することを犠牲にして、配線された位置の問題を解決するのに役立ちます。これは一部の人には役立ちますが、私はまだRetoの残りのソリューションに困惑しています。
このPastebinエントリ の作成者は、基本的に3つのフラグメントすべてが最初にコンテナに存在し、最初はフラグメントCがhide()
トランザクション操作を介して非表示になると言っている興味深いコードを示しています。 UIイベントが発生すると、show()
Cとhide()
Aを実行します。ただし、Bがサイズを変更するという事実をどのように処理するかはわかりません。また、明らかに複数のフラグメントを同じコンテナに追加できるという事実に依存しており、それが長期的に信頼できる動作であるかどうかはわかりません(もちろんfindFragmentById()
私はそれと一緒に暮らすことができます)。
このブログ投稿 の作成者は、GmailがsetCustomAnimations()
をまったく使用せず、代わりにオブジェクトアニメーターを直接使用することを示しています(「ルートビューの左マージンを変更し、右のビュー」)。ただし、これは依然として2フラグメントソリューションであるAFAICTであり、ここに示した実装では、ピクセル単位の寸法が再度配線されています。
私はこれでプラグインを続けますので、いつか自分で答えるかもしれませんが、誰かがこのアニメーションシナリオの3つのフラグメントソリューションを解決し、コード(またはそれへのリンク)を投稿できることを本当に望んでいます。 Androidのアニメーションは、私が私の髪を引っ張り出したいと思うようにします。そして、私を見たあなたの人々は、これがほとんど実りのない努力であることを知っています。
OK、質問のコメントでの@Christopherの提案ごとに、Email AOSPアプリから派生した私自身のソリューションがあります。
https://github.com/commonsguy/cw-omnibus/tree/master/Animation/ThreePane
@weakwireのソリューションは私のものを思い起こさせますが、彼はアニメーターではなく古典的なAnimation
を使用し、RelativeLayout
ルールを使用してポジショニングを実施しています。報奨金の観点から、彼はおそらく報奨金を得るでしょう、他の誰かがよりスマートな解決策でまだ答えを投稿しない限り。
一言で言えば、そのプロジェクトのThreePaneLayout
はLinearLayout
サブクラスであり、3人の子供がいる風景で動作するように設計されています。これらの子の幅は、任意の手段を使用してレイアウトXMLで設定できます。重みを使用して表示しますが、ディメンションリソースなどで特定の幅を設定できます。 3番目の子(質問のフラグメント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と呼ばれるものを表示するように切り替えたときに、幅管理がThreePaneLayout
に引き継がれます。およびBからフラグメントBおよびC。ThreePaneLayout
の用語では、フラグメントと特定の関係はありませんが、3つの部分はleft
、middle
、およびright
です。 。 hideLeft()
を呼び出すときに、left
およびmiddle
のサイズを記録し、3つのいずれかで使用された重みをゼロにするため、サイズを完全に制御できます。 。 hideLeft()
の時点で、right
のサイズをmiddle
の元のサイズに設定します。
アニメーションは2つあります。
ViewPropertyAnimator
を使用して、ハードウェア層を使用して、left
の幅で左に3つのウィジェットの変換を実行しますObjectAnimator
のカスタム擬似プロパティでmiddleWidth
を使用して、middle
の幅を元の幅からleft
の元の幅に変更します(これは今のところ動作しますが、AnimatorSet
とObjectAnimators
をこれらすべてに使用することをお勧めします)
(middleWidth
ObjectAnimator
は、かなり連続した無効化を必要とするため、ハードウェア層の値を無効にすることも可能です)
(間違いなくアニメーションの理解にまだギャップがあり、括弧で囲まれた文が好きな可能性があります)
最終的な効果は、left
が画面からスライドし、middle
が元の位置とサイズのleft
にスライドし、right
がmiddle
のすぐ後ろに移動することです。
showLeft()
は、アニメーターの同じミックスを使用して、単に方向を逆にして、プロセスを単純に逆にします。
アクティビティは、ThreePaneLayout
を使用して、ListFragment
ウィジェットとButton
のペアを保持します。左側のフラグメントで何かを選択すると、中央のフラグメントが追加(またはコンテンツの更新)されます。中央のフラグメントで何かを選択すると、Button
のキャプションが設定され、ThreePaneLayout
でhideLeft()
が実行されます。左側を非表示にした場合、BACKを押すと、showLeft()
;が実行されます。それ以外の場合、BACKはアクティビティを終了します。これは、アニメーションに影響を与えるためにFragmentTransactions
を使用しないため、その「バックスタック」を自分で管理することにこだわっています。
上記にリンクされているプロジェクトは、ネイティブフラグメントとネイティブアニメーターフレームワークを使用します。 Androidサポートフラグメントバックポートと NineOldAndroids をアニメーションに使用する同じプロジェクトの別のバージョンがあります。
https://github.com/commonsguy/cw-omnibus/tree/master/Animation/ThreePaneBC
バックポートは第1世代のKindle Fireで正常に機能しますが、ハードウェアの仕様が低く、ハードウェアアクセラレーションのサポートがないため、アニメーションは少しぎくしゃくしています。両方の実装は、Nexus 7およびその他の現行世代のタブレットではスムーズに見えます。
このソリューションを改善する方法や、ここでやったこと(または@weakwireで使用したもの)よりも明確な利点を提供する他のソリューションについては、私は確かにオープンです。
貢献してくれたすべての人に感謝します!
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
_があると仮定して、左ペインの重みをparamとして取得します。
したがって、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() {}
_
以下に2つのアニメーションを示します
Stage1
--- IN画面----------!----- OUT ----
[View1] [_____ View2 _____] [_____ View3_____]
Stage2
--OUT-!-------- IN画面------
[View1] [View2] [_____ View3_____]
この問題を解決するPanesLibraryというライブラリを構築しました。以前に提供されていたものよりもさらに柔軟です:
こちらで確認できます: https://github.com/Mapsaurus/Android-PanesLibrary
デモは次のとおりです。 http://www.youtube.com/watch?v=UA-lAGVXoLU&feature=youtu.be
基本的に、任意の数の動的サイズのペインを簡単に追加し、それらのペインにフラグメントを添付できます。お役に立てば幸いです! :)
リンクした例の1つを構築する( http://Android.amberfog.com/?p=758 )、layout_weight
プロパティ?この方法で、3つのフラグメントの重みの変化を一緒にアニメーション化でき、それらがすべてうまくスライドするというボーナスが得られます。
シンプルなレイアウトから始めます。 layout_weight
、3つのパネルのルートビューとしてLinearLayout
が必要です。
<?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を使用してアニメーションを実現していません(むしろ、フラグメントがアタッチされているビューをアニメーション化します)。したがって、すべてのバックスタック/フラグメントトランザクションを自分で行う必要があります。うまく、これは悪いトレードオフのようには見えません:)
動作中の恐ろしい低解像度ビデオ: http://youtu.be/Zm517j3bFCo
これはフラグメントを使用していません。3つの子を持つカスタムレイアウトです。メッセージをクリックすると、offsetLeftAndRight()
とアニメーターを使用して3つの子をオフセットします。
JellyBean
では、[開発者オプション]設定で[レイアウト境界の表示]を有効にできます。スライドアニメーションが完了すると、左側のメニューがまだ中央パネルの下にあることがわかります。
Cyril MottierのFly-inアプリメニュー に似ていますが、2ではなく3つの要素があります。
さらに、3番目の子のViewPager
は、この動作の別の指標です。通常、ViewPager
はフラグメントを使用します(必要がないことはわかっていますが、Fragment
)、および別のFragments
内でFragment
を使用できないため、3つの子はおそらくフラグメントではありません。
私は現在、そのようなことをしようとしていますが、フラグメントBスケールは利用可能なスペースを取り、十分なスペースがある場合は3ペインを同時に開くことができます。ここに私の解決策がありますが、私はそれを固守するつもりかどうかはわかりません。誰かが正しい道を示す答えを提供してくれることを願っています。
LinearLayoutを使用してウェイトをアニメートする代わりに、RelativeLayoutを使用してマージンをアニメートします。更新のたびにrequestLayout()を呼び出す必要があるため、最善の方法であるかどうかはわかりません。しかし、私のすべてのデバイスでスムーズです。
だから、私はレイアウトをアニメーション化し、フラグメントトランザクションを使用していません。フラグメント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;
}