본문 바로가기
오늘은 뭘 배울까?/Android

안드로이드 컨텍스트 메뉴(Context Menu)란?

by Kim Juhwan 2021. 9. 27.

[기본]
   1. Context Menu
       1-1. 개념
       1-2. Menu 파일 생성

   2. Context Menu 만들기
   3. 실행 결과
   4. 예제 코드

[심화]
   5. Context Menu Item에 접근하기
   6. RecyclerView에 Context Menu 적용하기
       6-1. Adapter
       6-2. Activity

 

 

 


 

 

1. Context Menu

1-1. 개념

컴퓨터에서의 Context Menu

 

컴퓨터를 사용할 때 어떤 요소를 마우스 우 클릭하면 이렇게 메뉴 창이 뜬다.

이런 걸 우리는 Context Menu라고 부른다.

안드로이드에서는 이런 메뉴 팝업 창을 띄울 수가 있는데 용도와 사용법에 따라 부르는 이름이 조금씩 다르다.

 

이번 포스팅에서는 요소를 길게 클릭하면 나타나는 플로팅 메뉴인 Context Menu를 알아보자.

 

Menu의 종류와 간단한 설명은 이전 포스팅의 [목차 1-1]에서 확인할 수 있다.

 

 

1-2. Menu 파일 생성

 

안드로이드 옵션 메뉴(Option Menu)란?

1. Menu  1-1. Menu란?  1-2. Option Menu란?  1-3. Menu 파일 생성 2. Option Menu 만들기 3. 실행결과 4. 예제 코드 1. Menu 1-1. Menu란? Option Menu 검색, 이메일 작성, 설정과 같이 앱 전체에 영향을 미..

todaycode.tistory.com

옵션 메뉴를 만들 때와 동일하기 때문에 위 링크의 [목차 1-3]을 보고 따라 만들면 된다.

 

 

2. Context Menu 만들기

    override fun onCreateContextMenu(
        menu: ContextMenu?,
        v: View?,
        menuInfo: ContextMenu.ContextMenuInfo?
    ) {
        super.onCreateContextMenu(menu, v, menuInfo)
        menuInflater.inflate(R.menu.menu_context, menu)
    }

onCreateContextMenu를 오버라이딩하여 컨텍스트 메뉴를 생성하면 된다.

inflate는 이전 포스팅에서도 언급했듯이 XML 리소스를 프로그래밍하기 위해 객체로 변환시키는 것이다.

 

    override fun onContextItemSelected(item: MenuItem): Boolean {
        when(item.itemId) {
            R.id.menu_item_red -> {
                btn1.setTextColor(Color.RED)
            }
            R.id.menu_item_blue -> {
                btn1.setTextColor(Color.BLUE)
            }
        }
        return super.onContextItemSelected(item)
    }

아이템이 선택되었을 때 할 작업을 onContextItemSelected에 정의해두면 된다.

이 코드에서는 버튼의 색상을 바꾸도록 하였다.

 

    lateinit var btn1: Button
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        btn1 = findViewById(R.id.btn1)
        registerForContextMenu(btn1) // 컨텍스트 메뉴를 어디에서 사용할건지 등록
    }

마지막으로 registerForContextMenu에 어떤 view에서 컨텍스트 메뉴를 사용할 건지 등록하면 끝이다.

 

class MainActivity : AppCompatActivity() {
    lateinit var btn1: Button
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        btn1 = findViewById(R.id.btn1)
        registerForContextMenu(btn1)
    }

    override fun onCreateContextMenu(
        menu: ContextMenu?,
        v: View?,
        menuInfo: ContextMenu.ContextMenuInfo?
    ) {
        super.onCreateContextMenu(menu, v, menuInfo)
        menuInflater.inflate(R.menu.menu_context, menu)
    }

    override fun onContextItemSelected(item: MenuItem): Boolean {
        when(item.itemId) {
            R.id.menu_item_red -> {
                btn1.setTextColor(Color.RED)
            }
            R.id.menu_item_blue -> {
                btn1.setTextColor(Color.BLUE)
            }
        }
        return super.onContextItemSelected(item)
    }
}

전체 코드이다.

 

 

3. 실행 결과

Context Menu를 사용한 모습

 

버튼을 꾹 누르면 왼쪽 사진처럼 Context Menu가 뜨며

'빨간 텍스트로 변경' 아이템을 클릭하니 버튼 텍스트 색상이 바뀌는 것을 확인할 수 있다.

 

 

4. 예제 코드

 

GitHub - juhwankim-dev/SelfStudy: 코틀린으로 공부한 것들을 올리는 공간입니다.

코틀린으로 공부한 것들을 올리는 공간입니다. Contribute to juhwankim-dev/SelfStudy development by creating an account on GitHub.

github.com

 

 

5. Context Menu Item에 접근하기

 

리스트 뷰 아이템을 꾹 눌러 예약을 선택하면 색이 칠해진다.

 

리스트 뷰에 Context Menu를 적용하면 이런 기능을 구현할 수 있다.

어떻게 하는건지는 다음 코드를 살펴보자.

 

override fun onContextItemSelected(item: MenuItem): Boolean {
        val info = item.menuInfo as AdapterView.AdapterContextMenuInfo
        val index = info.position
        val movie = MovieItemMgr.search(index)

        when(item?.itemId) {
            R.id.menu_item_reserve -> { // 예약하기를 누른 경우
                when(movie.isReserve) {
                    true -> { // 이미 예약 했다면
                        Toast.makeText(this, "이미 예약된 영화입니다.", Toast.LENGTH_SHORT).show()
                    }

                    else -> {
                        movie.isReserve = true
                        MovieItemMgr.update(index, movie)
                        info.targetView.setBackgroundColor(Color.CYAN)
                        this@MainActivity.adapter.notifyDataSetChanged()
                    }
                }
            }

            else -> { // 예약취소를 누른 경우
                when(movie.isReserve) {
                    true -> {
                        movie.isReserve = false
                        MovieItemMgr.update(index, movie)
                        info.targetView.setBackgroundColor(0x00000000)
                        this@MainActivity.adapter.notifyDataSetChanged()
                    }

                    else -> {
                        Toast.makeText(this, "예약되지 않은 영화입니다.", Toast.LENGTH_SHORT).show()
                    }
                }
            }
        }

        return super.onContextItemSelected(item)
    }

ListView에 대한 내용도 들어가고 로직에 대한 내용도 들어가서 다소 복잡해 보이지만 우리가 주목할 것은

val info = item.menuInfo as AdapterView.AdapterContextMenuInfo

info.targetView.setBackgroundColor(Color.CYAN)

이 두 줄이다. item.menuinfo.targetView를 사용하면 내가 선택한 그 아이템 뷰에 접근할 수 있다.

 

주의할 점 첫 번째는 menuInfo Context Menu를 ListView에 사용했을 때만 사용 가능한 메서드라는 것이다.

단순히 "오 내가 선택한 뷰를 가져올 수 있는 메서드구나!"라고 생각하면 안 된다.

만약 버튼이나 텍스트 같은 뷰에 Context Menu를 등록하고 menuInfo를 사용하면 null을 반환할 것이다.

 

java.lang.NullPointerException: null cannot be cast to non-null type android.widget.AdapterView.AdapterContextMenuInfo
이런 에러가 뜨게 된다.

 

6. RecyclerView에 ContextMenu 적용하기

버튼, 이미지, 리스트뷰 등등 다른 뷰들은 [목차 2] 방법대로 컨텍스트 메뉴를 사용하면 되지만

리사이클러뷰는 View.ViewGroup을 상속받은 것이기 때문에 기존의 방법으로는 컨텍스트 메뉴 사용이 불가능하다. 

그래서 이번 목차에서는 어떻게 하면 RecyclerView에 컨텍스트 메뉴를 사용할 수 있는지에 대해 적어보려고 한다.

단, 리사이클러뷰에 대해서는 이미 알고 있다는 가정하에 설명을 생략한다.

 

6-1. Adapter

class MovieAdapter() : RecyclerView.Adapter<MovieAdapter.MovieViewHolder>() {

    inner class MovieViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        // TODO
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
        // TODO
    }

    override fun onBindViewHolder(holder: MovieAdapter.MovieViewHolder, position: Int) {
        // TODO
    }

    override fun getItemCount(): Int {
        // TODO
    }
}

어댑터에서 리사이클러뷰 동작에 필요한 코드를 전부 걷어내고 틀만 남겨보았다.

영화 관련 코드를 제작하던걸 가져온 거라 이름 앞에 Movie가 붙는 건 양해 바람...

아무튼 이 어댑터에 우리가 해야 할 것은

 

class MovieAdapter() : RecyclerView.Adapter<MovieAdapter.MovieViewHolder>() {
                                                                                     // 1. 상속받는다.
    inner class MovieViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnCreateContextMenuListener {
        // TODO
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
        // TODO
    }

    override fun onBindViewHolder(holder: MovieAdapter.MovieViewHolder, position: Int) {
        // TODO
    }

    override fun getItemCount(): Int {
        // TODO
    }
}

뷰홀더에서 View.OnCreateContextMenuListener를 상속받는 것이다.

그러면 빨간 줄이 뜰 텐데 override를 해주면

 

class MovieAdapter() : RecyclerView.Adapter<MovieAdapter.MovieViewHolder>() {
                                                                                     // 1. 상속받는다.
    inner class MovieViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnCreateContextMenuListener {
        
        // 2. 이게 생긴다.
        override fun onCreateContextMenu(
            menu: ContextMenu?,
            v: View?,
            menuInfo: ContextMenu.ContextMenuInfo?
        ) {

        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
        // TODO
    }

    override fun onBindViewHolder(holder: MovieAdapter.MovieViewHolder, position: Int) {
        // TODO
    }

    override fun getItemCount(): Int {
        // TODO
    }
}

onCreateContextMenu가 생긴다.

[목차 2]에서는 액티비티에서 이 메서드를 오버라이딩 했지만

리사이클러뷰에서 사용할 때는 뷰홀더 안에서 오버라이딩 해야한다.

 

class MovieAdapter() : RecyclerView.Adapter<MovieAdapter.MovieViewHolder>() {
                                                                                     // 1. 상속받는다.
    inner class MovieViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnCreateContextMenuListener {
        // 3. 뷰홀더 내의 아이템뷰에 컨텍스트 메뉴 리스너를 등록해준다.
        init {
            itemView.setOnCreateContextMenuListener(this) // this는 뷰홀더를 의미
        }
        
        // 2. 이게 생긴다.
        override fun onCreateContextMenu(
            menu: ContextMenu?,
            v: View?,
            menuInfo: ContextMenu.ContextMenuInfo?
        ) {

        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
        // TODO
    }

    override fun onBindViewHolder(holder: MovieAdapter.MovieViewHolder, position: Int) {
        // TODO
    }

    override fun getItemCount(): Int {
        // TODO
    }
}

이제 뷰홀더 내의 아이템 뷰에 컨텍스트 메뉴 리스너를 등록해준다.

this는 위 코드에서 MovieViewHolder를 의미한다.

 

init은 생성자보다 먼저 불리는 거라 생각하면 되는데 자세한 설명은 생략

 

class MovieAdapter(val listener: ItemClickListener) : RecyclerView.Adapter<MovieAdapter.MovieViewHolder>() {
                                                                                     // 1. 상속받는다.
    inner class MovieViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnCreateContextMenuListener {
        // 3. 뷰홀더 내의 아이템뷰에 컨텍스트 메뉴 리스너를 등록해준다.
        init {
            itemView.setOnCreateContextMenuListener(this) // this는 뷰홀더를 의미
        }
        
        // 2. 이게 생긴다.
        override fun onCreateContextMenu(
            menu: ContextMenu?,
            v: View?,
            menuInfo: ContextMenu.ContextMenuInfo?
        ) {
            // 4. Context Menu 설정을 해준다.
            menu?.setHeaderTitle("선택하세요") // Context Menu 제목
            var item1 = menu?.add(0, 0, 0, "예약") // 메뉴 아이템 1
            var item2 = menu?.add(0, 1, 1, "예약취소") // 메뉴 아이템 2

            item1?.setOnMenuItemClickListener {
                // 메뉴 아이템 1 클릭됐을 때 할 작업
                true
            }

            item2?.setOnMenuItemClickListener {
                // 메뉴 아이템 2 클릭됐을 때 할 작업
                true
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
        // TODO
    }

    override fun onBindViewHolder(holder: MovieAdapter.MovieViewHolder, position: Int) {
        // TODO
    }

    override fun getItemCount(): Int {
        // TODO
    }
}
  • setHeaderTitle: context menu의 제목을 설정하는 메서드
  • add: 메뉴 아이템을 추가하는 메서드. 각각의 인자는 groupId, itemId, order, title의 의미를 가진다.
    • itemId는 각각의 아이템이 다른 값을 가져야 한다.

 

만약 setOnMenuItemClickListener에서 클릭된 아이템이 필요하다면 itemView를 사용하면 되고

클릭된 아이템의 포지션이 필요하다면 layoutPosition을 사용하면 되고

context가 필요하다면 itemView.context를 사용하면 된다.

 

setOnMenuItemClickListener에서 리턴해주는 Boolean값의 의미가 궁금해서 공식문서를 찾아보았다.
다른 콜백 함수가 이 클릭리스너를 실행하도록 할지 말지 정하는 의미를 가진다는 것 같은데 내 짬바가 부족한지 아직 이해가 잘 가지 않는다.
일단 true로 하든 false로 하든 동작은 잘하는 것을 확인했다.

 

6-2. Activity

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        registerForContextMenu(binding.rvMovie) // context menu 등록
    }
}

이해하기 쉬우라고 필요한 틀만 제외하고 전부 다 걷어냈다.

이대로 코드 돌린다고 절대 안 돌아감. 리사이클러뷰 설정하고 등등 다른 작업은 알아서 각자 해줘야 한다.

 

[목차 2]에서 처럼 액티비티에서 registerForContextMenu 메서드를 사용하면

RecyclerView에 Context Menu 등록하기가 끝난다.

다소 복잡한 것 같지만 막상 한 번 구현해보면 어렵지 않으니 시도해보자.

 

 


💡 느낀 점

  • 우리가 자주 사용하는 앱 중에서 Context Menu를 사용하는 게 있을까 생각해봤는데 암만 생각하고 찾아봐도 떠오르질 않는다. 요즘에는 사용하는 추세가 아닌 건가?
  • 사진을 꾹 누르면 뜨는 팝업 메뉴나 텍스트를 복사하기 위해 꾹 누르면 뜨는 팝업 메뉴가 Context Menu로 만들었을 거라 생각했는데 이건 시스템 레벨에서 만들어 놓은 거라 알 수 없다고 한다. (사용했을 수도 있고 아닐 수도 있고)
  • RecyclerView에 Context Menu 등록하는 내용이 공식문서에도 없고 인터넷에는 자바 코드로 짠 예전 버전만 있어서 kotlin 버전에 대한 정보를 얻기가 너무 어려웠다 ㅠㅠ

📘 참고한 자료


 

 

 

반응형

댓글