ticktakclockの日記

技術ポエムを綴ったりします。GitHub idも同じです (@ticktakclock)

Android Jetpack Navigationで画面遷移する

こんにちは、tkyです。

下タブ(BottomNavigation)とNavigationをあわせて画面遷移とアニメーションを実現するところまで実装してみようと思います。

Navigationがわからない人が

  • AtivityとFragmentの画面遷移ができる
  • アニメーションに必要なパラメータがわかる

なんとなくNavigationわかってきたくらいを目標に記事を書いています。

作ったプロジェクトはこちら

github.com

Navigation

公式ドキュメントも合わせて確認しながら理解していこうと思います。

Androidアプリ内の「目的地」間を移動するためのフレームワーク、とのことです。目的地はActivityでもFragmentでもどちらでも問題なく、一貫した呼び方ができるのが特徴のようです。

今まで画面遷移は activity.startActivity(Intent(this, FooDetailActivity::class.java)) だったり fragmentTransaction.replace(R.id.container, BarDetailFragment.newInstance() のように呼び方がActivity/Fragmentで異なりましたが、

Navigationを使用するとActivity/Fragment遷移関係なくfindNavController().navigate(R.id.navigation_home) のように統一されたIFなるみたいです。

developer.android.com

AndroidStudioのプロジェクトテンプレート

実はプロジェクトテンプレートでBottomNavigationActivityを選択すれば手早くサンプルアプリが構築できるので使わない手はありません。早速作ってみます

f:id:ticktakclock:20200126164121p:plain:w200
プロジェクトテンプレートからBottomNavigation

デフォルトでふわっとしたフェードアニメーションが付与されているのが確認できます。

f:id:ticktakclock:20200126185324g:plain:w200
アプリ動作

更に嬉しいのがViewModelまで作っておいてくれている親切設計。ありがたいですね

class NotificationsFragment : Fragment() {

    private lateinit var notificationsViewModel: NotificationsViewModel

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        notificationsViewModel =
            ViewModelProviders.of(this).get(NotificationsViewModel::class.java)
        val root = inflater.inflate(R.layout.fragment_notifications, container, false)
        val textView: TextView = root.findViewById(R.id.text_notifications)
        notificationsViewModel.text.observe(this, Observer {
            textView.text = it
        })
        return root
    }
}
class NotificationsViewModel : ViewModel() {

    private val _text = MutableLiveData<String>().apply {
        value = "This is notifications Fragment"
    }
    val text: LiveData<String> = _text
}

activity_main.xml

まずはActivityから理解を深めます。

BottomNavigationが配置されています。メニューは@menu/bottom_nav_menuで定義しています。Home,Dashboard,Notificationsが定義されているだけです。

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/nav_view"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="0dp"
        android:layout_marginEnd="0dp"
        android:background="?android:attr/windowBackground"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:menu="@menu/bottom_nav_menu" />

    <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toTopOf="@id/nav_view"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/mobile_navigation" />

次に nav_host_fragment というFragment領域が定義されています。 androidx.navigation.fragment.NavHostFragment が実体のようです。

ドキュメントによると「NavHostはナビゲーション グラフからの宛先を表示する空のコンテナ。」ということらしいです。NavHostFragmentはNavHostインターフェースの実装クラスということですね。

一番下には app:navGraph="@navigation/mobile_navigation" の記述があります。ナビゲーショングラフと呼ばれ、ユーザが遷移可能なすべてのデスティネーション(遷移先/宛先)を指定します。

これでNavHostとナビゲーションを紐付けているわけですね。

MainActivity

        val navView: BottomNavigationView = findViewById(R.id.nav_view)

        val navController = findNavController(R.id.nav_host_fragment)
        // Passing each menu ID as a set of Ids because each
        // menu should be considered as top level destinations.
        val appBarConfiguration = AppBarConfiguration(
            setOf(
                R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications
            )
        )
        setupActionBarWithNavController(navController, appBarConfiguration)
        navView.setupWithNavController(navController)

findNavController() は拡張関数です、ですのでJavaから呼ぶ場合は Navigation.findNavController(Activity, @IdRes int viewId) で呼ぶ必要があります。

続いてAppBarConfigurationですが表示している画面に合わせてAppBarの表示タイトルを変更するのに使っているぽいです。

コメントによると「メニューIDとナビゲーションのIDを一致させる必要がある」というような記述があります。 確認するとたしかにxml上でIDが一致していました。ここは知らないとハマりそうなポイントです。

MenuIdとNavigationIdが一致していれば、画面と画面名をAppBarと連携する必要がないならAppBarConfigurationの記述は省いても問題なさそうです。

後はメニューとNavitgationを関連付ける処理が入っている感じです。 メニューとNavigationを紐付けているのでドロワーメニューなどでも同様のやり方で画面遷移実現できそうです。

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/mobile_navigation"
    app:startDestination="@+id/navigation_home">

    <fragment
        android:id="@+id/navigation_home"   ★★★ここと★★★
        android:name="com.github.ticktakclock.bottomnavigationsample.ui.home.HomeFragment"
        android:label="@string/title_home"
        tools:layout="@layout/fragment_home" />

    <fragment
        android:id="@+id/navigation_dashboard"
        android:name="com.github.ticktakclock.bottomnavigationsample.ui.dashboard.DashboardFragment"
        android:label="@string/title_dashboard"
        tools:layout="@layout/fragment_dashboard" />

    <fragment
        android:id="@+id/navigation_notifications"
        android:name="com.github.ticktakclock.bottomnavigationsample.ui.notifications.NotificationsFragment"
        android:label="@string/title_notifications"
        tools:layout="@layout/fragment_notifications" />
</navigation>
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/navigation_home"   ★★★ここのIDを一致させる必要がある★★★
        android:icon="@drawable/ic_home_black_24dp"
        android:title="@string/title_home" />

    <item
        android:id="@+id/navigation_dashboard"
        android:icon="@drawable/ic_dashboard_black_24dp"
        android:title="@string/title_dashboard" />

    <item
        android:id="@+id/navigation_notifications"
        android:icon="@drawable/ic_notifications_black_24dp"
        android:title="@string/title_notifications" />

</menu>

mobile_navigation.xml

先程登場したナビゲーションのxmlですがデザインモードというのがあって、グラフィカルに画面が表示されるモードがあります。

ここでnavigation_homeをクリックしたまま→navigation_dashboardにドラッグして線を引いてみます。 するとxml側にアクションが追加されます。アクションIDから「HomeはDashboardへの遷移アクションを持っている」というような文脈が想定できます

    <fragment
        android:id="@+id/navigation_home"
        android:name="com.github.ticktakclock.bottomnavigationsample.ui.home.HomeFragment"
        android:label="@string/title_home"
        tools:layout="@layout/fragment_home" >
        <action
            android:id="@+id/action_navigation_home_to_navigation_dashboard"
            app:destination="@+id/navigation_dashboard" />
    </fragment>

せっかくなのでHomeFragmentにButtonを追加して遷移してみましょう。

findNavController().navigate()はActionかDestinationのリソースIDを受けて画面遷移します。

class HomeFragment : Fragment() {

    private lateinit var homeViewModel: HomeViewModel

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // 〜中略〜
        val button: Button = root.findViewById(R.id.button)
        button.setOnClickListener {
            findNavController().navigate(R.id.action_navigation_home_to_navigation_dashboard)
        }
        return root
    }

ボタンを押すとぱっと画面が切り替わることが確認できました。ここにはデフォルトのアニメーションは適用されないようです。

独自のアニメーションを実現するにはAnimationsを修正します。

f:id:ticktakclock:20200126190753p:plain:w200

Home->Dashboardへの画面遷移の場合の各アニメーションの役割です

  • Enter
  • Exit
    • Dashboardへ遷移する時のHome側のアニメーション
  • Pop Enter
    • Dashboardから離れる時のHome側のアニメーション
  • Pop Exit

それぞれ

nav_side_enter.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="@android:integer/config_mediumAnimTime"
        android:fromXDelta="100%p"
        android:toXDelta="0" />
</set>

nav_side_exit.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="@android:integer/config_mediumAnimTime"
        android:fromXDelta="0"
        android:toXDelta="-100%p" />
</set>

nav_side_pop_enter.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="@android:integer/config_mediumAnimTime"
        android:fromXDelta="-100%p"
        android:toXDelta="0" />
</set>

nav_side_pop_exit.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="@android:integer/config_mediumAnimTime"
        android:fromXDelta="0"
        android:toXDelta="100%p" />
</set>

またナビゲーションは先程定義したアニメーションのIDを指定していきます。

    <fragment
        android:id="@+id/navigation_home"
        android:name="com.github.ticktakclock.bottomnavigationsample.ui.home.HomeFragment"
        android:label="@string/title_home"
        tools:layout="@layout/fragment_home" >
        <action
            android:id="@+id/action_navigation_home_to_navigation_dashboard"
            app:destination="@+id/navigation_dashboard"
            app:enterAnim="@anim/nav_side_enter"
            app:exitAnim="@anim/nav_side_exit"
            app:popEnterAnim="@anim/nav_side_pop_enter"
            app:popExitAnim="@anim/nav_side_pop_exit"/>
    </fragment>

こんな感じに動きました

f:id:ticktakclock:20200126211447g:plain:w200

Activityに遷移する

DashboardFragmentにボタンを追加してSubActivityに遷移するように作ってみます。

f:id:ticktakclock:20200126222800p:plain:w300

先ほどと同様にmobile_navigation.xmlにはタグで定義してみます。同じようにdashboard->sub_activityに遷移するようなActionを定義します。

    <fragment
        android:id="@+id/navigation_dashboard"
        android:name="com.github.ticktakclock.bottomnavigationsample.ui.dashboard.DashboardFragment"
        android:label="@string/title_dashboard"
        tools:layout="@layout/fragment_dashboard" >
        <action
            android:id="@+id/action_navigation_dashboard_to_navigation_sub"
            app:destination="@id/navigation_sub"
            app:enterAnim="@anim/nav_default_enter_anim"
            app:exitAnim="@anim/nav_default_exit_anim"
            app:popEnterAnim="@anim/nav_default_pop_enter_anim"
            app:popExitAnim="@anim/nav_default_pop_exit_anim" />
    </fragment>

    <activity
        android:id="@+id/navigation_sub"
        android:name="com.github.ticktakclock.bottomnavigationsample.SubActivity"
        android:label="@string/title_sub"
        tools:layout="@layout/activity_sub" />

FragmentでもActivityでも Action として画面遷移を定義しているのでActivitiyは遷移先がActivity/Fragmentどちらなのか意識することなく画面遷移できていることがわかります。

class DashboardFragment : Fragment() {

    private lateinit var dashboardViewModel: DashboardViewModel

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // 〜中略〜
        val button: Button = root.findViewById(R.id.button)
        button.setOnClickListener {
            findNavController().navigate(R.id.action_navigation_dashboard_to_navigation_sub)
        }
        return root
    }
}

まとめ

  • Navigationを使うと画面遷移を定義できる
  • 画面遷移はActivity/Fragment意識することなく同じインターフェースで実現できる
  • 遷移はActionを使い、遷移時のアニメーションも定義できる

次はSafeArgsを試してみようと思います。

→Next article ticktakclock.hatenablog.com