Vintage appMaker의 Tech Blog

[Android] BottomNavigationView의 onBackPressed처리 본문

Source code or Tip/Android(Java, Kotlin)

[Android] BottomNavigationView의 onBackPressed처리

VintageappMaker 2022. 4. 8. 10:38

 

 

Android 화면을 만들면서 Activity만 사용하는 경우보다 Activity + Fragment(s)의 구조로 관리되는 경우가 많다. 그런 경우, BottomNavigationView를 만들어 하단메뉴를 구현할 때도 많다. BottomNavigationView로 하단메뉴를 만들 때에는 다음 3가지를 집중적으로 관리하게 된다. 

 

  • Fragment로 이동

Fragment를 대체할 XML내의 위젯을 배치한다. 이를 설명할 때, FrameLayout를 사용한 예제가 많은 편이다. 

Fragment로 이동하고자 한다면 supportFragmentManager에서 아래와 같은 코드로 Fragment를 넘겨주면 된다. 

 

그리고 setOnNavigationItemSelectedListener에서 하단메뉴를 클릭시, 원하는 Fragment로 이동하기 위해 위에서 구현한 setFragment 함수를 호출한다.

  • backkey를 눌렀을 때, 이전 Fragment로 복귀

대부분의 Android App에서 back key를 이용한 이전화면으로 이동 시, 더이상 이동할 화면이 없다면 앱을 종료시킨다. 그러므로 back key를 핸들링하는 onBackPressed 메소드를 다음과 같은 목적으로 구현해야 한다. 

Activity의 onBackPressed를 오버라이드한다. 
  ▶supportFragmentManager.fragments의 개수를 파악한다. 
    ▶ 남은 Fragment가 있다면 popBackStack(), executePendingTransactions()를 이용하여 복귀시킨다.       
    ▶ 남은 Fragment가 없다면 Activity를 종료한다. 
 
  ▶ backStackEntryCount와 fragments의 개수를 다시검사한다. 없다면 Activity를 종료한다. 
  ▶ (BottomNavigationView)setOnNavigationItemSelectedListener를 잠시 비활성화 시킨다. 
  ▶ (BottomNavigationView)의 화면을 재처리한다(이동시 화면아이콘 등등)
  ▶ (BottomNavigationView)setOnNavigationItemSelectedListener를 다시 활성화 시킨다.

  • Fragment안에서 back key처리

Fragment안에서 onBackPressed를 오버라이드 할 수 없다. 그러므로 Activity의 onBackPressed 메소드에서 처리해야 한다. 문제는 이곳에서 Fragment에서 제공하는 특별한 메소드가 존재하지 않는다는 점이다. 그래서 

 

- onBackPressed를 처리할 interface를 정의
- Fragment에서 정의된 interface를 구현상속

해야 한다.  예로 아래와 같이 (1) OnBackPressedListener를 정의한다. 

 

(2) Fragment에서는 OnBackPressedListner를 구현상속한다. 그리고 원하는 내용을 구현한다.  

 

(3) Activity에서는 onBackPressed에서 복귀된 Fragment가 Interface를 구현상속한 Fragment인지 채크한 후, 구현된 메소드를 호출해준다. 


전체소스 

 

■ activity_bottom_menu.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"

    android:background="@color/bottomBackground"
    android:layout_height="match_parent"

    tools:context="oftenutilbox.viam.psw.example.activity.BottomMenuActivity">

    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:id="@+id/coordinator_layout"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@id/bottom_nav"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <FrameLayout
            android:id="@+id/container"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
        </FrameLayout>

    </androidx.coordinatorlayout.widget.CoordinatorLayout>


    <com.google.android.material.bottomnavigation.BottomNavigationView
        app:itemTextAppearanceActive="@style/BottomNaviTextStyle"
        app:itemTextAppearanceInactive="@style/BottomNaviTextStyle"
        android:background="@color/transparent"
        android:id="@+id/bottom_nav"
        android:layout_width="0dp"

        app:elevation="0dp"
        android:outlineAmbientShadowColor="@android:color/transparent"
        android:outlineSpotShadowColor="@android:color/transparent"

        android:layout_height="wrap_content"
        app:itemIconTint="@null"
        app:itemBackground="@color/bottomBackground"
        app:itemTextColor="@color/bottom_navigation_checkstate"
        app:itemIconSize="20dp"
        app:itemHorizontalTranslationEnabled="false"
        app:labelVisibilityMode="labeled"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:menu="@menu/bottom_nav_menu" />

</androidx.constraintlayout.widget.ConstraintLayout>

위에서 bottomBackground는 아래와 같다. res의 values안의 colors.xml에 다음을 추가하면 된다. 

<color name="bottomBackground">#558B2F</color>

그리고 메뉴는 res의 menu에 bottom_navi_menu.xml로 다음을 정의하면 된다. 

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/menu_home"
        android:icon="@android:drawable/bottom_bar"
        android:title="Home" />

    <item
        android:id="@+id/menu_second"
        android:icon="@android:drawable/sym_action_call"
        android:title="Second" />

    <item
        android:id="@+id/menu_third"
        android:icon="@android:drawable/btn_star_big_on"
        android:title="Third" />


    <item
        android:id="@+id/menu_four"
        android:icon="@android:drawable/ic_menu_camera"
        android:title="Four" />


</menu>

참고로 BottomNavigationView의 구분자를 사용하지 않으려면 속성에 다음 3줄을 추가하면 된다. 

 

■ (res/color)bottom_navigation_checkstate.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="#8FE850" android:state_checked="true"/>
    <item android:color="#595858" android:state_checked="false"/>
</selector>

■ BottomMenuActivity.kt

package oftenutilbox.viam.psw.example.activity

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.fragment.app.Fragment
import com.test.psw.oftenutilbox.R
import com.test.psw.oftenutilbox.databinding.ActivityBottomMenuBinding
import oftenutilbox.viam.psw.example.fragment.*
import java.lang.Exception

class BottomMenuActivity : AppCompatActivity() {
    lateinit var binding: ActivityBottomMenuBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityBottomMenuBinding.inflate(layoutInflater)
        setContentView(binding.root)

        setFragment(HomeFragment.newInstance("", ""))
        setUpBottomMenu()
    }

    fun MenuIndex(s: String) : Int {
        val TABMENU = mapOf(
            "HOME"     to 0,
            "SECOND"   to 1,
            "THIRD"    to 2,
            "FOUR"     to 3
        )
        return TABMENU[s] ?: 0;
    }

    private fun setUpBottomMenu() {

        binding.bottomNav.apply {
            itemIconTintList = null

            menu.getItem(MenuIndex("HOME")).icon = getDrawable(android.R.drawable.bottom_bar)
            menu.getItem(MenuIndex("SECOND")).icon = getDrawable(android.R.drawable.sym_action_call)
            menu.getItem(MenuIndex("THIRD")).icon = getDrawable(android.R.drawable.btn_star_big_on)
            menu.getItem(MenuIndex("FOUR")).icon = getDrawable(android.R.drawable.ic_menu_camera)

            setOnNavigationItemSelectedListener { item ->
                when(item.itemId){
                    R.id.menu_home     -> {

                        setFragment(HomeFragment.newInstance("", ""))
                        true
                    }
                    R.id.menu_second -> {
                        setFragment(SecondFragment.newInstance("", "")); true}
                    R.id.menu_third   -> {
                        setFragment(ThirdFragment.newInstance("", "")); true}
                    R.id.menu_four    -> {
                        setFragment(FourFragment.newInstance("", "")); true}

                    else -> true
                }
            }
        }
    }

    fun setFragment(fragment: Fragment) {

        supportFragmentManager
            .beginTransaction()
            .replace(R.id.container, fragment)
            //.addToBackStack( if(bRegister) "wantFragment" else null)
            .addToBackStack( null)
            .commit()
    }

    override fun onBackPressed() {
        backKeyManager()
    }

    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
    private fun backKeyManager() {

        // fragment내에서 Onbackpressed 구현
        val fragmentList = supportFragmentManager.fragments
        if (fragmentList != null) {
            for (fragment in fragmentList) {
                if (fragment is OnBackPressedListener) {
                    // fragment에서 onBack을 처리하겠다고 하면 리턴
                    if ( (fragment as OnBackPressedListener).onBackPressed() )
                        return
                }
            }
        }

        supportFragmentManager.fragments.size?.let { nCount ->
            if (nCount == 0)
                // 메모리에서 삭제
                finishAndRemoveTask()
        }
        supportFragmentManager.popBackStack()
        supportFragmentManager.executePendingTransactions()

        // 종료처리
        if (supportFragmentManager.backStackEntryCount == 0) finish()
        val nIndx = supportFragmentManager.fragments.size
        if (nIndx < 1) finish()

        // 잠시 setOnNavigationItemSelectedListener를 비활성화
        binding.bottomNav.setOnNavigationItemSelectedListener { true }

        // Sync에 문제가 발생하여 index가 안맞을 경우 종료시킴
        try {
            // menu click 기능구현
            ActivateBackFragment()
        } catch (e: Exception) {
            e.printStackTrace()
            finish()
        }
    }

    private fun ActivateBackFragment() {
        val n = if ( supportFragmentManager.fragments.size > 1) 1 else 0

        val f = supportFragmentManager.fragments[n]
        when (f) {
            is HomeFragment -> {
                binding.bottomNav.selectedItemId =
                    binding.bottomNav.menu.getItem(MenuIndex("HOME")).itemId
            }
            is SecondFragment -> {
                binding.bottomNav.selectedItemId =
                    binding.bottomNav.menu.getItem(MenuIndex("SECOND")).itemId
            }
            is ThirdFragment -> {
                binding.bottomNav.selectedItemId =
                    binding.bottomNav.menu.getItem(MenuIndex("THIRD")).itemId
            }
            is FourFragment -> {
                binding.bottomNav.selectedItemId =
                    binding.bottomNav.menu.getItem(MenuIndex("FOUR")).itemId
            }

        }

        setUpBottomMenu()
    }
}

■ HomeFragment.kt

package oftenutilbox.viam.psw.example.fragment

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.test.psw.oftenutilbox.R
import com.test.psw.oftenutilbox.databinding.FragmentHomeBinding

// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"

/**
 * A simple [Fragment] subclass.
 * Use the [BlankFragment.newInstance] factory method to
 * create an instance of this fragment.
 */
class HomeFragment : Fragment() {
    // TODO: Rename and change types of parameters
    private var param1: String? = null
    private var param2: String? = null

    lateinit var binding : FragmentHomeBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            param1 = it.getString(ARG_PARAM1)
            param2 = it.getString(ARG_PARAM2)
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = FragmentHomeBinding.inflate(inflater)
        return binding.root
    }

    companion object {
        /**
         * Use this factory method to create a new instance of
         * this fragment using the provided parameters.
         *
         * @param param1 Parameter 1.
         * @param param2 Parameter 2.
         * @return A new instance of fragment BlankFragment.
         */
        // TODO: Rename and change types and number of parameters
        @JvmStatic
        fun newInstance(param1: String, param2: String) =
            HomeFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_PARAM1, param1)
                    putString(ARG_PARAM2, param2)
                }
            }
    }
}

■ SecondFragment.kt

package oftenutilbox.viam.psw.example.fragment

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.test.psw.oftenutilbox.R
import com.test.psw.oftenutilbox.databinding.FragmentSecondBinding

// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"

/**
 * A simple [Fragment] subclass.
 * Use the [BlankFragment.newInstance] factory method to
 * create an instance of this fragment.
 */
class SecondFragment : Fragment() {
    // TODO: Rename and change types of parameters
    private var param1: String? = null
    private var param2: String? = null

    lateinit var binding : FragmentSecondBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            param1 = it.getString(ARG_PARAM1)
            param2 = it.getString(ARG_PARAM2)
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = FragmentSecondBinding.inflate(inflater)
        return binding.root
    }

    companion object {
        /**
         * Use this factory method to create a new instance of
         * this fragment using the provided parameters.
         *
         * @param param1 Parameter 1.
         * @param param2 Parameter 2.
         * @return A new instance of fragment BlankFragment.
         */
        // TODO: Rename and change types and number of parameters
        @JvmStatic
        fun newInstance(param1: String, param2: String) =
            SecondFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_PARAM1, param1)
                    putString(ARG_PARAM2, param2)
                }
            }
    }
}

 

■ ThirdFragment.kt

package oftenutilbox.viam.psw.example.fragment

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.test.psw.oftenutilbox.R
import com.test.psw.oftenutilbox.databinding.FragmentFourBinding
import com.test.psw.oftenutilbox.databinding.FragmentThirdBinding

// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"

/**
 * A simple [Fragment] subclass.
 * Use the [BlankFragment.newInstance] factory method to
 * create an instance of this fragment.
 */

class ThirdFragment : Fragment() {
    // TODO: Rename and change types of parameters
    private var param1: String? = null
    private var param2: String? = null

    lateinit var binding: FragmentThirdBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            param1 = it.getString(ARG_PARAM1)
            param2 = it.getString(ARG_PARAM2)
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = FragmentThirdBinding.inflate(inflater)
        return binding.root
    }

    companion object {
        /**
         * Use this factory method to create a new instance of
         * this fragment using the provided parameters.
         *
         * @param param1 Parameter 1.
         * @param param2 Parameter 2.
         * @return A new instance of fragment BlankFragment.
         */
        // TODO: Rename and change types and number of parameters
        @JvmStatic
        fun newInstance(param1: String, param2: String) =
            ThirdFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_PARAM1, param1)
                    putString(ARG_PARAM2, param2)
                }
            }
    }
}

■ FourFragment.kt

package oftenutilbox.viam.psw.example.fragment

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.test.psw.oftenutilbox.databinding.FragmentFourBinding

// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"

/**
 * A simple [Fragment] subclass.
 * Use the [BlankFragment.newInstance] factory method to
 * create an instance of this fragment.
 */
class FourFragment : Fragment(), OnBackPressedListener {
    // TODO: Rename and change types of parameters
    private var param1: String? = null
    private var param2: String? = null

    private var backcount = 3
    // Fragment에서 OnBackpressed를 구현함
    override fun onBackPressed(): Boolean {
        if(backcount < 1) return false
        binding.textMessage.text  = "Four( back count remain )"
        binding.textMessage2.text = "${backcount}"
        backcount--
        return true
    }

    lateinit var binding: FragmentFourBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            param1 = it.getString(ARG_PARAM1)
            param2 = it.getString(ARG_PARAM2)
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        binding = FragmentFourBinding.inflate(inflater)
        return binding.root
    }

    companion object {
        /**
         * Use this factory method to create a new instance of
         * this fragment using the provided parameters.
         *
         * @param param1 Parameter 1.
         * @param param2 Parameter 2.
         * @return A new instance of fragment BlankFragment.
         */
        // TODO: Rename and change types and number of parameters
        @JvmStatic
        fun newInstance(param1: String, param2: String) =
            FourFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_PARAM1, param1)
                    putString(ARG_PARAM2, param2)
                }
            }
    }
}
Comments