效果图

android 文件 替换 安卓系统文本替换_Text

在点击粘贴之后弹出了一个toast提示,既然可以做到弹出toast,那想干其他事情还不简单。比如,将用户粘贴的文本替换成其他文本,这才是研究实现这个功能的原因。

先说一下实现方式,需要继承EditText/AppCompatEditText,再重写onTextContextMenuItem方法,先直接上代码。

public class CustomEditText extends AppCompatEditText {
    public CustomeEditText(Context context) {
        super(context);
    }

    public CustomEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomEditText(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTextContextMenuItem(int id) {
        if (id == android.R.id.paste) {
            paste();
            return true;
        }
        return super.onTextContextMenuItem(id);
    }

    private void paste() {
        //TODO 在这里实现想要实现的功能
        Toast.makeText(getContext(),"paste",Toast.LENGTH_SHORT).show();
    }
}

再稍微说一下思路吧

先说明一下,下面很多代码是根据猜代码去找的,而不是逐行阅读找到相应的代码,所以如果不能接受这种方式就没必要继续看(我也知道这不是一种好的方式,但看不懂源码只能靠猜代码)。

观察弹出粘贴菜单的过程,发现是通过长按输入框弹出来的,所以找了一下TextView内部setLongClickListener的调用,最后没有找到。所以搜索onTouchEvent方法,看到onTouch下面有这样一段代码。

if (mEditor != null) {
    mEditor.onTouchEvent(event);

    if (mEditor.mSelectionModifierCursorController != null
                    && mEditor.mSelectionModifierCursorController.isDragAcceleratorActive()) {
        return true;
    }
}

所以就查看Editor是怎么实现这个功能的,在Editor的onTouch方法里面调用了这个方法:updateFloatingToolbarVisibility。这个方法没有注释,但从名称上看就已经知道了是更新长按EditText的弹窗的显示状态。

void onTouchEvent(MotionEvent event) {
    final boolean filterOutEvent = shouldFilterOutTouchEvent(event);
    mLastButtonState = event.getButtonState();
    if (filterOutEvent) {
        if (event.getActionMasked() == MotionEvent.ACTION_UP) {
            mDiscardNextActionUp = true;
        }
        return;
    }
    updateTapState(event);
    updateFloatingToolbarVisibility(event);
    ...
}
updateFloatingToolbarVisibility的源码
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();
        }
    }
}

可以看到在ACTION_MOVE的时候隐藏Toolbar,在ACTION_UP和ACTION_CANCEL的时候显示Toolbar。

下面猜代码开始了。

没有去思考mTextActionMode是什么时候初始化的,只是在源码中找到这样一段代码。

/**
 * Start an Insertion action mode.
 */
void startInsertionActionMode() {
    if (mInsertionActionModeRunnable != null) {
        mTextView.removeCallbacks(mInsertionActionModeRunnable);
    }
    if (extractedTextModeWillBeStarted()) {
        return;
    }
    stopTextActionMode();

    ActionMode.Callback actionModeCallback =
            new TextActionModeCallback(TextActionMode.INSERTION);
    mTextActionMode = mTextView.startActionMode(
            actionModeCallback, ActionMode.TYPE_FLOATING);
    if (mTextActionMode != null && getInsertionController() != null) {
        getInsertionController().show();
    }
}

这里对mTextActionMode做初始化操作,不过View.startActionMode听都没听过,所以不知道这个方法是干嘛的,所以看了一下actionModeCallback的实现。

发现是一个继承自ActionMode.Callback2的类

private class TextActionModeCallback extends ActionMode.Callback2 {
    ...
}

这里面有一个方法,onCreateActionMode,看一下文档

/**
 * Called when action mode is first created. The menu supplied will be used to
 * generate action buttons for the action mode.
 *
 * @param mode ActionMode being created
 * @param menu Menu used to populate action buttons
 * @return true if the action mode should be created, false if entering this
 *              mode should be aborted.
 */
public boolean onCreateActionMode(ActionMode mode, Menu menu);

大概的意思是首次创建action mode的时候调用,提供的menu将用于为action mode生成操作按钮。

TextActionModeCallback对onCreateActionMode的实现

@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
    mAssistClickHandlers.clear();
    mode.setTitle(null);
    mode.setSubtitle(null);
    mode.setTitleOptionalHint(true);
    populateMenuWithItems(menu);
    ...
}

这里的populateMenuWithItems就是生成menu的方法

private void populateMenuWithItems(Menu menu) {
    ...
    if (mTextView.canPaste()) {
        menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
                com.android.internal.R.string.paste)
                        .setAlphabeticShortcut('v')
                        .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
    }
    ...
}

可以看到这里通过TextView判断能否粘贴,如果可以粘贴就往menu添加一个item,这里的itemId使用的是TextView.ID_PASTE。

说实话,由于对ActionMode完全不了解,到了这里之后就没思路了。所以这个时候只能去查看TextActionModeCallback的最顶层的接口:android.view.ActiomMode.Callback,看到里面有一个叫onActionItemClicked的方法。

/**
 * Called to report a user click on an action button.
 *
 * @param mode The current ActionMode
 * @param item The item that was clicked
 * @return true if this callback handled the event, false if the standard MenuItem
 *          invocation should continue.
 */
public boolean onActionItemClicked(ActionMode mode, MenuItem item);

再看一下TextActionModeCallback对该方法的实现

@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
    getSelectionActionModeHelper()
            .onSelectionAction(item.getItemId(), item.getTitle().toString());
    if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
        return true;
    }
    Callback customCallback = getCustomCallback();
    if (customCallback != null && customCallback.onActionItemClicked(mode, item)) {
        return true;
    }
    if (item.getGroupId() == TextView.ID_ASSIST && onAssistMenuItemClicked(item)) {
        return true;
    }
    return mTextView.onTextContextMenuItem(item.getItemId());
}

前面的代码我都没看,所以不清楚前面的代码的作用,我只看到了最后一行,调用了TextView的onTextContextMenuItem方法

再看一下这个方法的具体实现

/**
 * Called when a context menu option for the text view is selected.  Currently
 * this will be one of {@link android.R.id#selectAll}, {@link android.R.id#cut},
 * {@link android.R.id#copy}, {@link android.R.id#paste} or {@link android.R.id#shareText}.
 *
 * @return true if the context menu item action was performed.
 */
public boolean onTextContextMenuItem(int id) {
    int min = 0;
    int max = mText.length();

    if (isFocused()) {
        final int selStart = getSelectionStart();
        final int selEnd = getSelectionEnd();

        min = Math.max(0, Math.min(selStart, selEnd));
        max = Math.max(0, Math.max(selStart, selEnd));
    }
    switch (id) {
    case ID_SELECT_ALL:
        final boolean hadSelection = hasSelection();
        selectAllText();
        if (mEditor != null && hadSelection) {
            mEditor.invalidateActionModeAsync();
        }
        return true;

    case ID_UNDO:
        if (mEditor != null) {
            mEditor.undo();
        }
        return true;  // Returns true even if nothing was undone.

    case ID_REDO:
        if (mEditor != null) {
            mEditor.redo();
        }
        return true;  // Returns true even if nothing was undone.

    case ID_PASTE:
        paste(min, max, true /* withFormatting */);
        return true;
    ...
    }
    return false;
    
}

可以看到,里面对IN_PASTE这个id进行判断,和上面的populateMenuWidthItems的itemId是一致的。看到paste这个方法,发现是一个private的,所以没办法重写这个方法。

/**
 * Paste clipboard content between min and max positions.
 */
private void paste(int min, int max, boolean withFormatting) {
    ClipboardManager clipboard = getClipboardManagerForUser();
    ClipData clip = clipboard.getPrimaryClip();
    if (clip != null) {
        boolean didFirst = false;
        for (int i = 0; i < clip.getItemCount(); i++) {
            final CharSequence paste;
            if (withFormatting) {
                paste = clip.getItemAt(i).coerceToStyledText(getContext());
            } else {
                // Get an item as text and remove all spans by toString().
                final CharSequence text = clip.getItemAt(i).coerceToText(getContext());
                paste = (text instanceof Spanned) ? text.toString() : text;
            }
            if (paste != null) {
                if (!didFirst) {
                    Selection.setSelection(mSpannable, max);
                    ((Editable) mText).replace(min, max, paste);
                    didFirst = true;
                } else {
                    ((Editable) mText).insert(getSelectionEnd(), "\n");
                    ((Editable) mText).insert(getSelectionEnd(), paste);
                }
            }
        }
        sLastCutCopyOrTextChangedTime = 0;
    }
}

既然没办法重写paste方法,并且onTextContextMenuItem是public且不是final,那就重写onTextContextMenuItem方法,

但这个时候又发现一个问题,TextView.IN_PASTE静态常量是包私有,不过好在这个常量是引用R文件的一个id,所以问题不大。

static final int ID_PASTE = android.R.id.paste;

所以这个时候重写之后就可以判断itemId是否为android.R.id.paste,如果是,就return true,所以就有了开头的那段代码。

如果有哪里写得不好,望指正,共同进步。