效果图
在点击粘贴之后弹出了一个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,所以就有了开头的那段代码。
如果有哪里写得不好,望指正,共同进步。