android系统文本选择器添加
看这里https://www.jianshu.com/p/89970f098012?from=jiantop.com
先看下效果图
image.png image.png可以看到,在默认的复制,共享等选项后边多两个。
一个是今日头条的,一个是我们自己弄的名字叫Demo
显示的文字和图标是这么来的,不过我们textview显示的弹框只有文字,
不知道为啥备忘录里的弹框显示的是图片+文字[后来感觉这玩意可能是软件自定义的]
image.png
就是这么简单,自定义一个activity,然后添加如下的intent-filter就可以了,这样其他app弹出文本选择框的时候我们这个activity就会出现在选项上了。如果不需要可以设置android:exported="false"
点击弹框选项那个demo,就跳到我们的activity拉
<activity
android:name=".a1.ActivityATest"
android:icon="@drawable/love_red"
android:exported="true"
android:label="demo" >
<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>
完事在activity里处理你要做的事,
数据有2个,如下
EXTRA_PROCESS_TEXT:获取的是选中的文本
EXTRA_PROCESS_TEXT_READONLY:那个文本的可读状态,如果是edittext这种可编辑的就是true了,textview这种不可以编辑的就是false
if(TextUtils.equals(intent.action,Intent.ACTION_PROCESS_TEXT)){
val content=intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT)
val readOnly=intent.getBooleanExtra(Intent.EXTRA_PROCESS_TEXT_READONLY,false)
println("select result=========${content}=====$readOnly")
}
看下这几个参数的注释
/**
* Activity Action: Process a piece of text.
* <p>Input: {@link #EXTRA_PROCESS_TEXT} contains the text to be processed.
* {@link #EXTRA_PROCESS_TEXT_READONLY} states if the resulting text will be read-only.</p>
* <p>Output: {@link #EXTRA_PROCESS_TEXT} contains the processed text.</p>
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_PROCESS_TEXT = "android.intent.action.PROCESS_TEXT";
/**
* The name of the extra used to define the text to be processed, as a
* CharSequence. Note that this may be a styled CharSequence, so you must use
* {@link Bundle#getCharSequence(String) Bundle.getCharSequence()} to retrieve it.
*/
public static final String EXTRA_PROCESS_TEXT = "android.intent.extra.PROCESS_TEXT";
/**
* The name of the boolean extra used to define if the processed text will be used as read-only.
*/
public static final String EXTRA_PROCESS_TEXT_READONLY =
"android.intent.extra.PROCESS_TEXT_READONLY";
如何修改文本选择器
在自己的app里如果想修改文本选择器咋办?
这个参考开头的帖子就行,textView设置setTextIsSelectable(true)即可
自己处理触摸事件弹框咋处理
setTextIsSelectable 系统会自动处理弹框事件的,可如果是自定义的咋办?
我的情况是一个viewgroup里包含2个textview,而我要处理viewgroup的触摸事件,如果设置了setTextIsSelectable为true,那么触摸事件就被textview消费了,viewgoup没法处理了,所以只能自己处理文字选中的操作了
思路
- 监听触摸事件,如果在1秒内没有移动,那我就按照选择文字的事件处理,否则按照移动处理。
- 这里就要模拟文字选中了。找到手指按下的文字位置,以及最后移动的位置,完事给文字添加背景色
- 模仿系统的弹个选项出来
根据坐标点的位置,可以通过如下方法获取文字的索引
val index=tv_show.getOffsetForPosition(event.getX(),event.getY())
添加选中的背景,start,end 就是按下的位置获取的index和移动的位置获取的index,谁小谁就是start
val ss=SpannableString(tv_show.text.toString())
ss.setSpan(BackgroundColorSpan(Color.RED),start,end,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
背景有了,完事就是弹框了。在event为ACTION_UP的时候弹框出来
var actionMode:ActionMode?=null//下次action_down的时候调用finish方法关闭弹框
if (event.actionMasked==MotionEvent.ACTION_UP ) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
actionMode= tv_show.startActionMode(callback,ActionMode.TYPE_FLOATING)
}else{
tv_show.startActionMode(callback)
}
}
ActionMode说下
弹框的模式,有两种
一种是悬浮的TYPE_FLOATING,一种TYPE_PRIMARY就是显示在toolbar位置的,如下图
TYPE_FLOATING.png TYPE_PRIMARY.png
弹框的内容是通过menu添加的,看下xml文件
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/Informal22"
android:title="自定义22" />
<item
android:id="@+id/Informal33"
android:title="自定义2222222222" />
<item
android:id="@+id/Informal44"
android:icon="@drawable/love_red"
android:title="自定义2" />
</menu>
弹框模式要分版本,api23以下的我们无法设置模式,就是默认的TYPE_PRIMARY,而且调用的方法callback也有2种,也是23以上和以下两种
callback
23以上的callback2多了个方法,可以控制悬浮bar的位置,就是那个outRect,这个outRect设置的是选中文本的范围,悬浮弹框会自动显示在这个范围的上边或者下边
public static abstract class Callback2 implements ActionMode.Callback {
/**
* Called when an ActionMode needs to be positioned on screen, potentially occluding view
* content. Note this may be called on a per-frame basis.
*
* @param mode The ActionMode that requires positioning.
* @param view The View that originated the ActionMode, in whose coordinates the Rect should
* be provided.
* @param outRect The Rect to be populated with the content position. Use this to specify
* where the content in your app lives within the given view. This will be used
* to avoid occluding the given content Rect with the created ActionMode.
*/
public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
if (view != null) {
outRect.set(0, 0, view.getWidth(), view.getHeight());
} else {
outRect.set(0, 0, 0, 0);
}
}
}
看下callback2的简单实现,方法的解释可以自己看下源码
object : ActionMode.Callback2() {
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
println("2==============onActionItemClicked===" + item.title)
if (item.itemId==android.R.id.selectAll) {
mode.invalidate()//会刷新弹框,调用onPrepareActionMode方法
} else {
mode.finish()//弹窗bar会关闭
}
//这里根据item的id可以自己处理事件,要干啥。
return false
}
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
println("2==============onCreateActionMode===" + menu.size())
menu.clear()
mode.menuInflater.inflate(R.menu.select_menu2, menu)
return true
}
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
println("2==============onPrepared===" + menu.size())
return true
}
override fun onDestroyActionMode(mode: ActionMode) {
println("2==============onDestroyActionMode===" + mode.title)
}
override fun onGetContentRect(mode: ActionMode?, view: View?, outRect: Rect) {
//start ,end就是选中的文字的起始和结尾的索引
//下边的方法是仿照textview源码里写的,
var mSelectionPath=Path()
val mSelectionBounds=RectF()
tv_show.getLayout().getSelectionPath(start, end, mSelectionPath)
mSelectionPath.computeBounds(mSelectionBounds, true)
outRect.set((mSelectionBounds.left+tv_show.paddingLeft).toInt(), (mSelectionBounds.top+tv_show.paddingTop).toInt(),
(mSelectionBounds.right+tv_show.paddingRight).toInt(), (mSelectionBounds.bottom+tv_show.paddingBottom).toInt())
}
}
其他问题说明
看下系统的选中一行最后一个文字的效果,这个返回的outRect是最后两行的范围
image.png
如果不是最后一个文字,这个返回的outRect就是选中文字的范围
image.png
为啥差距这么大,分析下返回的那个outRect,其实也就是这个path的结果,
tv_show.getLayout().getSelectionPath(start, end, mSelectionPath)
看下这个方法
对于选中一行最后一个文字,那么start和end获取的startline和endline就差一行拉。
public void getSelectionPath(int start, int end, Path dest) {
dest.reset();
if (start == end)//start和end一样就返回空了,
return;
if (end < start) {
int temp = end;
end = start;
start = temp;
}
int startline = getLineForOffset(start);
int endline = getLineForOffset(end);
int top = getLineTop(startline);
int bottom = getLineBottom(endline);
if (startline == endline) {
//同一行的比较简单了,top和bottom都有了,完事根据文字的start和end再计算出left和right即可
addSelection(startline, start, end, top, bottom, dest);
} else {
final float width = mWidth;
//把startline这行的左右上下范围添加进来
addSelection(startline, start, getLineEnd(startline),
top, getLineBottom(startline), dest);
//省略代码
for (int i = startline + 1; i < endline; i++) {
top = getLineTop(i);
bottom = getLineBottom(i);
//下一行的话就是整行范围加进来了。所以,其实只要换行了,那么悬浮bar就是居中的拉
dest.addRect(0, top, width, bottom, Path.Direction.CW);
}
top = getLineTop(endline);
bottom = getLineBottom(endline);
addSelection(endline, getLineStart(endline), end,
top, bottom, dest);
if (getParagraphDirection(endline) == DIR_RIGHT_TO_LEFT)
dest.addRect(width, top, getLineRight(endline), bottom, Path.Direction.CW);
else
dest.addRect(0, top, getLineLeft(endline), bottom, Path.Direction.CW);
}
}
看下选择器的弹出代码
以TextView为例,我们知道有两种情况
1.手指长按事件就会弹出来
2.滑动的时候弹框消失了,之后需要在手指离开屏幕的时候,弹框出来的,那么就看下触摸事件
public boolean onTouchEvent(MotionEvent event) {
if (mEditor != null) {
mEditor.onTouchEvent(event);
}
//省略
去Editor里看下onTouchEvent,找名字差不多的
void onTouchEvent(MotionEvent event) {
updateFloatingToolbarVisibility(event);
}
//继续看上边的方法
private void updateFloatingToolbarVisibility(MotionEvent event) {
if (mTextActionMode != null) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_MOVE://上边说的,长按弹出,移动的时候会消失的
hideFloatingToolbar(ActionMode.DEFAULT_HIDE_DURATION);
break;
case MotionEvent.ACTION_UP: // fall through
case MotionEvent.ACTION_CANCEL://然后在松开手指的时候又弹出来了。
showFloatingToolbar();
}
}
}
showFloatingToolbar()继续看,执行了一个Runnable
private void showFloatingToolbar() {
if (mTextActionMode != null) {
mTextView.postDelayed(mShowFloatingToolbar, delay);
}
}
//
private final Runnable mShowFloatingToolbar = new Runnable() {
@Override
public void run() {
if (mTextActionMode != null) {
mTextActionMode.hide(0); // hide off.
}
}
};
//变量这样来的
mTextActionMode = mTextView.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING);
回到TextView,可以看到调用的parent的方法,那去ViewGroup里找,然后会发现它里边又继续调用了parent的方法,所以啊,我们就直接去最底层的view去看,也就是DecorView了
public ActionMode startActionMode(ActionMode.Callback callback, int type) {
ViewParent parent = getParent();
if (parent == null) return null;
try {
return parent.startActionModeForChild(this, callback, type);
} catch (AbstractMethodError ame) {
// Older implementations of custom views might not implement this.
return parent.startActionModeForChild(this, callback);
}
}
DecorView的代码
private ActionMode startActionMode(
View originatingView, ActionMode.Callback callback, int type) {
ActionMode.Callback2 wrappedCallback = new ActionModeCallback2Wrapper(callback);
ActionMode mode = null;
if (mWindow.getCallback() != null && !mWindow.isDestroyed()) {
try {
//这里返回null,解释在下边
mode = mWindow.getCallback().onWindowStartingActionMode(wrappedCallback, type);
} catch (AbstractMethodError ame) {
// Older apps might not implement the typed version of this method.
//23以上type是floating,这里不会走
if (type == ActionMode.TYPE_PRIMARY) {
try {
mode = mWindow.getCallback().onWindowStartingActionMode(
wrappedCallback);
} catch (AbstractMethodError ame2) {
// Older apps might not implement this callback method at all.
}
}
}
}
if (mode != null) {
if (mode.getType() == ActionMode.TYPE_PRIMARY) {
cleanupPrimaryActionMode();
mPrimaryActionMode = mode;
} else if (mode.getType() == ActionMode.TYPE_FLOATING) {
if (mFloatingActionMode != null) {
mFloatingActionMode.finish();
}
mFloatingActionMode = mode;
}
} else {
//最终走了这里
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 {
mWindow.getCallback().onActionModeStarted(mode);
} catch (AbstractMethodError ame) {
// Older apps might not implement this callback method.
}
}
return mode;
}
上边核心方法就是
mode = mWindow.getCallback().onWindowStartingActionMode(wrappedCallback, type);
也就是,我们得找到这个callback,去activity里找下即可【这玩意有两种情况,这里就不分析了】
我们就简单分析下不带toolbar的,也就是callback的实现在activity里,原理应该都是一样的
api23以上的是floating模式,所以下边的会返回null
public ActionMode onWindowStartingActionMode(ActionMode.Callback callback, int type) {
try {
mActionModeTypeStarting = type;
return onWindowStartingActionMode(callback);
} finally {
mActionModeTypeStarting = ActionMode.TYPE_PRIMARY;
}
}
public ActionMode onWindowStartingActionMode(ActionMode.Callback callback) {
// Only Primary ActionModes are represented in the ActionBar.
if (mActionModeTypeStarting == ActionMode.TYPE_PRIMARY) {
initWindowDecorActionBar();
if (mActionBar != null) {
return mActionBar.startActionMode(callback);
}
}
return null;
}
看下创建过程
private ActionMode createFloatingActionMode(
View originatingView, ActionMode.Callback2 callback) {
mFloatingToolbar = new FloatingToolbar(mWindow);
final FloatingActionMode mode =
new FloatingActionMode(mContext, callback, originatingView, mFloatingToolbar);
过了个周末继续看
public final class FloatingActionMode extends ActionMode
在构造方法里,可以看到另外一个类package com.android.internal.widget
mFloatingToolbar = floatingToolbar
.setMenu(mMenu)
.setOnMenuItemClickListener(item -> mMenu.performItemAction(item, 0));
看下这个类的show方法,可以看到又用到了一个mPopup的东西
/**
* Shows this floating toolbar.
*/
public FloatingToolbar show() {
mContext.unregisterComponentCallbacks(mOrientationChangeHandler);
mContext.registerComponentCallbacks(mOrientationChangeHandler);
List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu);
if (!isCurrentlyShowing(menuItems) || mWidthChanged) {
mPopup.dismiss();
mPopup.layoutMenuItems(menuItems, mMenuItemClickListener, mSuggestedWidth);
mShowingMenuItems = getShowingMenuItemsReferences(menuItems);
}
if (!mPopup.isShowing()) {
mPopup.show(mContentRect);
} else if (!mPreviousContentRect.equals(mContentRect)) {
mPopup.updateCoordinates(mContentRect);
}
mWidthChanged = false;
mPreviousContentRect.set(mContentRect);
return this;
}
在layoutMenuItems方法里可以看到一个类FloatingToolbarOverflowPanel
好奇怪,里边用的是listview加载的数据,可listview可以横向显示吗?
简单分析下加载menu的过程
textview相关的Editor里可以看到
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
mode.setTitle(null);
mode.setSubtitle(null);
mode.setTitleOptionalHint(true);
populateMenuWithItems(menu);//这个是加载系统默认的,复制,粘贴,剪切等item
Callback customCallback = getCustomCallback();
if (customCallback != null) {
if (!customCallback.onCreateActionMode(mode, menu)) {//这里调用了我们自定义的回调处理menu
// The custom mode can choose to cancel the action mode, dismiss selection.
Selection.setSelection((Spannable) mTextView.getText(),
mTextView.getSelectionEnd());
return false;
}
}
if (mTextView.canProcessText()) {
mProcessTextIntentActionsHandler.onInitializeMenu(menu);//这里是加载支持文字处理的activity的
}
}
继续看,r
public void onInitializeMenu(Menu menu) {
final int size = mSupportedActivities.size();
loadSupportedActivities();
for (int i = 0; i < size; i++) {
final ResolveInfo resolveInfo = mSupportedActivities.get(i);
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_IF_ROOM);
}
}
看下如何获取支持的activity
private void loadSupportedActivities() {
mSupportedActivities.clear();
PackageManager packageManager = mTextView.getContext().getPackageManager();
List<ResolveInfo> unfiltered =
packageManager.queryIntentActivities(createProcessTextIntent(), 0);
for (ResolveInfo info : unfiltered) {
if (isSupportedActivity(info)) {
mSupportedActivities.add(info);
}
}
}
private boolean isSupportedActivity(ResolveInfo info) {
return mPackageName.equals(info.activityInfo.packageName)
|| info.activityInfo.exported
&& (info.activityInfo.permission == null
|| mContext.checkSelfPermission(info.activityInfo.permission)
== PackageManager.PERMISSION_GRANTED);
}
private Intent createProcessTextIntentForResolveInfo(ResolveInfo info) {
return createProcessTextIntent()
.putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, !mTextView.isTextEditable())
.setClassName(info.activityInfo.packageName, info.activityInfo.name);
}
//根据intent的action和type来查找info
private Intent createProcessTextIntent() {
return new Intent()
.setAction(Intent.ACTION_PROCESS_TEXT)
.setType("text/plain");
}
private CharSequence getLabel(ResolveInfo resolveInfo) {
return resolveInfo.loadLabel(mPackageManager);
}
这种图片带文字的,都是系统默认的软件,感觉是自定义的好像。
samsung平板,一个是备忘录,一个浏览器
image.png
image.png