LongClick原理、上下文菜单原理、EditText长按弹窗原理、WebView长按弹窗自定义、修复WebView全选重复bug

长按事件

Android中按住一个View不松手,会触发长按事件。

使用的场景

1、EditText的长按快捷操作。
2、自定义上下文菜单等。
3、WebView中长按快捷菜单。

个人遇到的问题

在使用WebView过程中,发现长按弹出的菜单,“全选”功能项点击后并没有消失,而EditText点击“全选”后,新弹出的菜单不会再显示全选。最终确定为WebView的bug。

长按原理分析

解决问题得先研究原理,况且WebView的问题需要更改android源码。首先分析长按的原理。
一、首先长按肯定是触摸事件,先看View.onTouchEvent

    public boolean onTouchEvent(MotionEvent event) {
    	if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                .......
            	case MotionEvent.ACTION_DOWN:
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    mHasPerformedLongPress = false;
                    if (!clickable) {
                        // 这里去检查是否属于长按事件
                        checkForLongClick(
                                ViewConfiguration.getLongPressTimeout(),
                                x,y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                        break;
                    }
                    ......
             }
             return true;
        }
        return false;
    }
    
    private void checkForLongClick(long delay, float x, float y, int classification) {
        if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
            mHasPerformedLongPress = false;
            // 通过postDelayed,然后会执行CheckForLongPress
            if (mPendingCheckForLongPress == null) {
                mPendingCheckForLongPress = new CheckForLongPress();
            }
            ......
            postDelayed(mPendingCheckForLongPress, delay);
        }
    }
	
    private final class CheckForLongPress implements Runnable {
        ......
        @Override
        public void run() {
            if ((mOriginalPressedState == isPressed()) && (mParent != null)
                    && mOriginalWindowAttachCount == mWindowAttachCount) {
                recordGestureClassification(mClassification);
                if (performLongClick(mX, mY)) {
                    mHasPerformedLongPress = true;
                }
            }
        }
        ......
    }
    public boolean performLongClick(float x, float y) {
        mLongClickX = x;
        mLongClickY = y;
        final boolean handled = performLongClick();
        mLongClickX = Float.NaN;
        mLongClickY = Float.NaN;
        return handled;
    }
    public boolean performLongClick(float x, float y) {
        mLongClickX = x;
        mLongClickY = y;
        final boolean handled = performLongClick();
        mLongClickX = Float.NaN;
        mLongClickY = Float.NaN;
        return handled;
    }
    public boolean performLongClick() {
        return performLongClickInternal(mLongClickX, mLongClickY);
    }
    private boolean performLongClickInternal(float x, float y) {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);

        boolean handled = false;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLongClickListener != null) {
            handled = li.mOnLongClickListener.onLongClick(View.this);
        }
        if (!handled) {
            final boolean isAnchored = !Float.isNaN(x) && !Float.isNaN(y);
            handled = isAnchored ? showContextMenu(x, y) : showContextMenu();
        }
        if ((mViewFlags & TOOLTIP) == TOOLTIP) {
            if (!handled) {
                handled = showLongClickTooltip((int) x, (int) y);
            }
        }
        if (handled) {
            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
        }
        return handled;
    }

这里最终可以看到,执行了performLongClickInternal方法。这个方法内部会按序执行三个方法:
1、li.mOnLongClickListener.onLongClick(View.this);这个mOnLongClickListener就是setOnLongClickListener传入的监听者,监听者消费了这个时间,那么performLongClickInternal到这里也就结束了。
2、如果长按没有被消费,会继续执行isAnchored ? showContextMenu(x, y) : showContextMenu();官方对这个的解释是上下文菜单。
3、showLongClickTooltip,这个是一个tool窗口的提示,比较简单,有兴趣的同学可以自行了解。
二、我们看下showContextMenu它最终哪里调用的:

    public boolean showContextMenu(float x, float y) {
        return getParent().showContextMenuForChild(this, x, y);
    }

然后getParent()会一直到DecorView.java

    @Override
    public boolean showContextMenuForChild(View originalView, float x, float y) {
        return showContextMenuForChildInternal(originalView, x, y);
    }
   
    private boolean showContextMenuForChildInternal(View originalView,
            float x, float y) {
        // Only allow one context menu at a time.
        if (mWindow.mContextMenuHelper != null) {
            mWindow.mContextMenuHelper.dismiss();
            mWindow.mContextMenuHelper = null;
        }

        // Reuse the context menu builder.
        final PhoneWindowMenuCallback callback = mWindow.mContextMenuCallback;
        if (mWindow.mContextMenu == null) {
            mWindow.mContextMenu = new ContextMenuBuilder(getContext());
            mWindow.mContextMenu.setCallback(callback);
        } else {
            mWindow.mContextMenu.clearAll();
        }

        final MenuHelper helper;
        final boolean isPopup = !Float.isNaN(x) && !Float.isNaN(y);
        if (isPopup) {
            helper = mWindow.mContextMenu.showPopup(getContext(), originalView, x, y);
        } else {
            helper = mWindow.mContextMenu.showDialog(originalView, originalView.getWindowToken());
        }

        if (helper != null) {
            // If it's a dialog, the callback needs to handle showing
            // sub-menus. Either way, the callback is required for propagating
            // selection to Context.onContextMenuItemSelected().
            callback.setShowDialogForSubmenu(!isPopup);
            helper.setPresenterCallback(callback);
        }

        mWindow.mContextMenuHelper = helper;
        return helper != null;
    }

ContextMenuBuilder就是传说中的上下文菜单。showPopup和showDialog会显示上下文菜单,同时返回是否消费了此事件。我们继续看上下文菜单是怎么被设置和显示的。看下ContextMenuBuilder.java

public class ContextMenuBuilder extends MenuBuilder implements ContextMenu {
    ......
    public MenuDialogHelper showDialog(View originalView, IBinder token) {
        if (originalView != null) {
            // 让相关视图及其填充上下文侦听器填充上下文菜单
            originalView.createContextMenu(this);
        }

        if (getVisibleItems().size() > 0) {
            EventLog.writeEvent(50001, 1);
            
            MenuDialogHelper helper = new MenuDialogHelper(this); 
            helper.show(token);
            
            return helper;
        }
        
        return null;
    }
    
    public MenuPopupHelper showPopup(Context context, View originalView, float x, float y) {
        // 这个跟showDialog基本一致,只是使用了MenuPopupHelper
    }
}

填充上下文菜单通过originalView.createContextMenu(this),展示是通过MenuDialogHelper,originalView也就是View.showContextMenu()传入的this,这里也就回到了View.createContextMenu()方法,看下View.java类

    public void createContextMenu(ContextMenu menu) {
        ContextMenuInfo menuInfo = getContextMenuInfo();

        // Sets the current menu info so all items added to menu will have
        // my extra info set.
        ((MenuBuilder)menu).setCurrentMenuInfo(menuInfo);

        onCreateContextMenu(menu);
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnCreateContextMenuListener != null) {
            li.mOnCreateContextMenuListener.onCreateContextMenu(menu, this, menuInfo);
        }

        // Clear the extra information so subsequent items that aren't mine don't
        // have my extra info.
        ((MenuBuilder)menu).setCurrentMenuInfo(null);

        if (mParent != null) {
            mParent.createContextMenu(menu);
        }
    }

这里可以看到,调用了li.mOnCreateContextMenuListener.onCreateContextMenu(menu, this, menuInfo);mOnCreateContextMenuListener是setOnCreateContextMenuListener设置的

    public void setOnCreateContextMenuListener(OnCreateContextMenuListener l) {
        if (!isLongClickable()) {
            setLongClickable(true);
        }
        getListenerInfo().mOnCreateContextMenuListener = l;
    }

setOnCreateContextMenuListener的调用者在Activity、Dialog、Fragment都有,我们看下Activity的调用的地方,代码Activity.java

    public void registerForContextMenu(View view) {
        view.setOnCreateContextMenuListener(this);
    }

	// li.mOnCreateContextMenuListener.onCreateContextMenu重写这个方法对menu添加菜单项
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
    }

很明显registerForContextMenu注册上下文对象到固定View,同时this也就是Activity需要重写onCreateContextMenu方法填充菜单项。我们继续看下getVisibileItems原理:

		if (getVisibleItems().size() > 0) {
            EventLog.writeEvent(50001, 1);
            
            MenuDialogHelper helper = new MenuDialogHelper(this); 
            helper.show(token);
            
            return helper;
        }

先看下getVisibleItems()是MenuBuilder.java的方法

	// addInternal会往ArrayList<MenuItemImpl>填充数据
	public ArrayList<MenuItemImpl> getVisibleItems() {
	}
    public MenuItem add(CharSequence title) {
        return addInternal(0, 0, 0, title);
    }

那么哪里调用的add呢?
回头看ContextMenuBuilder.showDialog()

    public MenuDialogHelper showDialog(View originalView, IBinder token) {
        if (originalView != null) {
            originalView.createContextMenu(this);
        }
    }

这个this就是Activity.createContextMenu传入的ContextMenu。如果Activity重写了createContextMenu并对ContextMenuBuilder执行了add操作,那么getVisibleItems.size就大于0,就会通过MenuDialogHelper显示上下文菜单。
三、注册并填充了菜单项也通过MenuDialogHelper显示菜单了,那么点击监听怎么处理的呢?
首先MenuDialogHelper需要对Dialog做click监听,代码在MenuDialogHelper.java

    public void onClick(DialogInterface dialog, int which) {
        mMenu.performItemAction((MenuItemImpl) mPresenter.getAdapter().getItem(which), 0);
    }

调用了MenuBuilder.java

    public boolean performItemAction(MenuItem item, int flags) {
        return performItemAction(item, null, flags);
    }

    public boolean performItemAction(MenuItem item, MenuPresenter preferredPresenter, int flags) {
        MenuItemImpl itemImpl = (MenuItemImpl) item;
        
        if (itemImpl == null || !itemImpl.isEnabled()) {
            return false;
        }

        boolean invoked = itemImpl.invoke();
        ......
        return invoked;
    }

itemImpl.invoke()调用了MenuItemImpl.java

    public boolean invoke() {
        if (mClickListener != null &&
            mClickListener.onMenuItemClick(this)) {
            return true;
        }

        if (mMenu.dispatchMenuItemSelected(mMenu, this)) {
            return true;
        }
		......
        return false;
    }

如果mClickListener通过MenuItem.setOnMenuItemClickListener单独定制了listener,会优先回调;如果没有,会回调mMenu.dispatchMenuItemSelected,代码在MenuBuilder.java

    boolean dispatchMenuItemSelected(MenuBuilder menu, MenuItem item) {
        return mCallback != null && mCallback.onMenuItemSelected(menu, item);
    }
    
    public void setCallback(Callback cb) {
        mCallback = cb;
    }

setCallback调用的地方设置了Menu的监听回调。设置的地方在PhoneWindow.java

    protected boolean initializePanelMenu(final PanelFeatureState st) {
    	......
        final MenuBuilder menu = new MenuBuilder(context);
        menu.setCallback(this);
        st.setMenu(menu);
        return true;
    }

    public final boolean preparePanel(PanelFeatureState st, KeyEvent event) {
    	......
    	if (!initializePanelMenu(st) || (st.menu == null)) {
        	return false;
        }
        ......
    }

    void doInvalidatePanelMenu(int featureId) {
    	......
    	preparePanel(st, null);
    	......
    }

    private final Runnable mInvalidatePanelMenuRunnable = new Runnable() {
        @Override public void run() {
            for (int i = 0; i <= FEATURE_MAX; i++) {
                if ((mInvalidatePanelMenuFeatures & 1 << i) != 0) {
                    doInvalidatePanelMenu(i);
                }
            }
            mInvalidatePanelMenuPosted = false;
            mInvalidatePanelMenuFeatures = 0;
        }
    };

    private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        }
        ......
   }

    @Override
    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        }
        ......
    }

PhoneWindow关联Activity的contentView的时候会把this作为MenuBuilder.Callback传入。那么PhoneWindow需要重写onMenuItemSelected

    public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            final PanelFeatureState panel = findMenuPanel(menu.getRootMenu());
            if (panel != null) {
                return cb.onMenuItemSelected(panel.featureId, item);
            }
        }
        return false;
    }

    public final Callback getCallback() {
        return mCallback;
    }

    public void setCallback(Callback callback) {
        mCallback = callback;
    }

以Activity调用setCallback()为例,看下Activity调用的地方

    final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);

        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        ......
    }

Window.setCallback(this),Activity需要实现Window.Callback,里边同样有一个onMenuItemSelected,看下Activity.java实现onMenuItemSelected

    public boolean onMenuItemSelected(int featureId, @NonNull MenuItem item) {
        CharSequence titleCondensed = item.getTitleCondensed();

        switch (featureId) {
            case Window.FEATURE_OPTIONS_PANEL:
                // Put event logging here so it gets called even if subclass
                // doesn't call through to superclass's implmeentation of each
                // of these methods below
                if(titleCondensed != null) {
                    EventLog.writeEvent(50000, 0, titleCondensed.toString());
                }
                if (onOptionsItemSelected(item)) {
                    return true;
                }
                if (mFragments.dispatchOptionsItemSelected(item)) {
                    return true;
                }
                if (item.getItemId() == android.R.id.home && mActionBar != null &&
                        (mActionBar.getDisplayOptions() & ActionBar.DISPLAY_HOME_AS_UP) != 0) {
                    if (mParent == null) {
                        return onNavigateUp();
                    } else {
                        return mParent.onNavigateUpFromChild(this);
                    }
                }
                return false;

            case Window.FEATURE_CONTEXT_MENU:
                if(titleCondensed != null) {
                    EventLog.writeEvent(50000, 1, titleCondensed.toString());
                }
                if (onContextItemSelected(item)) {
                    return true;
                }
                return mFragments.dispatchContextItemSelected(item);

            default:
                return false;
        }
    }

case Window.FEATURE_CONTEXT_MENU:条件时会执行onContextItemSelected(item)。所以,上下文菜单的点击监听需要重写Activity.onContextItemSelected。

四、到这里,整个以上下文菜单为例的长按原理就梳理完了。使用上下文菜单需要在Activity重写三个方法:
上下文菜单的使用

EditText的长按快捷操作

当我们长按EditText时,会弹出剪切,复制,全选等菜单操作。那么这个的原理是啥呢?首先体验了下EditText的长按,发现它跟上下文菜单并不一样,当EditText长按的时候只是选择了文本,并没有弹出菜单;当松开长按时才会弹出快捷操作菜单。因此,EditText长按选择了文本,松开长按弹出了快捷菜单
1、EditText继承TextView,TextView重写了performLongClick

    @Override
    public boolean performLongClick() {
        ......
        if (mEditor != null) {
            handled |= mEditor.performLongClick(handled);
            mEditor.mIsBeingLongClicked = false;
        }
        ......
   }

继续看下Editor.performLongClick

    public boolean performLongClick(boolean handled) {
    	// 这个方法对TextView的内容进行了选择
    	selectCurrentWordAndStartDrag();
    }

2、松开弹出的快捷菜单原理,松开的操作肯定是在onTouchEvent.downUP了。我们看下TextView.onTouchEvent

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        final int action = event.getActionMasked();
        if (mEditor != null) {
            mEditor.onTouchEvent(event);
        }
        ......
    }

这里可以看到所有的touch都转交给了Editor处理。继续看下Editor.onTouchEvent,因为是ACTION_UP触发的,直接看关键代码

        public void onTouchEvent(MotionEvent event) {
            // This is done even when the View does not have focus, so that long presses can start
            // selection and tap can move cursor from this tap position.
            final float eventX = event.getX();
            final float eventY = event.getY();
            final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
            switch (event.getActionMasked()) {
    			......
                case MotionEvent.ACTION_UP:
                    if (!isDragAcceleratorActive()) {
                        break;
                    }
                    updateSelection(event);

                    // No longer dragging to select text, let the parent intercept events.
                    mTextView.getParent().requestDisallowInterceptTouchEvent(false);

                    // No longer the first dragging motion, reset.
                    resetDragAcceleratorState();

                    if (mTextView.hasSelection()) {
                        // Drag selection should not be adjusted by the text classifier.
                        startSelectionActionModeAsync(mHaventMovedEnoughToStartDrag);
                    }
                    break;
            }
        }

	    void startSelectionActionModeAsync(boolean adjustSelection) {
     	   getSelectionActionModeHelper().startSelectionActionModeAsync(adjustSelection);
    	}

继续看SelectionActionModeHelper.java

    public void startSelectionActionModeAsync(boolean adjustSelection) {
        ......
        startSelectionActionMode(null);
        ......
    }

    private void startSelectionActionMode(@Nullable SelectionResult result) {
        startActionMode(Editor.TextActionMode.SELECTION, result);
    }

    private void startActionMode(
            @Editor.TextActionMode int actionMode, @Nullable SelectionResult result) {
        ......
        if (mEditor.startActionModeInternal(actionMode)) {
            ......
        }
        ......
    }

又回到了Editor.startActionModeInternal

    boolean startActionModeInternal(@TextActionMode int actionMode) {
        if (extractedTextModeWillBeStarted()) {
            return false;
        }
        if (mTextActionMode != null) {
            // Text action mode is already started
            invalidateActionMode();
            return false;
        }

        ActionMode.Callback actionModeCallback = new TextActionModeCallback(actionMode);
        mTextActionMode = mTextView.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING);
		......
    }
	
    private class TextActionModeCallback extends ActionMode.Callback2 {
        @Override
        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
            mAssistClickHandlers.clear();

            mode.setTitle(null);
            mode.setSubtitle(null);
            mode.setTitleOptionalHint(true);
            populateMenuWithItems(menu);
			......
            return true;
        }

        private void populateMenuWithItems(Menu menu) {
            if (mTextView.canCut()) {
                menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
                        com.android.internal.R.string.cut)
                                .setAlphabeticShortcut('x')
                                .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
            }

            if (mTextView.canCopy()) {
                menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
                        com.android.internal.R.string.copy)
                                .setAlphabeticShortcut('c')
                                .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
            }
			......
            updateSelectAllItem(menu);
            updateReplaceItem(menu);
            updateAssistMenuItems(menu);
        }

        @Override
        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
            updateSelectAllItem(menu);
            updateReplaceItem(menu);
            updateAssistMenuItems(menu);

            Callback customCallback = getCustomCallback();
            if (customCallback != null) {
                return customCallback.onPrepareActionMode(mode, menu);
            }
            return true;
        }

        @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());
        }
    }

onCreateActionMode创建的时候调用,onPrepareActionMode更新调用,onActionItemClicked点击调用。Editor自定义了ActionMode.Callback2去添加了actionMode同时增加了特殊的ItemClick处理。自定义了callback2,最终显示调用了TextView.startActionMode(),看下TextView.java的这个方法

    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);
        }
    }

parent最终回到DecorView.startActionModeForChild(),看下DecorView.java

    @Override
    public ActionMode startActionModeForChild(View originalView,
            ActionMode.Callback callback) {
        return startActionModeForChild(originalView, callback, ActionMode.TYPE_PRIMARY);
    }

    @Override
    public ActionMode startActionModeForChild(
            View child, ActionMode.Callback callback, int type) {
        return startActionMode(child, callback, type);
    }

    private ActionMode startActionMode(
            View originatingView, ActionMode.Callback callback, int type) {
        ActionMode.Callback2 wrappedCallback = new ActionModeCallback2Wrapper(callback);
        ActionMode mode = null;
        ......
        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;
            }
        }
        ......
        return mode;
    }

    private void setHandledActionMode(ActionMode mode) {
        if (mode.getType() == ActionMode.TYPE_PRIMARY) {
            setHandledPrimaryActionMode(mode);
        } else if (mode.getType() == ActionMode.TYPE_FLOATING) {
            setHandledFloatingActionMode(mode);
        }
    }

    private void setHandledFloatingActionMode(ActionMode mode) {
        mFloatingActionMode = mode;
        mFloatingActionMode.invalidate();  // Will show the floating toolbar if necessary.
        mFloatingActionModeOriginatingView.getViewTreeObserver()
            .addOnPreDrawListener(mFloatingToolbarPreDrawListener);
    }

mFloatingActionMode.invalidate();最终显示了EditText的文本操作弹窗。
3、源码分析完了,他们上下文菜单的逻辑不一样。但是都是基于长按做的。这里引入了ActionMode的相关类和方法,官方的描述是:表示用户界面的上下文模式。ActionMode可用于提供替代交互模式,并替换部分常规UI,直到完成。好的操作模式包括文本选择和上下文操作。不过目前ActionMode似乎也只在TextView中用到了。

解决WebView的全选bug

1、项目中要解决长按webView文本后,已经全选文案弹窗中还有“全选”选项的bug。
2、WebView长按的逻辑跟EditText还不一样,EditText是长按松开才会弹窗。但是WebView是长按就会弹出,类似上下文菜单。
3、问题分析:经过上边的源码分析,长按弹出无非ContextMenu或者ActionMode。ContextMenu需要重写onCreateContextMenu和onContextItemSelected;ActionMode需要调用startActionMode。我们看下WebVeiw.java的源码,看下有没有重写这几个方法。
源码路径frameworks/base/core/java/android/webkit/WebView.java
发现源码中没有重写ContextMenu相关方法,因为Webview.apk源码看不到,所以是否调用了ActionMode也是不知道的。
4、长按还设计两种方法,一种是重写performLongClick,另一种重写onTouch自行处理,看下WebVeiw.java

    @SuppressWarnings("deprecation")  // for super() call into deprecated base class constructor.
    protected WebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes,
            Map<String, Object> javaScriptInterfaces, boolean privateBrowsing) {
        super(context, attrs, defStyleAttr, defStyleRes);
        ensureProviderCreated();
        ......
    }

    private void ensureProviderCreated() {
        checkThread();
        if (mProvider == null) {
            // As this can get called during the base class constructor chain, pass the minimum
            // number of dependencies here; the rest are deferred to init().
            mProvider = getFactory().createWebView(this, new PrivateAccess());
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return mProvider.getViewDelegate().onTouchEvent(event);
    }

    @Override
    public boolean performLongClick() {
        return mProvider.getViewDelegate().performLongClick();
    }

    @SystemApi
    public class PrivateAccess {
        public boolean super_performLongClick() {
            return WebView.super.performLongClick();
        }
        ......
    }

WebView重写了onTouch和performLongClick,然后通过Provider传入PrivateAccess。Provider的源码看不到,所以不知道如何处理的了长按,但是PrivateAccess是暴露出来供第三方自定义的,重写performLongClick还是可以拦截长按操作。但是并没有暴露onTouch,因此具体如何弹出的文本操作菜单,不得而知。
5、WebView没有实现上下文菜单,也没有自定义performLongClick,那么唯一的可能就是onTouch内部封装调用了ActionMode,于是在DecorView.startActionMode()加了日志,测试WebView长按调用了此方法。
6、解决问题,分析完之后有两种解决办法:
1)重写performLongClick,仿照EditText完全重写文本操作弹窗。我不想这么做,太麻烦了,还容易出错。
2)重写startActionMode,继承ActionMode.Callback实现扩展处理。我选择了这个。
7、直接上代码,改动WebView.java

    @Override
    public boolean onTouchEvent(MotionEvent event) {
// Fix Webview repeat show the select all action mode
		// 这里需要在onTouch的时候对菜单全选进行重置
        hasSelectedAll = false;
// END
        return mProvider.getViewDelegate().onTouchEvent(event);
    }

// Fix Webview repeat show the select all action mode
    @Override
    public ActionMode startActionModeForChild(View originalView,
                                              ActionMode.Callback callback) {
        return startActionModeForChild(originalView, callback, ActionMode.TYPE_PRIMARY);
    }

    @Override
    public ActionMode startActionModeForChild(
            View child, ActionMode.Callback callback, int type) {
        return startActionModeInternal(child, callback, type);
    }

    @Override
    public ActionMode startActionMode(ActionMode.Callback callback) {
        return startActionMode(callback, ActionMode.TYPE_PRIMARY);
    }

    @Override
    public ActionMode startActionMode(ActionMode.Callback callback, int type) {
        return startActionModeInternal(this, callback, type);
    }

    private ActionMode actionMode = null;
    private boolean hasSelectedAll = false;
    private int selectAllItemId = 0;

    private ActionMode startActionModeInternal(View originatingView, ActionMode.Callback callback, int type) {
        if (actionMode != null) {
            actionMode.finish();
        }
        actionMode = getParent().startActionModeForChild(originatingView, new ActionModeCallbackWebView(callback), type);
        return actionMode;
    }

	// 装饰原有ActionMode.Callback2,加入自己的处理
    private class ActionModeCallbackWebView extends ActionMode.Callback2 {
        private final ActionMode.Callback mWrapped;

        public ActionModeCallbackWebView(ActionMode.Callback wrapped) {
            mWrapped = wrapped;
        }

        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
            boolean created = mWrapped.onCreateActionMode(mode, menu);
            updateSelectAllItem(menu);
            return created;
        }

        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
            boolean prepared = mWrapped.onPrepareActionMode(mode, menu);
            updateSelectAllItem(menu);
            return prepared;
        }

        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
            boolean clicked = mWrapped.onActionItemClicked(mode, item);
            // 因为不知道selectAll的ItemId是啥,所以选择通过View的文案识别是否选择了全选
            if (getResources().getString(com.android.internal.R.string.selectAll).equals(item.getTitle())) {
                hasSelectedAll = true;
                selectAllItemId = item.getItemId();
                // 因为WebView点击全选后没有关闭菜单栏,这里调用hide隐藏
                mode.hide(1000);
            }
            return clicked;
        }

        public void onDestroyActionMode(ActionMode mode) {
            mWrapped.onDestroyActionMode(mode);
        }

        @Override
        public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
            if (mWrapped instanceof ActionMode.Callback2) {
                ((ActionMode.Callback2) mWrapped).onGetContentRect(mode, view, outRect);
            } else {
                super.onGetContentRect(mode, view, outRect);
            }
        }

		// 如果已经展示过全选,把全选从menu中移除
        private void updateSelectAllItem(Menu menu) {
            if (hasSelectedAll && selectAllItemId != 0) {
                menu.removeItem(selectAllItemId);
            }
        }
    }
// END

总结

1、重写performLongClick可以自定义长按处理。
2、重写onCreateContextMenu和onContextItemSelected自定义上下文菜单使用。
3、调用startActionMode使用文本编辑菜单栏。
4、重写onTouch可以做任何屏幕点击处理,不建议,太危险。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值