---
# System prepended metadata

title: SelectionActionMode
tags: [Android, Kotlin]

---

###### tags: `Android` `Kotlin`
# SelectionActionMode
文本選取動作是指在選取文本後，跳出一欄視窗，而視窗提供各種功能
基本提供了複製、貼上、全選、分享，更進階的還有開啟地圖搜尋地址
如下圖所示 (6.0版本的介面不同，左為6.0以上、右為6.0以下)

![](https://hackmd.io/_uploads/HkwXiFAV3.png)

## 系統預設選取動作
EditText 本身就預設有被選取的功能
TextView 需額外加上可被選取的屬性，可用 xml 或 code 設定
設定完後，在選取文本時就會產生視窗，提供系統預設的功能

xml
```xml=
android:textIsSelectable="true"
```

code
```kotlin=
textView.setTextIsSelectable(true)
```

## 客製化選取動作
如果希望有系統預設外的功能，就要進行客製化
還好系統有提供開發者客製化的函式 ==customSelectionActionModeCallback==
開發者只要實作 ==ActionMode.Callback 或 ActionMode.Callback2== 即可

||Callback|Callback2|
|:--:|:--:|:--:|
|差異|不限制版本|6.0版本以上、多一個 onGetContentRect 可選方法|

系統客製化分成==插入新動作、完全取代==兩種
前者是除預設動作外，再額外增加新動作
後者是沒有預設動作，僅有自己增加的新動作

:::danger
有些手機不支援系統提供的客製化，例如：小米系列，這樣就只能全部自己動手刻
:::

### 插入新動作
==以下使用 Callback2 實作==
先在 res 建立 menu (名稱：selection_action_menu)，並建立 item
```xml=
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item
            android:id="@+id/item_toast"
            android:title="Toast" />
    <item
            android:id="@+id/item_snackbar"
            android:title="Snack bar" />
</menu>
```

在 layout 檔建立 TextView，並在 activity onCreate 中輸入程式碼
完成後就如圖一所示，點擊新增的動作，顯示被選取的文本內容
```kotlin=
//判斷 Android 6.0 以上
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {

    tv_self_custom.customSelectionActionModeCallback = object : ActionMode.Callback2() {
        override fun onCreateActionMode(p0: ActionMode?, p1: Menu?): Boolean {
            Log.e("lifecycle", "onCreateActionMode")
            val menuInflater = p0?.menuInflater
            menuInflater?.inflate(R.menu.selection_action_menu, p1)
            return true//返回 false 則不會顯示彈窗
        }

        override fun onPrepareActionMode(p0: ActionMode?, p1: Menu?): Boolean {
            Log.e("lifecycle", "onPrepareActionMode")
            return false
        }

        override fun onActionItemClicked(p0: ActionMode?, p1: MenuItem?): Boolean {
            val content = tv_self_custom.text.substring(tv_self_custom.selectionStart, tv_self_custom.selectionEnd)
            when (p1?.itemId) {
                R.id.item_toast -> {
                    Toast.makeText(this@MainActivity, content, Toast.LENGTH_SHORT).show()
                    p0?.finish()//收起操作菜單
                }
                R.id.item_snackbar -> {
                    Snackbar.make(tv_self_custom, content, Snackbar.LENGTH_SHORT).show()
                    p0?.finish()
                }
            }
            return false//返回 true 則系統的"複製"、"搜尋"的item將無效(但還是會顯示)，只有自定義有回應
        }

        override fun onDestroyActionMode(p0: ActionMode?) {
            Log.e("lifecycle", "onDestroyActionMode")
        }

        //可選 用於改變彈出菜單位置
        override fun onGetContentRect(mode: ActionMode?, view: View?, outRect: Rect?) {
            super.onGetContentRect(mode, view, outRect)
        }
    }
}
```

### 完全取代
==以下使用 Callback 實作==
一樣先在 res 建立 menu (名稱：selection_action_menu)，並建立 item
在 layout 檔建立 TextView，並在 activity onCreate 中輸入程式碼
與插入新動作的差別在於 ==onCreateActionMode、onPrepareActionMode==

```kotlin=
tv_custom.customSelectionActionModeCallback = object : ActionMode.Callback2() {
    override fun onCreateActionMode(p0: ActionMode?, p1: Menu?): Boolean {
        return true
    }

    override fun onPrepareActionMode(p0: ActionMode?, p1: Menu?): Boolean {
        val menuInflater = p0?.menuInflater
        p1?.clear() //因 onPrepareActionMode 會執行兩次，所以要清除上次加入的 menu
        menuInflater?.inflate(R.menu.selection_action_menu, p1)
        return true
    }

    override fun onActionItemClicked(p0: ActionMode?, p1: MenuItem?): Boolean {
        val content = tv_custom.text.substring(tv_custom.selectionStart, tv_custom.selectionEnd)
        when (p1?.itemId) {
            R.id.item_toast -> {
                Toast.makeText(this@MainActivity, content, Toast.LENGTH_SHORT).show()
                p0?.finish()
            }
            R.id.item_snackbar -> {
                Snackbar.make(tv_custom, content, Snackbar.LENGTH_SHORT).show()
                p0?.finish()
            }
        }
        return false
    }

    override fun onDestroyActionMode(p0: ActionMode?) {}
}
```

## 參考文章
[文本選擇菜單](https://www.jianshu.com/p/89970f098012)
[自己刻選擇菜單](https://jaeger.itscoder.com/android/2016/11/21/selectable-text-helper.html)

## 疑難雜症
在做 listView holder 中的 SelectionActionMode 遇到了一些問題
1. 有些 item 無法被選取
2. 若使用者不點擊 ActionMode 要把 View 回收掉
3. item 點擊不靈敏，需要點兩次才觸發點擊事件或selection

### 有些 item 無法被選取
這問題很奇妙，查了一下別人也有遇到這個問題 
https://codeday.me/bug/20180820/224852.html
解決辦法：把 Adapter layout 的那個能選取的元件改成 wrap_content
但這會造成選取功能需要選取完再按一次，才會出現功能框

### 若使用者不點擊 ActionMode 要把 View 回收掉(有問題)
==有問題、待測試==
經自己驗證後發現onClick不會被調用，應該直接在adapter destroy actionmode 就好

以下原文：
如果不主動消失 listView 拖動後可能會有意料之外的問題

解決思路：
先在 Adapter 儲存 ActionMode 的實體，然後在它的兩個生命週期中處理
```kotlin=
override fun onPrepareActionMode(p0: ActionMode?, p1: Menu?): Boolean {
    actionMode = p0
    //...略
}

override fun onDestroyActionMode(p0: ActionMode?) {
    actionMode = null //消失時清除儲存值
}
```

接下來，我第一個想法是讓 ActionMode 在 listView 拖動時調用 actionMode?.finish()
所以設置 setOnScrollListener，但 listView 會一直奪取焦點，造成無法將選取的範圍拖動，宣告失敗

最後解決辦法是將能選取的元件設置 setOnClickListener，再設置 setOnFocusChangeListener
一但焦點改變就進行 actionMode?.finish()，接下來只要在 Adapter 刷新畫面時，把 viewGroup 的焦點都清除，概念如下

```kotlin=
override fun getView(position: Int, convertView: View?, viewGroup: ViewGroup): View? {
    viewGroup.clearFocus() //清除父視圖焦點
    
    holder.tv_main.setOnClickListener {
        it.setOnFocusChangeListener { v, hasFocus ->
            actionMode?.finish()
        }
    }
        
    //...略
}
```

### item 點擊不靈敏，需要點兩次才觸發點擊事件或selection
第一個可能是因為 item 的 SelectionActionMode 與點擊事件衝突
而真正的原因是 textIsSelectable 的屬性，如果使用 TextView 要自己設定
如果是 EditText 預設就有，所以只要這個屬性是 true 的狀態
點擊事件就會有問題，連帶影響 SelectionActionMode 不靈敏

第二個可能是 layout，這應該是 Android 的 Bug，因為出現有些 item 無法被選取
所以我調整 layout 變成 wrap_content，這樣造成選取功能需要選取完再按一次，才會出現功能框
因此如果要解決不靈敏問題，就要改回 match_parent，但又會出現有些 item 無法被選取的情況

## 未完成事項
- 撰寫 自己刻文本選擇動作 的內容
- ActionMode 風格修改