Android

[Android Jetpack] AAC Navigation Component -3. NavigationUI & Animation/Transition

남잭슨 2019. 8. 18. 21:51

아래의 AAC Navigation에 대한글을 먼저 보고 오세요! 

2019/03/31 - [Android] - [Android Jetpack] AAC Navigation Component - 1. Navigation 소개,구성 및 개요 , BackStack관리

2019/08/14 - [Android] - [Android Jetpack] AAC Navigation Component - 2. SafeArgs & Deeplink & Action

 

이번 글에는 AAC Navigation에서 지원하는 NavigationUIAnimiation ,Transition에 대해 소개할 예정이다.

 

 

Navigation libraries Structure

Navigation은 4개의 모듈로 나눠진다. 

navigation-common은 내부적으로 동작하는 부분이고, 

navigation-runtime은 NavController에 해당한다.

navigation-fragment는 NavHostFragment, Destinations(Fragments)에 해당하고, 

navigation-UI는 bottom-navigation, appbar 등의 UI 동작을 지원해주는 부분이다.

 

NavigationUI

NavigationUI에 대해 간단하게 알아보자

NavigationUI를 사용하여  아래의 Menu, Drawers, toolbar등의 UI들의 Action을 직접 구현하지 않고,

간단하게 연결하여 사용이 가능하다. 

  • Toolbar
  • CollapsingToolbarLayout
  • ActionBar
  • DrawerLayout
  • BottomNavigationView

먼저 xml에 navigation의 컴포넌트를 사용하고 

각 컴포넌트의 NavController와 Menu를 연결해주면 된다. 

주의해야할점은 Menu의 <Item>의 id와 NavGraph의 Destination(<fragment>)의 id가 같아야된다.

 

다음은 bottomNavigation을 구현해보는 예제이다.

        <com.google.android.material.bottomnavigation.BottomNavigationView
                android:id="@+id/nav_view"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:menu="@menu/bottom_nav_menu"/>
                

먼저 xml에 BottomNavigation 컴포넌트를 추가한다. 

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
            android:id="@+id/dashboard"
            android:title="@string/fragment_dashboard"/>
    <item
            android:id="@+id/home_list"
            android:title="@string/fragment_home_list"/>
    <item
            android:id="@+id/mypageFragment"
            android:title="@string/fragment_mypage"/>
</menu>

menu 리소스를 추가한다.

( 이때 NavGraph(xml)의 Destination Fragment 의 Id들과 매핑되기때문에 일치시켜주어야한다.)

binding.navView.setupWithNavController(navController)

이제 Activity내에서 위의 navController을 setUp해주면 된다. 

 

구현은 간단하지만, 빌드해보면 첫번째 탭외에 다른 탭으로 이동시마다 backstack이 쌓이는 현상을 볼수있다. 

BackButton이나 , appbar사용을 위한 backstack사용에 대한 추가적인 작업이 필요하다. 

 

AppBarConfiguration

이러한 작업을 위해 AppBarConfiguration를 지원해준다. 

Tab의 경우나, BottomNavigation등의 경우는 각 탭들 모두  최상단의 Destination이기 때문에,

이를 지원하기 위해 Top-Level-Destination을 정의할 수 있다.

 

AppBarConfiguration 에서 Top-Level-Destination을 지정해주어야한다. 

var appBarConfiguration: AppBarConfiguration

appBarConfiguration = AppBarConfiguration(
  setOf(R.id.home_list, R.id.dashboard, R.id.mypageFragment)
)
        

HomeList,Dashboard, MyPage가 Top-Level-Destination임을 선언 후,

아래 코드처럼 ActionBar에도 Top-Level-Destination 설정 하면,

Top-Level-Destincation이 아니라면 Actionbar에 뒤로 가기[<-] 가 표현된다 ( Up Button)

setupActionBarWithNavController(host.navController, appBarConfiguration)

onSupportNavigateUp에 appBarConfiguration Top-Level-Destination설정이 포함한 정보를 추가해주면,

Up-Button 의 이벤트에도 Top-Level-Destination설정이 가능하다.

override fun onSupportNavigateUp(): Boolean {
	return findNavController(R.id.nav_container).navigateUp(appBarConfiguration)
}

※ Navigation 에서 Up-Button 과 Back Button은 동일한 작업이 되어야하지만, 

( BottomNavigation 탭에서는 Multi - Start Point - Destination을 따로 지원하지 않기 때문에 ,,,)

Top-Level-Destination에서도, Back Button 이벤트 발생시,

StartPoint의 Destination(Fragment)에서 BackStack처리가 된다.

 

기본적으로 Navigation 기본원칙(Fixed Start Destination)으로 구현되어 있어,

appBar나 Up-Button의 경우는 Top-Level-Destination은 지원하지만 Back-Button의경우는

아직까진 Navigation에서 multiple start destinations을 지원하지 않는 것 같다.

 

아래는 조금이 편법을 사용하여, TopLevel 체크를 하였다.

override fun onBackPressed() {

  if(appBarConfiguration.topLevelDestinations.contains( host.navController.currentDestination?.id)){
    if (System.currentTimeMillis() - Back_Key_Before_Time < 2000) {
    	finish()
    } else {
    	Toast.makeText(applicationContext, "한번더 누르면 꺼집니다.", Toast.LENGTH_SHORT).show()
    	Back_Key_Before_Time = System.currentTimeMillis()
    }
    return;
  }

  super.onBackPressed()
}

 

- 추 후엔 back 버튼에도 Top-Level-Destination을 기준으로 multiple start destinations을 지원해 주지 않을까 싶다.

 

 

추가

또, Single Activity 기반이기 때문에 navHostFragment가 아닌 범위 ( Appbar, BottomNavigation) 등의 영역은 

Navigation에서 따로 지원해주지 않기 때문에, 수동으로 처리해주어야한다.

( 사실 Appbar의 경우는 Up-Button[<-]을 지원해주긴한다.)

 

(예를 들어 BottomNavigation은 각 Tab 화면 들의 Detail화면에서는 보여지지 않아야할때, 

Top-Level-level의 Fragment 에서만 하단 BottomNavigation 탭이 보여야할때 수동으로 처리해주어야한다 .

 

아래코드는 해당 작업의 예시이다.

 

host.navController.addOnDestinationChangedListener { _, destination, _ ->
    when (destination.id) {
      R.id.dashboard, R.id.home_list, R.id.mypageFragment -> showBotNav()
      else -> hideBotNav()
    }
}

 

navController에 ChangedListener을 달아서, Bottom Navigation이 보여야할지, 말아야할지 처리해주어야한다.

 

 

 

Animation

Navigation에서는 Action에 Animation기능을 제공한다.  

  • enterAnim : action 실행시, 이동할 Destination에 대한 애니메이션
  • exitAnim : action을 실행할 때 현재 Destination에 대한 애니메이션
  • popExitAnim : 이전 화면으로 돌아갈 때(Pop or Back) 종료되는 현재 Destincation 에 대한 애니메이션
  • popEnterAnim : 이전 화면으로 돌아갈 때(Pop or Back) 이동할 BackStack의 Destination에 대한 애니메이션

 

Animation추가는 두가지 방법이 있다. 

 

1. navigate 실행시, NavOptions 을 정의해서 사용하는방법

val options = navOptions {
  anim {
    enter = R.anim.slide_in_right
    exit = R.anim.slide_out_left
    popEnter = R.anim.slide_in_left
    popExit = R.anim.slide_out_right
  }
}

<!-- 사용 -->
navController.navigate(R.id.flow_step_one_dest, null, options)

2. NavGraph에 명시하여 사용하는 방법

<action
      android:id="@+id/action_to_add"
      app:destination="@id/addFragment"
      app:enterAnim="@anim/slide_in_up"
      app:exitAnim="@anim/fade_out"
      app:popEnterAnim="@anim/fade_in"
      app:popExitAnim="@anim/slide_out_down"/>

마찬가지로 navigate를 사용하여 Action 실행시, Animation이 실행된다.

 

 

Transition

Navigation 에서도 간단하게 Transition 구현이 가능하다

일단, 각 Component View( XML )에 같은 TransitionName을 정의해준다.

<ImageView
        android:transitionName="test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/test"
        android:layout_gravity="top"/>

어떤 TransitionName이  naming이든  상관은 없지만, 전,후 화면의 name은 같아야 실행된다.

 

val extras = FragmentNavigatorExtras(
	binding.imgview to "test"
)
findNavController().navigate( HomeListFragmentDirections.actionToDetail(itemId), extras)

 

먼저 action을 실행시키는 화면에서 FragmentNavigationExtras에 Transition에 대한 정보를 담아서 

navigate에 파라미터로 추가하여 실행한다.

sharedElementEnterTransition = 
	TransitionInflater.from(context).inflateTransition(android.R.transition.move)

 

받는 화면에서 위의 코드만 작성하면 Transition이 실행된다.

 

 

 

 

샘플 코드

해당 예제 코드는 아래 깃허브에 구현하였습니다.

(네비게이션 외에도 클린아키텍쳐 스터디를 위해 오픈소스를 진행하고 있습니다. 많은 Star 와 많은 PR,Issue 참여해주세요! )

https://github.com/namjackson/Clean-Architecture-App

 

namjackson/Clean-Architecture-App

Sample app for studying the Android clean architecture. - namjackson/Clean-Architecture-App

github.com

 

다음글 

AAC Navigation Component -1. Navigation 소개, 구성 및 개여, BackStack 관리 ( 이전 글 )

AAC Navigation Component -2. SafeArgs & Deeplink & Action ( 이전 글 )

AAC Navigation Component -3. NavigationUI & Animation/Transition ( 현재 글 )

AAC Navigation Component -4. Navigation Upgrade (ViewModel with NavGraph Scope & Modularizing(Nested NavGraph) & Multiple-Start-Destination ) & 나의 후기, 장단점( 다음글 )

 

 

참고

https://medium.com/@fornewid/io19-jetpack-navigation-33da8811c9de