android系统文本选择器添加

2018-12-25  本文已影响22人  有点健忘

看这里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. 监听触摸事件,如果在1秒内没有移动,那我就按照选择文字的事件处理,否则按照移动处理。
  2. 这里就要模拟文字选中了。找到手指按下的文字位置,以及最后移动的位置,完事给文字添加背景色
  3. 模仿系统的弹个选项出来

根据坐标点的位置,可以通过如下方法获取文字的索引

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
上一篇下一篇

猜你喜欢

热点阅读