これは、android developer blogのContinuous Shared Element Transitions: RecyclerView to ViewPager 記事を解析した内容になります。
今回のサンプルは、RecyclerViewのGrid形式にして、アイテムをタップするとViewPager形式の
ViewにFadeOutしながら遷移するというもので、
ページビューで遷移元とは異なる画像に切り替えて、Grid画面に戻っても
SharedElementをかけています。
実装方法
まず、グリッド画面となるGridFragmentのonCreateViewで
sharedElementなどのアニメーションの設定をします。
1 2 3 4 5 6 7 8 9 10 11 |
@Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { recyclerView = (RecyclerView) inflater.inflate(R.layout.fragment_grid, container, false); recyclerView.setAdapter(new GridAdapter(this)); prepareTransitions(); postponeEnterTransition(); return recyclerView; } |
prepareTranstion関数では、fragmentを遷移する時の遷移animationとcallbackの設定をしています。
1 2 3 4 5 6 7 8 9 10 |
/** * Prepares the shared element transition to the pager fragment, as well as the other transitions * that affect the flow. */ private void prepareTransitions() { // Fragmentから遷移する際の遷移アニメーションを設定する setExitTransition(TransitionInflater.from(getContext()) .inflateTransition(R.transition.grid_exit_transition)); // ... } |
遷移アニメーション方法はxmlで定義します。
1 2 3 4 5 6 7 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> |
続いて、GridFragmentからViewPagerに遷移する時とViewPagerからGridFragmentに遷移する際
に呼ばれるSharedElementCallbackクラスのセットと実装をします。
onMapSharedElements関数では、
RecyclerViewと対象のPageViewで表示されるImageの遷移先の設定をmapに埋め込んでいます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// A similar mapping is set at the ImagePagerFragment with a setEnterSharedElementCallback. setExitSharedElementCallback( new SharedElementCallback() { @Override public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) { // Locate the ViewHolder for the clicked position. RecyclerView.ViewHolder selectedViewHolder = recyclerView .findViewHolderForAdapterPosition(MainActivity.currentPosition); if (selectedViewHolder == null || selectedViewHolder.itemView == null) { return; } // Map the first shared element name to the child ImageView. sharedElements .put(names.get(0), selectedViewHolder.itemView.findViewById(R.id.card_image)); } }); |
また、ViewPagerが表示されるImagePageFragmentでも同じように遷移先を
SharedElementsにセットしています。
こう設定することで、ViewPagerで他のimageに切り替えた場合でも
ViewPagerで選択したimageをアニメーション対象にすることができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
private void prepareSharedElementTransition() { Transition transition = TransitionInflater.from(getContext()) .inflateTransition(R.transition.image_shared_element_transition); setSharedElementEnterTransition(transition); // A similar mapping is set at the GridFragment with a setExitSharedElementCallback. setEnterSharedElementCallback( new SharedElementCallback() { @Override public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) { // Locate the image view at the primary fragment (the ImageFragment that is currently // visible). To locate the fragment, call instantiateItem with the selection position. // At this stage, the method will simply return the fragment at the position and will // not create a new one. Fragment currentFragment = (Fragment) viewPager.getAdapter() .instantiateItem(viewPager, MainActivity.currentPosition); View view = currentFragment.getView(); if (view == null) { return; } // Map the first shared element name to the child ImageView. sharedElements.put(names.get(0), view.findViewById(R.id.image)); } }); } |
遅延遷移をさせる
サンプルでは、urlから画像をloadする際など、
画像が読み込めていない場合のためにpostponeEnterTransition()
により、遅延遷移の設定をしています。
postponeEnterTransition() を読んだ場合に遷移をスタートさせるためには、
startPostponedEnterTransition()を呼びます。
表示されるGrid Imageの1つ1つはFragmentを継承したImageFragmentクラスになっています。
ImageFragmentクラスのonCreateViewでGlideを使って画像を読み込み
読み込みが終了したことを知らせるCallback関数onResouceReady()
または、読み込みが失敗した時に呼ばれるonLoadFailed()で
startPostponedEnterTransition()を呼ぶことにより遅延遷移を実装しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { final View view = inflater.inflate(R.layout.fragment_image, container, false); Bundle arguments = getArguments(); @DrawableRes int imageRes = arguments.getInt(KEY_IMAGE_RES); // Just like we do when binding views at the grid, we set the transition name to be the string // value of the image res. view.findViewById(R.id.image).setTransitionName(String.valueOf(imageRes)); // Load the image with Glide to prevent OOM error when the image drawables are very large. Glide.with(this) .load(imageRes) .listener(new RequestListener<Drawable>() { @Override public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) { // The postponeEnterTransition is called on the parent ImagePagerFragment, so the // startPostponedEnterTransition() should also be called on it to get the transition // going in case of a failure. getParentFragment().startPostponedEnterTransition(); return false; } @Override public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) { // The postponeEnterTransition is called on the parent ImagePagerFragment, so the // startPostponedEnterTransition() should also be called on it to get the transition // going when the image is ready. getParentFragment().startPostponedEnterTransition(); return false; } }) .into((ImageView) view.findViewById(R.id.image)); return view; } |
sharedElementを使った遷移
RecyclerView.Adapterクラスを継承したGridAdapterクラス内にて、
clickListenerをセットしsharedElementを使った遷移をおこなっています。
まず、ViewPagerからバックキーを押して戻った際にsharedElementを用いた遷移を
行えるように、Activityの位置を保存するためのstatic変数に位置を代入します。
続いて、アニメーションの対象元から自身を除くために、excludeTarget()を呼びます。
最後にFragmentを切り替える際にaddSharedElement()を呼び、sharedElementの設定をし、
commit()を実行します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
/** * Handles a view click by setting the current position to the given {@code position} and * starting a {@link ImagePagerFragment} which displays the image at the position. * * @param view the clicked {@link ImageView} (the shared element view will be re-mapped at the * GridFragment's SharedElementCallback) * @param position the selected view position */ @Override public void onItemClicked(View view, int position) { // Update the position. MainActivity.currentPosition = position; // Exclude the clicked card from the exit transition (e.g. the card will disappear immediately // instead of fading out with the rest to prevent an overlapping animation of fade and move). ((TransitionSet) fragment.getExitTransition()).excludeTarget(view, true); ImageView transitioningView = view.findViewById(R.id.card_image); fragment.getFragmentManager() .beginTransaction() .setReorderingAllowed(true) // Optimize for shared element transition .addSharedElement(transitioningView, transitioningView.getTransitionName()) .replace(R.id.fragment_container, new ImagePagerFragment(), ImagePagerFragment.class .getSimpleName()) .addToBackStack(null) .commit(); } |
addSharedFragment
sharedElement |
View : A View in a disappearing Fragment to match with a View in an appearing Fragment. |
name |
String : The transitionName for a View in an appearing Fragment to match to the shared element. |
遷移対象となるnameは、onBind関数で設定しています。
1 2 3 4 5 6 |
void onBind() { int adapterPosition = getAdapterPosition(); setImage(adapterPosition); // Set the string value of the image resource as the unique transition name for the view. image.setTransitionName(String.valueOf(IMAGE_DRAWABLES[adapterPosition])); } |
PageViewから戻った際にスクロールポジションを合わせる
GridFragmentのonCreatedViewにて、LayoutChangeListenerをセットし、
Activityに保存していたポジションに戻すことでスクロール位置をPageViewで
選択されたイメージの位置に移動させています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
@Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); scrollToPosition(); } /** * Scrolls the recycler view to show the last viewed item in the grid. This is important when * navigating back from the grid. */ private void scrollToPosition() { recyclerView.addOnLayoutChangeListener(new OnLayoutChangeListener() { @Override public void onLayoutChange(View v, 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); // Scroll to position if the view for the current position is null (not currently part of // layout manager children), or it's not completely visible. if (viewAtPosition == null || layoutManager .isViewPartiallyVisible(viewAtPosition, false, true)) { recyclerView.post(() -> layoutManager.scrollToPosition(MainActivity.currentPosition)); } } }); } |
以上になります。
全体のsourceはdeveloper pageのgitを参照ください。
clone先
https://github.com/google/android-transition-examples.git