###### 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 風格修改