マテリアルデザインでトランジションを使用すると、アプリに視覚的な連続性が与えられます。 ユーザーがアプリケーションを歩き回っている間、その中のインターフェース要素の状態が変わります。 ある画面から別の画面への対応する要素の遷移のアニメーションは、インターフェースが具体的であるという考えを強調しています。
この記事の目的は、Android OSのフラグメント間の特定の継続的な移行のガイドラインと実装を提供することです。 「共有要素」を使用して遷移に関与する要素を決定するために、RecyclerViewの画像からViewPager内の画像へ、またはその逆に遷移を実装する方法を示します。 また、元々グリッドの画面から外れていた要素にページをスクロールした後、グリッドに切り替えるという難しいケースも処理します。
これが私たちが達成したい結果です(カットされたアニメーション版)。

説明をスキップしてすぐにコードの学習を開始したい場合は、 ここで見つけることができます 。
翻訳者から。 さらに、多くのコードとgif(推定によると、20メガバイト)があります。
一般的な要素は何ですか?
共通の要素を使用したトランジションは、2つのフラグメントに存在するビューがそれらの間を移動する方法を定義します。 たとえば、AとBの両方でImageViewに表示される画像は、Bが表示されるとAからBに渡されます。
一般的な要素がどのように機能するか、およびフラグメント間の基本的な遷移を実装する方法を説明する以前に公開された多くの例があります。 この記事では、基本をスキップし、代わりにViewPagerの移行実装の機能について説明します。 ただし、移行について詳しく知りたい場合は、Android開発者サイトで移行について読み始め、Google I / O 2016でこのプレゼンテーションをご覧ください。
難しさ
共通要素のマッピング
シームレスな往復トランジションを提供したいと考えています。 これは、グリッドから詳細画面への遷移です(元のページャーで、翻訳では下の「ページ」という用語を使用します)。ユーザーがページを別の画像に切り替えると、グリッド内の関連する画像に戻ります。
これを行うには、共通要素のマッピングを動的に再割り当てして、Android移行システムに魔法の発生に必要なすべてのものを提供する方法を見つける必要があります。
遅延読み込み
一般的な要素の遷移には強力な機能がありますが、それらに移動する前にロードする必要がある要素を操作することは依然として困難です。 ターゲットフラグメントのビューがまだレイアウトを完了しておらず、たとえば画像がロードされている場合、移行は期待どおりに機能しない可能性があります。
このプロジェクトには、読み込み時間が共通要素間の遷移に影響する2つの領域があります。
- ViewPagerは、ネストされたフラグメントをロードするのに数ミリ秒かかります。 また、ページの表示されたフラグメントにイメージをダウンロードするのに時間がかかります(ネットワーク経由でイメージをダウンロードする時間も含めることができます)。
- また、RecyclerViewは、ビューでの画像の読み込みが遅れます。
デモデザイン
基本構造
移行のまさに核心に突入する前に、デモアプリケーションの構造について少し説明します。
MainActivityはGridFragmentをロードして、写真で構成されるRecyclerViewを表示します。 RecyclerViewアダプターは、ピクチャのリスト(ItemDataクラスで定義された不変配列)をロードし、画面上のGridFragmentフラグメントをImagePagerFragmentフラグメントに置き換えることでonClickイベントを管理します。
ImagePagerFragmentアダプターは、ネストされたImageFragmentsをロードして、ユーザーがページをめくるときに個々の画像を表示します。
注 :デモアプリケーションの実装では、 Glideライブラリを使用して、ビュー内の画像を非同期的に読み込みます。 デモアプリケーションの写真は、同梱されています。 ただし、ImageDataクラスを簡単に調整して、インターネット上の画像を指すURLを保存できます。
選択および表示されたアイテムを同期する
選択した画像の位置をフラグメント間で転送するには、MainActivityを同期ポイントとして使用します。
アイテムをクリックするか、ページを変更すると、対応するアイテム番号がMainActivityに渡されます。
保存された位置は、いくつかの場所で使用されます。
- ViewPagerに表示するページを決定します。
- グリッドに戻り、選択したアイテムが表示されることを確認する位置に自動スクロールするとき。
- そしてもちろん、次のセクションで見るように、遷移ハンドラをバインドします。
変換設定
前に述べたように、魔術に必要なすべてを移行システムに提供するために、共通要素のマッピングを動的に変更する方法を見つける必要があります。
XMLマークアップでImageViewのtransitionName
属性を使用して静的マッピングを使用すると、同じレイアウトを使用する一連のビュー(RecyclerViewアダプターによってコンパイルされたビューやImageFragmentによってコンパイルされたビューなど)を扱うため、機能しません。
これを実現するために、移行システムが提供するメソッドを使用します。
-
setTransitionName
を呼び出して、写真の遷移識別子をsetTransitionName
ます。 これにより、ビューが一意の遷移名に関連付けられます。setTransitionName
は、GridFragmentグリッドのRecyclerViewアダプター、およびImageFragmentのonCreateViewでビューをバインドするときに呼び出されます。 どちらの場所でも、プレゼンテーションの識別子として一意の名前または画像リンクを使用します。 -
SharedElementCallbacks
を設定して、SharedElementCallbacks
をインターセプトし、共通要素の名前のビューへのマッピングを修正します。 これは、GridFragmentの終了時およびImagePagerFragmentの開始時に行われます。
FragmentManagerトランザクションのセットアップ
フラグメントを置き換えるトランジションを開始するには、まずFragmentManagerトランザクションの準備中に何かを微調整します。 共通の要素を持つ移行があることをシステムに通知する必要があります。
fragment.getFragmentManager() .beginTransaction() .setReorderingAllowed(true) // setAllowOptimization before 26.1.0 .addSharedElement(imageView, imageView.getTransitionName()) .replace(R.id.fragment_container, new ImagePagerFragment(), ImagePagerFragment.class.getSimpleName()) .addToBackStack(null) .commit();
上記のコードでは、 setReorderingAllowed
true
setReorderingAllowed
ていtrue
。 これにより、フラグメントの状態変更の順序を変更して、遷移の外観を良くすることができます。 追加されたフラグメントについては、 onDestroy
削除されたフラグメントでonDestroy
れる前にonCreate(Bundle)
onDestroy
れます。これにより、移行が始まる前に共通のインターフェイス要素を作成して配置できます。
移行の写真
遷移アニメーション中に画像が新しい場所にどのように変換されるかを判断するために、XMLファイルにTransitionSetをセットアップし、ImagePagerFragmentにロードします。
<ImagePagerFragment.java>
Transition transition = TransitionInflater.from(getContext()) .inflateTransition(R.transition.image_shared_element_transition); setSharedElementEnterTransition(transition);
<image_shared_element_transition.xml>
<?xml version="1.0" encoding="utf-8"?> <transitionSet xmlns:android="http://schemas.android.com/apk/res/android" android:duration="375" android:interpolator="@android:interpolator/fast_out_slow_in" android:transitionOrdering="together"> <changeClipBounds/> <changeTransform/> <changeBounds/> </transitionSet>
一致する共通要素を変更する
GridFragmentを終了するときの変更から始めましょう。 これを行うには、 setExitSharedElementCallback()
を呼び出して、 SharedElementCallback
トランジション名とビューに一致するSharedElementCallback
を渡します。
このハンドラは、フラグメントトランザクションの処理中にフラグメントから呼び出したとき、およびバックスタックからプッシュされたときにフラグメントを入力したときに(逆ナビゲーション中に)呼び出されることに注意することが重要です。 この動作を使用して、一般的なビューの対応をマッピングし、画像のあるページをめくった後にビューが変更された場合に対応できるように遷移を調整します。
この特殊なケースでは、グリッドからViewPagerが表示するフラグメントに1つのImageViewを移動するだけなので、 onMapSharedElements
ハンドラーで受け取った最初の名前付き要素のマッピングを変更するだけです。
<GridFragment.java>
setExitSharedElementCallback( new SharedElementCallback() { @Override public void onMapSharedElements( List<String> names, Map<String, View> sharedElements) { // ViewHolder . RecyclerView.ViewHolder selectedViewHolder = recyclerView .findViewHolderForAdapterPosition(MainActivity.currentPosition); if (selectedViewHolder == null || selectedViewHolder.itemView == null) { return; } // ImageView. sharedElements .put(names.get(0), selectedViewHolder.itemView.findViewById(R.id.card_image)); } });
ImagePagerFragmentを入力するときに、共通要素のマッピングを変更する必要もあります。
これを行うには、 setEnterSharedElementCallback()
を呼び出します。
setEnterSharedElementCallback( new SharedElementCallback() { @Override public void onMapSharedElements( List<String> names, Map<String, View> sharedElements) { // ImageView ( ImageFragment, // ). , // instantiateItem . // // . Fragment currentFragment = (Fragment) viewPager.getAdapter() .instantiateItem(viewPager, MainActivity.currentPosition); View view = currentFragment.getView(); if (view == null) { return; } // ImageView. sharedElements.put(names.get(0), view.findViewById(R.id.image)); } });
移行の延期
移行プロセス中に移動したい画像は、グリッドおよびページにロードするのに時間がかかります。 すべてが正しく機能するためには、参加するビューの準備が整うまで(たとえば、それらのビューのレイアウトが完了し、画像が読み込まれるまで)、移行を延期する必要があります。
これを行うには、フラグメントのonCreateView()
メソッドでpostponeEnterTransition()
を呼び出し、画像が読み込まれると、 startPostponedEnterTransition()
呼び出して遷移を開始します。
注 :「defer」は、グリッドとページの両方で呼び出され、アプリケーションをナビゲートするときに直接遷移と逆遷移を提供します。
Glideを使用して画像をアップロードするため、画像のロード後に遷移をトリガーするハンドラーを設定します。
これは、次の2つの場所で行う必要があります。
- 画像がImageFragmentにロードされると、親ImagePagerFragmentが呼び出されて遷移が開始されます。
- グリッドに戻ると、選択した画像が読み込まれると遷移が始まります。
これは、ImageFragmentが画像をアップロードし、その準備ができたことを親に通知する方法です。
startPostponedEnterTransition
は子ImageFragmentから呼び出されstartPostponedEnterTransition
が、 postponeEnterTransition
への呼び出しはImagePagerFragmentで行われます。
Glide.with(this) .load(arguments.getInt(KEY_IMAGE_RES)) // .listener(new RequestListener<Drawable>() { @Override public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) { getParentFragment().startPostponedEnterTransition(); return false; } @Override public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) { getParentFragment().startPostponedEnterTransition(); return false; } }) .into((ImageView) view.findViewById(R.id.image));
お気づきかもしれませんが、画像の読み込みに失敗した場合、遅延移行の開始も発生します。 これは、ロードが失敗したときにUIがフリーズするのを防ぐためです。
最後の仕上げ
遷移をさらにスムーズにするために、画像がページ画面に移動したときにグリッド要素を暗くしたいと思います。
これを実現するには、TransitionSetを作成し、GridFragmentを終了するトランジションとして使用します。
setExitTransition(TransitionInflater.from(getContext()) .inflateTransition(R.transition.grid_exit_transition));
<?xml version="1.0" encoding="utf-8"?> <transitionSet xmlns:android="http://schemas.android.com/apk/res/android" android:duration="375" android:interpolator="@android:interpolator/fast_out_slow_in" android:startDelay="25"> <fade> <targets android:targetId="@id/card_view"/> </fade> </transitionSet>
出力アニメーションを設定した後の遷移は次のようになります。
お気づきかもしれませんが、移行はまだ完全には洗練されていません。 調光アニメーションは、ページに移動する画像を含むものを含む、すべてのグリッドカードに対して実行されます。
これを修正するために、GridAdapterでフラグメントトランザクションをコミットする前に、選択したカードを終了遷移から除外します。
// view - , , . ((TransitionSet) fragment.getExitTransition()).excludeTarget(view, true);
この変更後、アニメーションは非常に良くなります(選択したカードは、他のカードとは異なり、終了トランジションの一部として隠されません):
最後に、GridFragmentを設定して、ページから戻るときにスクロールするカードを表示します(これはonViewCreated
で行われonViewCreated
)。
recyclerView.addOnLayoutChangeListener( new OnLayoutChangeListener() { @Override public void onLayoutChange(View view, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { recyclerView.removeOnLayoutChangeListener(this); final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); View viewAtPosition = layoutManager.findViewByPosition(MainActivity.currentPosition); // , null (.. // layout-) // . if (viewAtPosition == null || layoutManager.isViewPartiallyVisible(viewAtPosition, false, true)){ recyclerView.post(() -> layoutManager.scrollToPosition(MainActivity.currentPosition)); } } });
まとめると
この記事では、RecyclerViewからViewPagerへ、またはその逆へのスムーズな移行を実装しました。
すべてのビューの準備が整ったら、移行を延期して開始する方法を示しました。 また、アプリケーションのナビゲート中に共通のビューが動的に変化する場合の遷移のアニメーションを提供するために、共通要素の再マッピングを実装しました。
アプリケーションのフラグメント間の遷移に対するこれらの変更により、ユーザーにとって視覚的に連続性が高まりました。
デモアプリケーションコードが添付されています。