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

## 系統預設選取動作
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 風格修改