Android
MaterialDesign

SharedElement Transtitonでページ遷移をする

More than 1 year has passed since last update.

Android/iOSのYoutubeアプリで、動画のサムネイルをタップすると、サムネイルの部分が拡大しながら動画一覧が消えていき、プレイヤーと動画詳細が出てくると思います。

あの部分もMaterial DesignのMaterial motionという項目できちんと定義されており、実装したいところです。

ただ、単純なUIではなくMotionなので、どう実装すれば良いのかわかりませんでしたが、ちゃんと実装するためのクラスが用意されいていました。

今回はFragment→Fragmentの遷移で、下のような動きをするのを書いていこうと思います。

transition.gif

ちなみに、「一覧から1つをタップすると、それが拡大していき、それの詳細が現れる」みたいなアニメーションの「一覧と詳細で共通している要素」をSharedElementと呼ぶらしいです。

アニメーション開始時と終了時にエレメントを置いたレイアウトを作成する。

まずは開始時と終了時の位置にElementを置いたレイアウトを作成します。

上のgifを例にすると

開始時:上にImageView・左下にTextView

終了時:右上にTextView・下にImageView

となっているFragmentを作成しています。

開始時のFragmentをMainFragment 終了時のFragmentをDetailFragmentとすると、

fragment_mainは

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.core.fragmentsharedelement.fragments.MainFragment">
    <ImageView
        android:src="@drawable/hogehoge"
        android:id="@+id/img_main"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:adjustViewBounds="true"
        android:scaleType="centerInside"/>

    <TextView
        android:id="@+id/text_main"
        android:textSize="20sp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_blank_fragment"
        android:layout_gravity="left|bottom"/>

</FrameLayout>

fragment_detailは

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.core.fragmentsharedelement.fragments.DetailFragment">

    <!-- TODO: Update blank fragment layout -->
    <ImageView
        android:src="@drawable/hogehoge"
        android:id="@+id/img_detail"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:adjustViewBounds="true"
        android:scaleType="centerInside"
        android:layout_gravity="bottom" />

    <TextView
        android:id="@+id/text_detail"
        android:textSize="20sp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_blank_fragment"
        android:layout_gravity="right|top" />

</FrameLayout>

としておきます。

MainFragmentにいろいろ設定する

ここからはMainFragmentからどのFragmentに遷移するか、どのようなアニメーションにするかなどの設定をしていきます。

まずはonCreateViewでViewを保持しておきます。

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_main, container, false);
        this.fragment =  view;
        return view;
    }

次はSharedElementに与えるアニメーションを設定します。

TransitionSetというオブジェクトに与えるアニメーションを設定し、それをどういった順番で実行するかも設定します。

    @Override
    public void onStart(){
        super.onStart();
        TransitionSet ts = new TransitionSet();
        ts.setOrdering(TransitionSet.ORDERING_TOGETHER);
        ts.addTransition(new ChangeBounds());

ts.addTransition()に与えているクラスの役割としては

ChangeBounds()
遷移前と遷移後のSharedElementの変化(大きさ・回転など)をチェックし、それをアニメーションにする。

他にも、ChangeTransformやChangeImageTransformなどがありますが、正直使いどころがわかりませんでした。

ImageViewはChangeImageTransformでないと大きさが動的に変化しないのかなと思っていたらChangeBoundsだけで普通に変化しましたし謎です。詳しい人教えて下さい。

順番が逆になりましたが、
ts.setOrdering(TransitionSet.ORDERING_TOGETHER)

はセットした動作を同時に行うか直列に行うかを設定します。

この場合は同時に行っていますが、直列で行う場合はTransitionSet.ORDERING_SEQUENTIALを与えます。

さて、次はセットしたアニメーションを実際に遷移先のFragmentに適用します。

まずは遷移先のFragment(DetailFragmentとします。)を生成して、

final Fragment frg = new DetailFragment();

finalなのは実際の遷移のEventをクリックイベントで行うからであって特に深い意味は無いです。

生成したFragmentにアニメーションを適用します

frg.setSharedElementEnterTransition(ts);
frg.setSharedElementReturnTransition(ts);

EnterはタップしてDetailFragmentに遷移する時

ReturnはバックキーなどでDetailFragmentからMainFragmentに戻る時のアニメーションです。

つまり、ここではやりませんが、Main→DetailとDetail→Mainで実行するアニメーションを変更することも出来ます。

そして一緒に今回はありませんがMainFragmentが何らかの一覧表示だった場合、他に表示されていたアイテムを退かす必要がありますよね。

その場合の退かし方を設定しましょう。

setEnterTransition(new Explode());
setExitTransition(new Explode());

メソッドは名前見れば役割はだいたいわかると思います。

与える事ができるパラメータはExplode以外にも幾つかあり、動作を見るほうがわかりやすいと思いますので、こちらを見てください。Activity/Fragment Transitionsのつかいかた#transition

上のURLの例だと、Explode Fade Slideが適用できます。

そして最後に、SharedElementにする各要素にsetTransitionNameというIDのようなものを付けます。

これは遷移前後のFragmentで要素のひも付けに使われます。

と言ってもやることは簡単で、

final View targetImage = this.fragment.findViewById(R.id.img_main);
targetImage.setTransitionName("image");

final View targetText = this.fragment.findViewById(R.id.text_main);
targetText.setTransitionName("text");

onCreateViewで保持しておいたViewからSharedElementにする要素のViewを取得し、setTransitionNameでIDを指定するだけです。

finalにしているのは例によってこれもonClickListenerの中で使うからです。

で、締めに、

final FragmentTransaction ft = getFragmentManager().beginTransaction();
targetImage.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        ft.replace(R.id.frag, frg)
                .addSharedElement(targetImage, "image")
                .addSharedElement(targetText, "text")
                .addToBackStack(null)
                .commit();
    }
});

今回はonClickをトリガーとするので、その中でFragmentTransactionにSharedElementを設定しています。

ここで指定する第二引数はsetTransitionNameで指定したIDと同一にしておいてください。

以上でMainFragmentでの設定は終了です。

遷移先のDetailFragmentでの設定

記述量が多かった遷移前に比べ、遷移後はかなり少ないです。

ここからはすべてonCreateViewに書きます。

まずはいつものごとくViewを保持しておいて、

View v = inflater.inflate(R.layout.fragment_detail, container, false);

保持したViewを利用し、紐つけるViewを取得してそれに遷移前に指定したIDをsetTransitionNameの引数に指定するだけです。

View view = v.findViewById(R.id.img_detail);
view.setTransitionName("image");

View v1 = v.findViewById(R.id.text_detail);
v1.setTransitionName("text");

これでFragmentからFragmentへのSharedElementによるページ遷移が行えます。

Activity to ActivityやActivity to Fragmentは結構情報があったのですがFragment to Fragmentはあまりなくて苦労しました。

参考になれば幸いです。