TextView.SetLinkMovementMethod后拦截所有点击事件的原因以及解决方法

在需要给TextView的某句话添加点击事件的时候,我们一般会使用ClickableSpan来进行富文本编辑。与此同时我们还需要配合

 textView.setMovementMethod(LinkMovementMethod.getInstance());

方法才能使点击处理生效。但与此同时还会有一个问题:如果我们给父布局添加一个点击事件,需要在点击非链接的时候触发(例如RectclerView自定义的onItemClickListener有一部分就是给itemView添加onClick事件),但是设置了setMovementMethod方法后整个TextView就无法触发父布局的点击事件了,无论点击的地方是否有链接。
网络上大都是自定义TextView的onTouchEvent,把LinkMovementMethod方法的onTouch事件放到TextView中去处理,这么做侵入性太高。
其实TextView的setLinkMethod方法拦截所有点击事件的原因有两个。

  • LinkMovementMethod方法本身就会拦截所有点击事件.
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
            int x = (int) event.getX();
            int y = (int) event.getY();

            x -= widget.getTotalPaddingLeft();
            y -= widget.getTotalPaddingTop();

            x += widget.getScrollX();
            y += widget.getScrollY();

            Layout layout = widget.getLayout();
            int line = layout.getLineForVertical(y);
            int off = layout.getOffsetForHorizontal(line, x);

            ClickableSpan[] links = buffer.getSpans(off, off, ClickableSpan.class);

            if (links.length != 0) {
                if (action == MotionEvent.ACTION_UP) {
                    links[0].onClick(widget);
                } else if (action == MotionEvent.ACTION_DOWN) {
                    Selection.setSelection(buffer,
                            buffer.getSpanStart(links[0]),
                            buffer.getSpanEnd(links[0]));
                }
                return true;
            } else {
                Selection.removeSelection(buffer);
            }
        }

可以看到其实LinkMovementMethod方法本身判断逻辑是点击的位置是否有ClickSpan,如果有就返回true,如果没有就交给父布局,问题就在父布局这里,LinkMovementMethod 的父布局 ScrollingMovementMethod是控制内容滑动的MovementMethod,在它的onTouch方法中默认会给所有的点击事件返回true

case MotionEvent.ACTION_DOWN:
            ds = buffer.getSpans(0, buffer.length(), DragState.class);

            for (int i = 0; i < ds.length; i++) {
                buffer.removeSpan(ds[i]);
            }

            buffer.setSpan(new DragState(event.getX(), event.getY(),
                            widget.getScrollX(), widget.getScrollY()),
                    0, 0, Spannable.SPAN_MARK_MARK);
            return true;
case MotionEvent.ACTION_UP://简直前后呼应,down默认给加一个DragState,up就判断是否有DragState
            ds = buffer.getSpans(0, buffer.length(), DragState.class);

            for (int i = 0; i < ds.length; i++) {
                buffer.removeSpan(ds[i]);
            }

            if (ds.length > 0 && ds[0].mUsed) {
                return true;
            } else {
                return false;
            }

因此,如果我们想要TextView只拦截处理ClickableSpan某些字段的事件,我们就需要重写LinkMovementMethod,把它的super.onTouchEvent直接改成false即可,这里建议做一个boolean设置,如果内容过长需要滑动,这个super还是需要加上的。但与此同时父布局的点击事件就无法再TextView上做响应了(某种意义上来说这两种情况也不可能同时发生)。

  • setMovementMethod方法
    setMovementMethod源码如下
public final void setMovementMethod(MovementMethod movement) {
        if (mMovement != movement) {
            mMovement = movement;
            if (movement != null && !(mText instanceof Spannable)) {
                setText(mText);
            }
            fixFocusableAndClickableSettings();
            if (mEditor != null) mEditor.prepareCursorControllers();
        }
    }
    private void fixFocusableAndClickableSettings() {
        if (mMovement != null || (mEditor != null && mEditor.mKeyListener != null)) {
            setFocusable(FOCUSABLE);
            setClickable(true);
            setLongClickable(true);
        } else {
            setFocusable(FOCUSABLE_AUTO);
            setClickable(false);
            setLongClickable(false);
        }
    }

重点在fixFocusableAndClickableSettings方法上,它会给TextView把所有焦点事件都设置上。而TextView的onTouch事件通过源码可以看到。

   @Override
    public boolean onTouchEvent(MotionEvent event) {
        final int action = event.getActionMasked();
       //省略无关代码
        final boolean superResult = super.onTouchEvent(event);
        if (mEditor != null && mEditor.mDiscardNextActionUp && action == MotionEvent.ACTION_UP) {
          //省略无关代码
            return superResult;
        }
        final boolean touchIsFinished = (action == MotionEvent.ACTION_UP)
                && (mEditor == null || !mEditor.mIgnoreActionUpEvent) && isFocused();
        if ((mMovement != null || onCheckIsTextEditor()) && isEnabled()
                && mText instanceof Spannable && mLayout != null) {
            boolean handled = false;
            if (mMovement != null) {
                handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);
            }
            final boolean textIsSelectable = isTextSelectable();
            //开了自动关联才会触发,省略
            if (touchIsFinished && mLinksClickable && mAutoLinkMask != 0 && textIsSelectable)、、
            //文字可以被修改才会触发,省略
        if (touchIsFinished && (isTextEditable() || textIsSelectable))、、
            if (handled) {
                return true;
            }
        }
        return superResult;
    }

可以看到,如果movementMothed的onTouch方法返回false之后,实际上TextView返回的是super的onTouch,即View的onTouch,
而View.onTouch

final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {//全是break,没有return ,所以省略.
                case MotionEvent.ACTION_UP:
                 .
                 .
                 .
                 }
            return true;
        }

可以看到,只要clickable=true,onTouch都会拦截所有事件,关联上边的setLinkMovementMethod方法的源码,对clickAble,longClickAble的设置会导致onTouchEvent方法返回true。
解决方法归总:
1.重写LinkMovementMethod方法,根据需要控制super.ontouch()的返回值。
2.在setMovementMethod之前保存一下textView之前的xxxAble属性,设置完之后对这些属性进行还原。

你可能感兴趣的:(api)