閒聊Android懸浮的“系統文字選擇選單”和“ActionMode解析”——附上原理分析
theme: arknights highlight: androidstudio
- 小知識,大挑戰!本文正在參與“程式設計師必備小知識”創作活動。
- 本文已參與 「掘力星計劃」 ,贏取創作大禮包,挑戰創作激勵金。
1.前言
國慶節釣魚的時候,也沒閒著,就想起之前做的一款筆記類app的時候,給系統文字選擇選單
增加過一個新的選項入口,點選此入口,可以獲取選擇的文字內容,傳遞到我們的app裡面(有些小夥伴已經知道這其實能做很多事情
),就想著給大家分享一下,具體使用起來其實很簡單,且聽我們慢慢道來,不要著急划走,有實現原理分析
下面我們從如何使用 及 原始碼這兩個角度去展開介紹:ActionMode 和 系統文字選擇選單,至於為什麼把這兩個放到一起,我相信你們看完本篇文章,會有自己的見解
2.ActionMode
此處的官方使用指南有了,我為什麼還要寫?
1.想寫就寫咯,🙃太無聊了
2.寫例子玩玩🤣😅
ActionMode是一個抽象類,它將使用者互動的重點放在執行關聯操作上,ActionMode有兩種模式,一種是:TYPE_PRIMARY(預設模式)
、另一種是:TYPE_FLOATING(浮動工具欄)
A.如何使用
ActionMode內部有個Callback,註釋中寫道可以使用
View.startActionMode(ActionMode.Callback) 或者View.startActionMode(ActionMode.Callback,int type) 來啟動
Activity裡面的startActionMode最終也是呼叫的View.startActionMode
使用起來也非常簡單,示例如下:
- 1.提供一個上下文選單的資源xml
```xml
``
- 2.實現
ActionMode.Callback` 介面
kotlin
var actionMode:ActionMode? = null
val actionModeCallback = object: ActionMode.Callback{
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
//提供上下文選單項的選單資源,官方文件使用指南里面有
mode?.menuInflater?.inflate(R.menu.context_menu,menu)
return true
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
return false
}
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
return when (item?.itemId) {
R.id.item_action_favorite -> {
Toast.makeText(applicationContext,"❤️收藏成功",Toast.LENGTH_SHORT).show()
mode?.finish()
true
}
.....
else -> false
}
}
override fun onDestroyActionMode(mode: ActionMode?) {
actionMode = null
}
}
- 3.啟動關聯操作模式
```kotlin //type = TYPE_PRIMARY actionMode = it.startActionMode(actionModeCallback)
//type = TYPE_FLOATING if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { actionMode = it.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING) } ```
示例-演示效果
B.原始碼分析
當呼叫了View.startActionMode之後會執行到下面這裡
```java //android.view.View
public ActionMode startActionMode(ActionMode.Callback callback, int type) { ViewParent parent = getParent(); if (parent == null) return null; try { //開始遞迴呼叫ViewGroup的startActionModeForChild return parent.startActionModeForChild(this, callback, type); } catch (AbstractMethodError ame) { // 使用預設型別ActionMode.TYPE_PRIMARY為指定檢視啟動操作模式 return parent.startActionModeForChild(this, callback); } } ``` 遞迴呼叫ViewGroup的startActionModeForChild,最終會執行到DecorView的startActionMode方法裡面
```java //com.android.internal.policy.DecorView
private ActionMode startActionMode(
View originatingView, ActionMode.Callback callback, int type) {
ActionMode.Callback2 wrappedCallback = new ActionModeCallback2Wrapper(callback);
ActionMode mode = null;
//mWindow是PhoneWindow
if (mWindow.getCallback() != null && !mWindow.isDestroyed()) {
try {
//此處將會觸發 AppCompatWindowCallback#onWindowStartingActionMode
mode = mWindow.getCallback().onWindowStartingActionMode(wrappedCallback, type);
} catch (AbstractMethodError ame) {
......
}
}
if (mode != null) {
......
} else {
//本篇文章示例中
//當呼叫startMode傳入的type=ActionMode.TYPE_FLOATING會執行到這裡
//內部執行到createFloatingActionMode方法
//然後內部初始化一個FloatingToolbar並返回FloatingActionMode
//FloatingToolbar:是一個顯示上下文選單項的浮動工具欄,內部是通過popWindow實現的,感興趣的同學可以研究一下
mode = createActionMode(type, wrappedCallback, originatingView);
if (mode != null && wrappedCallback.onCreateActionMode(mode, mode.getMenu())) {
setHandledActionMode(mode);
} else {
mode = null;
}
}
if (mode != null && mWindow.getCallback() != null && !mWindow.isDestroyed()) {
try {
//回撥Activity裡面的onActionModeStarted方法空實現
//通知Activity,ActionMode已經啟動了
mWindow.getCallback().onActionModeStarted(mode);
} catch (AbstractMethodError ame) {
}
}
return mode;
}
``
我們看一下
AppCompatWindowCallback#onWindowStartingActionMode`內部實現
```java //androidx.appcompat.app.AppCompatDelegateImpl.AppCompatWindowCallback
public android.view.ActionMode onWindowStartingActionMode(
android.view.ActionMode.Callback callback, int type) {
if (isHandleNativeActionModesEnabled()) {
switch (type) {
case android.view.ActionMode.TYPE_PRIMARY:
// TYPE_PRIMARY型別觸發此方法呼叫
return startAsSupportActionMode(callback);
}
}
// 不滿足上面的條件,最終會執行到Activity的onWindowStartingActionMode
return super.onWindowStartingActionMode(callback, type);
}
``
繼續看一下
startAsSupportActionMode`
```java //androidx.appcompat.app.AppCompatDelegateImpl.AppCompatWindowCallback
final android.view.ActionMode startAsSupportActionMode( android.view.ActionMode.Callback callback) { // ActionMode.Callback包裝器 final SupportActionModeWrapper.CallbackWrapper callbackWrapper = new SupportActionModeWrapper.CallbackWrapper(mContext, callback);
// 往下翻,有分析
final androidx.appcompat.view.ActionMode supportActionMode =
startSupportActionMode(callbackWrapper);
if (supportActionMode != null) {
//返回包裝後的ActionMode
return callbackWrapper.getActionModeWrapper(supportActionMode);
}
return null;
}
我們看一下上面的`startSupportActionMode`
java
//androidx.appcompat.app.AppCompatDelegateImpl
public ActionMode startSupportActionMode(@NonNull final ActionMode.Callback callback) {
......
//包裝Callback,當action mode被銷燬時,清除內部引用
final ActionMode.Callback wrappedCallback = new ActionModeCallbackWrapperV9(callback);
ActionBar ab = getSupportActionBar();
if (ab != null) {
//此處的supportActionBar是WindowDecorActionBar
mActionMode = ab.startActionMode(wrappedCallback);
......
}
......
return mActionMode;
}
`ab.startActionMode(wrappedCallback)`實現如下
java
//androidx.appcompat.app.WindowDecorActionBar
public ActionMode startActionMode(ActionMode.Callback callback) { ...... //內部會初始化MenuBuilder,並繫結MenuBuilder.Callback ActionModeImpl mode = new ActionModeImpl(mContextView.getContext(), callback); //會觸發:WindowDecorActionBar#dispatchOnCreate //最終會回撥到我們上面示例demo中的ActionMode.Callback //實現的onCreateActionMode方法中,然後解析menu.xml,將xml填充到menu中 if (mode.dispatchOnCreate()) { mActionMode = mode; //狀態變化或者檢視更新 mode.invalidate(); //mContextView是ActionBarContextView //初始化一個返回按鈕的mClose的View,並將mClose新增到ActionBarContextView裡面 //根據mMenuLayoutRes獲取mMenuView //接著初始化多個MenuView.ItemView並填充到mMenuView並更新MenuView在容器中的位置 //執行mMenuView.requestLayout重新整理檢視 mContextView.initForMode(mode); //執行DecorToolBar的INVISIBLE和AppBarContextView的VISIBLE動畫 animateToMode(true); //傳送視窗狀態變更的事件(只有使用了AccessbilityService服務的才可以感知) mContextView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); return mode; } return null; } ```
C.小結
(1). View.startActionMode呼叫之後,通過遞迴呼叫最終會執行到DecorView的startActionMode方法
(2). DecorView內部會呼叫AppCompatWindowCallback#onWindowStartingActionMode
當type = ActionMode.TYPE_PRIMARY時:
內部觸發WindowDecorActionBar#startActionMode
,然後初始化ActionModeImpl,
並執行dispatchOnCreate方法,dispatchOnCreate方法最終會回撥ActionMode.Callback介面內部的onCreateActionMode方法中,
由開發者呼叫menuInflate.inflate將xml填充到menu中(即MenuBuilder)
,
然後將mClose(ImageView)
新增到ActionBarContextView中,初始化mMenuView,取出ActionMode內部的MenuBuilder資料
填充多個MenuView.ItemView然後更新檢視位置順序,呼叫requestLayout重新整理檢視,執行DecorToolBar的INVISIBLE和AppBarContextView的VISIBLE動畫來控制隱藏和顯示;
當type = ActionMode.TYPE_FLOATING時:
mWindow.getCallback().onWindowStartingActionMode方法即(AppCompatWindowCallback#onWindowStartingActionMode)
返回的是null,接下來會執行createActionMode方法,並在內部執行createFloatingActionMode方法:初始化一個FloatingToolbar並返回FloatingActionMode;
FloatingToolbar:是一個顯示上下文選單項的浮動工具欄,內部是通過popWindow實現的;
3.系統文字選擇選單
我們看一下下面兩個問題: - A、如何彈出系統文字選擇選單?系統文字選擇選單內部是怎麼實現的? - B、如何給系統文字選擇選單增加屬於自己app的選項?
A.如何彈出系統文字選擇選單?及內部實現
舉個例子,我們在使用TextView顯示文字的時候,如果想內容可以被選中,可以顯示覆制、全選按鈕,這個時候使用TextView的方法setTextIsSelectable(boolean selectable)
就可以了,裡面做了什麼,是什麼原理?
我們開啟TextView原始碼檢視setTextIsSelectable方法
```java //android.widget.TextView
public void setTextIsSelectable(boolean selectable) { if (!selectable && mEditor == null) return; //初始化Editor createEditorIfNeeded(); //防止重複設定 if (mEditor.mTextIsSelectable == selectable) return; //更新mTextIsSelectable mEditor.mTextIsSelectable = selectable; ...... } ``` 我們進Editor裡面看mTextIsSelectable,發現在Editor的內部類TextActionModeCallback初始化的時候使用此變數,原來這裡使用的也是ActionMode
```java //android.widget.Editor
void startInsertionActionMode() { ...... ActionMode.Callback actionModeCallback = new TextActionModeCallback(TextActionMode.INSERTION); //這裡啟動的是TYPE_FLOATING型別的ActionMode,內部是popwindow實現,彈出系統文字選擇選單 mTextActionMode = mTextView.startActionMode( actionModeCallback, ActionMode.TYPE_FLOATING); if (mTextActionMode != null && getInsertionController() != null) { //如果游標插入控制器存在的話,會重新彈出一個選單,用於給使用者貼上內容用的 getInsertionController().show(); } } ```
B.如何給系統文字選擇選單增加屬於自己app的選項?
我們要找到系統在哪新增這些選項的?仍然以TextView為例
剛剛上面我們提到TextActionModeCallback,我們從上面ActionMode分析知道,在onCreateActionMode裡面會把menu.xml中的內容填充到Menu中,那麼系統自帶的如何做的呢?往下看分析:
```java //android.widget.Editor.TextActionModeCallback
private class TextActionModeCallback extends ActionMode.Callback2 {
......
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
......
//這裡面都是系統動態新增的menu選項,如:剪下、貼上、複製、分享等等
populateMenuWithItems(menu);
Callback customCallback = getCustomCallback();
if (customCallback != null) {
if (!customCallback.onCreateActionMode(mode, menu)) {
//上面條件成立,取消選擇的文字
Selection.setSelection((Spannable) mTextView.getText(),
mTextView.getSelectionEnd());
return false;
}
}
if (mTextView.canProcessText()) {
//如果當前文字支援分享、複製、長度大於0 && 選擇的長度大於0等條件成立
//從原始碼的註釋中可以看到:查詢出 “Intent.ACTION_PROCESS_TEXT” 符合條件Activity列表新增到menu中
mProcessTextIntentActionsHandler.onInitializeMenu(menu);
}
......
return true;
}
......
private void populateMenuWithItems(Menu menu) {
if (mTextView.canCut()) {//剪下
menu.add(Menu.NONE, TextView.ID_CUT,......);
}
if (mTextView.canCopy()) {//複製
menu.add(Menu.NONE, TextView.ID_COPY, ......);
}
......
if (mTextView.canRequestAutofill()) {//自動填充
menu.add(Menu.NONE, TextView.ID_AUTOFILL,......);
}
if (mTextView.canPasteAsPlainText()) {//貼上
menu.add(Menu.NONE,TextView.ID_PASTE_AS_PLAIN_TEXT,......);
}
......
}
......
}
``
我們看一下
mProcessTextIntentActionsHandler.onInitializeMenu(menu)`這個方法
java
//android.widget.Editor.ProcessTextIntentActionsHandler
public void onInitializeMenu(Menu menu) {
//加載出所有action含PROCESS_TEXT的Activity列表
loadSupportedActivities();
final int size = mSupportedActivities.size();
for (int i = 0; i < size; i++) {
final ResolveInfo resolveInfo = mSupportedActivities.get(i);
//獲取到支援的列表,動態新增到menu中
menu.add(Menu.NONE, Menu.NONE,
Editor.MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i,
getLabel(resolveInfo))
.setIntent(createProcessTextIntentForResolveInfo(resolveInfo))
.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
}
}
//加載出所有支援處理PROCESS_TEXT的Intent
private void loadSupportedActivities() {
mSupportedActivities.clear();
if (!mContext.canStartActivityForResult()) {
return;
}
PackageManager packageManager = mTextView.getContext().getPackageManager();
//查詢符合條件的意圖
List<ResolveInfo> unfiltered =
packageManager.queryIntentActivities(createProcessTextIntent(), 0);
for (ResolveInfo info : unfiltered) {
if (isSupportedActivity(info)) {
mSupportedActivities.add(info);
}
}
}
//處理PROCESS_TEXT的Intent
private Intent createProcessTextIntent() {
return new Intent()
.setAction(Intent.ACTION_PROCESS_TEXT)
.setType("text/plain");
}
結合我們上面分析的ActionMode內容,這一下就全部明白了,那麼我們給系統的文字選擇選單增加一個選項入口還不是很簡單嗎?
- 1. 將意圖過濾器新增到AndroidManifest.xml
xml
<activity
android:name=".CustomTextActivity"
android:excludeFromRecents="false"
android:configChanges="locale|orientation|keyboardHidden|screenSize"
android:label="點贊❤️+收藏❤️=學會❤️">
<intent-filter>
<action android:name="android.intent.action.PROCESS_TEXT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain"/>
</intent-filter>
</activity>
- 2. 處理意圖
kotlin
class CustomTextActivity : AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_custom_text)
val selectedText = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) intent?.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT) else null
textShowContent.text = "$selectedText \n\n 文字內容長度: ${selectedText?.length?:0}"
}
}
系統文字選擇選單-增加選項(演示示例)
往期文章推薦:
1.Android跨程序傳大圖思考及實現——附上原理分析
2.Jetpack Compose實現bringToFront功能——附上原理分析
3.Jetpack Compose UI建立佈局繪製流程+原理 —— 內含概念詳解(滿滿乾貨)
4.Jetpack App Startup如何使用及原理分析
5.Jetpack Compose - Accompanist 元件庫
6.原始碼分析 | ThreadedRenderer空指標問題,順便把Choreographer認識一下
7.原始碼分析 | 事件是怎麼傳遞到Activity的?
8.聊聊CountDownLatch 原始碼
9.Android正確的保活方案,不要掉進保活需求死迴圈陷進
- 鴻蒙ArkUI如何開發跨平臺應用?
- HarmonyOS玩轉ArkUI動效 - 水母動畫
- Compose挑燈夜看 - 照亮手機螢幕裡面的書本內容
- 順手修復了Jetpack Compose官方文件中的一個多點觸控示例的Bug
- 正確實踐Jetpack SplashScreen API —— 在所有Android系統上使用總結,內含原理分析
- Jetpack Compose處理“導航欄、狀態列、鍵盤” 影響內容顯示的問題集錦
- 閒聊Android懸浮的“系統文字選擇選單”和“ActionMode解析”——附上原理分析
- Jetpack Compose實現bringToFront功能——附上原理分析
- Android跨程序傳大圖思考及實現——附上原理分析