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 version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:title="@string/menu_favorite"
android:id="@+id/item_action_favorite"
android:icon="@drawable/ic_favorite_svg"/>
<item
android:title="@string/menu_cjsh"
android:id="@+id/item_action_cjsh"/>
</menu>
- 2.实现
ActionMode.Callback
接口
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.启动关联操作模式
//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之后会执行到下面这里
//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方法里面
//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
内部实现
//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
//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
//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)
实现如下
//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方法
//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
//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中,那么系统自带的如何做的呢?往下看分析:
//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)
这个方法
//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
<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. 处理意图
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正确的保活方案,不要掉进保活需求死循环陷进