- 전체
- 명
- 오늘 찾아주신 분
- 명
정말 오랜만에 작성하는 포스팅이다.
이직한 이후로 많이 게을렀고, 기록할 만한 내용들도 거의 없는 사무적인 루틴이다보니 작성을 계속 미뤘는데 오랜만에 하나 적어본다.
이번 주제는 ActionMode.Callback 를 통해 "잘라내기", "복사", "붙여넣기" 기능을 커스터마이즈 하거나, "메뉴 추가" 그리고 기타 내용에 대해서 알아보려고 한다.
ActionMode 란 다음 문서에서는 자세하게 나와있다.
메뉴 추가 | Views | Android Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. 메뉴 추가 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Compose 사용해 보기 Jetpack Compose는 Android에 권
developer.android.com
그러나 설명이 기니까 짧게 얘기한다면, ActionMode 라는 것은 사용자가 취한 행동 (텍스트 롱클릭을 통한 컨텍스트 메뉴 호출이라던가 더보기 버튼을 눌러 호출한 드롭다운 컨텍스트 메뉴 호출이라던가....) 에 대한 거라고 생각하면 쉬울 것 같고, ActionMode 를 통해 상호작용되는 행동들은 ActionMode.Callback 을 통해 컨텍스트 메뉴의 [생성, 준비, 클릭 이벤트, 소멸] 을 콜백 리스너를 통하여 신호를 받을 수 있는 기능이라고 하면 될 것 같다.
여기 다음의 View 가 있다.


기본적으로 EditText 는 롱클릭을 하게 되면 컨텍스트 메뉴를 지원해주는데, 기본 기능에는 [잘라내기, 복사, 붙여넣기] 가 있다.
여기서 ActionMode.Callback 을 어떻게 적용하고 다룰 수 있냐... 하면,
1. [잘라내기, 복사, 붙여넣기] 기능을 후킹해보자.
기본 기능인 이 3개의 기능을, 어떤 요구사항에서는 "'복사' 나 '잘라내기' 를 할 때, 어떤 형식으로 복사가 되도록 처리하라!" 라는 거나, "붙여넣기 할 때에는 문자열을 한번 훑어서 텍스트를 붙여넣어라!" 라는 일이 생길 수 있다. 그럴 때에는 다음과 같이 적용할 수 있다.
ActionMode.Callback 은 Interface 이다. 그래서 다음의 메소드를 구현해야 하는데,
class ActionModeCallback: ActionMode.Callback {
// 아이템의 클릭 이뤄질 때
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean = false
// 컨텍스트 메뉴가 생성될 때
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean = true
// 컨텍스트 메뉴가 종료될 때
override fun onDestroyActionMode(mode: ActionMode?) = Unit
// 컨텍스트 메뉴가 갱신될 때
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean = false
}
여기서 onActionItemClicked 를 사용할 수 있다. 컨텍스트 메뉴 버튼을 클릭하게 되면 onActionItemClicked 가 호출되며, 접근은 item 파라미터로 할 수 있다. item 객체에서 기능 구분은 itemId 를 통해 가능한데, 각 메뉴의 ID 는 다음 값으로 구분하기 때문에 when 으로 처리할 수 있다.
- 잘라내기 : android.R.id.cut
- 복사 : android.R.id.copy
- 붙여넣기 : android.R.id.paste
ActionMode.Callback 과 EditText 를 연결하는 Setter 는 두가지를 사용할 수 있다.
- customInsertionActionModeCallback
- customSelectionActionModeCallback
customInsertionActionModeCallback 의 경우에는, EditText 내에 텍스트가 있거나 없거나 커서에 아무것도 선택되지 않았을 때 발생하는 콜백이라고 단순하게 생각하면 된다. customSelectionActionModeCallback 은 이미 텍스트가 입력이 되어있는 EditText 에 텍스트를 선택한 경우에 발생하는 콜백이라고 생각하면 쉽다. 그렇다면 단어를 선택하거나, 드래그를 통한 문자열 범위 선택에 나오는 컨텍스트 메뉴에 대해서도 customSelectionActionModeCallback 이 호출되는 것이다. 이 두 Setter 를 각각 연결해도 되고, 둘 다 연결해도 되고, 이것은 방향에 맞게 결정하면 된다.
연결이 완료되었다면 ActionMode.Callback 의 onActionItemClicked 를 구현해보도록 한다. 세부 기능은 알아서 처리해보도록 하고, print 만 찍어보며 확인할 수 있는 로직만 작성하도록 하겠다.
override fun onCreate(savedInstanceState: Bundle?) {
// ...
with(findViewById<EditText>(R.id.textarea)) {
customInsertionActionModeCallback = DefaultActionModeCallback()
customSelectionActionModeCallback = DefaultActionModeCallback()
}
}
// ActionModeCallback 은 abstract 화 시킴
private class DefaultActionModeCallback: ActionModeCallback() {
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
when(item?.itemId) {
android.R.id.copy -> println("Copy")
android.R.id.paste -> println("Paste")
android.R.id.cut -> println("Cut")
}
mode?.finish()
// true 로 놓는 경우 컨텍스트 메뉴가 종료되지 않음
// 그래서 ActionMode 에 있는 finish() 를 수동 호출해줘야 한다.
return true
}
}
onActionItemClicked 의 리턴값을 false 로 지정하게 된다면 시스템이 가지고 있던 일반적인 기능을 호출하게 되기 때문에, 일반기능과는 다른 커스터마이즈가 필요하다면 true 값으로 지정할 필요가 있다. 단, return true 인 경우에는 컨텍스트 메뉴가 사라지는 부분까지 처리해야 하며, 이 부분은 ActionMode 파라미터에 있는 finish 메소드를 사용하도록 한다.
2. 컨텍스트 메뉴를 추가 / 삭제 해보자
이번에는 컨텍스트 메뉴 항목을 추가하거나 삭제해보도록 한다. 메뉴를 구성하기 위해서는 onCreateActionMode 를 통해 가능하다. Setter 가 Insertion / Selection 두개가 있는 만큼, 두 가지 모드에 대해서 각각 처리를 할 수 있다. 그럼 간단하게 나눠 보도록 한다.
override fun onCreate(savedInstanceState: Bundle?) {
//...
with(findViewById<EditText>(R.id.textarea)) {
customInsertionActionModeCallback = InsertionActionModeCallback()
customSelectionActionModeCallback = SelectionActionModeCallback()
}
}
// Insertion 에서 일어나는 ActionMode.Callback
private class InsertionActionModeCallback: ActionModeCallback() {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
return super.onCreateActionMode(mode, menu)
}
}
// Selection 에서 일어나는 ActionMode.Callback
private class SelectionActionModeCallback: ActionModeCallback() {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
return super.onCreateActionMode(mode, menu)
}
}
Insertion 에서 나타날 메뉴와 Selection 에서 나타날 메뉴를 다르게 하기 위해서 클래스를 구분하였다. 표현하기 쉽게 각각 임의의 메뉴를 추가해보도록 하겠다.
간단하고 빠르게 구현해보면,
override fun onCreate(savedInstanceState: Bundle?) {
// ...
with(findViewById<EditText>(R.id.textarea)) {
customInsertionActionModeCallback = InsertionActionModeCallback()
customSelectionActionModeCallback = SelectionActionModeCallback()
}
}
private class InsertionActionModeCallback: ActionModeCallback() {
companion object {
private const val ID_INSERTION = 441321
}
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
menu?.add(Menu.FIRST, ID_INSERTION, Menu.NONE, "추가된메뉴")
return super.onCreateActionMode(mode, menu)
}
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
if(item?.itemId == ID_INSERTION){
println("추가된 메뉴")
mode?.finish()
return true
}
return super.onActionItemClicked(mode, item)
}
}
private class SelectionActionModeCallback: ActionModeCallback() {
companion object {
private const val ID_SELECTION = 441324
}
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
menu?.add(Menu.FIRST, ID_SELECTION, Menu.NONE, "선택된메뉴")
return super.onCreateActionMode(mode, menu)
}
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
if(item?.itemId == ID_SELECTION){
println("선택된 메뉴")
mode?.finish()
return true
}
return super.onActionItemClicked(mode, item)
}
}
각 Insertion 과 Selection 클래스에 메뉴를 추가하였고, 해당 메뉴를 클릭 할 때 어떤 동작을 해야 하는 지 클릭 이벤트도 분기처리 하는 로직이다. 이렇게 EditText 의 각 ActionMode 마다 콜백 클래스를 활용하여 기능을 부여할 수 있다


3. (기타) ActionMode.Callback 을 쓰지 않고 기본 컨텍스트 메뉴 기능을 후킹해보기
ActionMode.Callback 의 onActionItemClicked 를 통해 클릭을 후킹해봤지만, [잘라내기, 복사, 붙여넣기] 기능에 대해서는 ActionMode.Callback 을 통한 후킹 이외에도 다른 방식으로 후킹을 할 수 있다. 바로, TextView (EditText) 를 상속 받아 구현하는 경우이다. 특수한 메뉴 구성을 하지 않는 경우에는 이 방식도 고려해볼 수도 있을 것 같지만, 참고용으로만 기억하고 있을 필요가 있을 것 같다.
우선, TextView 내에는 onTextContextMenuItem 이라는 메소드가 있다. on 타입의 메소드이기 때문에 외부에서 호출하는 메소드가 아니라 상속을 통해 구현을 해야한다. EditText 는 TextView 의 확장형이기 때문에 EditText 내에도 해당 메소드를 가지고 있으니 필요시 상속 받아 구현할 수 있다.
이번에도 간단하게 구현해보자. 먼저, EditText 를 상속 받은 클래스를 준비하도록 한다. 이 클래스는 onTextContextMenuItem 을 오버라이드 했다.
class CEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.editTextStyle):
EditText(context, attrs, defStyleAttr) {
override fun onTextContextMenuItem(id: Int): Boolean {
when(id){
android.R.id.copy -> {
println("Copy")
return true
}
android.R.id.paste -> {
println("Paste")
return true
}
android.R.id.cut -> {
println("Cut")
return true
}
}
return super.onTextContextMenuItem(id)
}
}
다음은, XML 에 추가만 해주고 실행하면 된다.
onTextContextMenuItem 은 ActionMode.Callback 의 onAcitonItemClicked 와 같이 리턴값을 true 로 설정하게 되면 컨텍스트 메뉴가 사라지지 않는다. 하지만 onTextContextMenuItem 에는 ActionMode 객체를 받지 않기 때문에 종료처리를 어떻게 해야 하나 싶다. 이럴 때는 clearFocus() 를 호출해주면 된다.
추가로, ActionMode.Callback 과 onTextContextMenuItem 을 같이 구현하는 경우, 클릭 이벤트의 순서는 ActionMode.Callback > onTextContextMenuItem 순으로 이어진다. 개발 방법은 편의대로 구현하면 될 것 같다
| [Android] 다음 주소 찾기 서비스를 편하게 사용하기 (1) | 2023.11.23 |
|---|---|
| [Android] Custom View 에서의 롱클릭 후 드래그 허용하기 (0) | 2023.06.07 |
| Groovy 에서 KTS 로 변경하기 (1) | 2023.01.26 |
| [Android] Logcat 에서 보고 싶은 태그만 필터링하기 (0) | 2022.05.16 |
| [Android] Web 에서 App 으로 데이터를 받아보자 (0) | 2022.02.04 |