美文网首页
android系统文本选择器添加

android系统文本选择器添加

作者: 有点健忘 | 来源:发表于2018-12-25 10:03 被阅读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

    相关文章

      网友评论

          本文标题:android系统文本选择器添加

          本文链接:https://www.haomeiwen.com/subject/ppuwcqtx.html