解决TextView 设置ClickableSpan之后,点击和滑动冲突以及空白区域处理

TextView 设置ClickableSpan之后,需要设置setMovementMethod(LinkMovementMethod.getInstance()),而LinkMovementMethod 继续ScrollingMovementMethod ,这样对于长文本有如下两个问题:
原文地址

GitHub测试Demo
1:ClickableSpan的点击和TextView的长文本滑动冲突
2:点击TextView空白区域,总选中最后一个文本

基于此实现了一个简单的英文阅读器点击链接查看
主要实现
1:实现页面中单词点击选中
2:实现分页功能
3:实现简单的翻页动画

1分析原有的LinkMovementMethod 点击和滑动逻辑

1)根据点击位置查找当前行的ClickableSpan,若点击空白区域,则为当前行最后一个ClickableSpan;若ClickableSpan集合不为空,则拦截down和up事件。

@Override
public boolean onTouchEvent(TextView widget, Spannable buffer,
                            MotionEvent event) {
    int action = event.getAction();

    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,若点击空白区域,则为当前行最后一个ClickableSpan
        ClickableSpan[] links = buffer.getSpans(off, off, ClickableSpan.class);
        //ClickableSpan集合不为空,则拦截down和up事件
        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);
        }
    }

    return super.onTouchEvent(widget, buffer, event);
}

2)调用父类onTouchEvent透传事件

@Override
public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
    return Touch.onTouchEvent(widget, buffer, event);
}

3)处理拖拽事件。
ACTION_DOWN设置拖拽状态,ACTION_UP清除拖拽状态,ACTION_MOVE如果有拖拽状态,满足条件则移动内容。

/**
 * Handles touch events for dragging.  You may want to do other actions
 * like moving the cursor on touch as well.
 */
public static boolean onTouchEvent(TextView widget, Spannable buffer,
                                   MotionEvent event) {
    DragState[] ds;

    switch (event.getActionMasked()) {
    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:
        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;
        }

    case MotionEvent.ACTION_MOVE:
        ds = buffer.getSpans(0, buffer.length(), DragState.class);
        //如果有拖拽状态,满足条件则移动内容
        if (ds.length > 0) {
            if (ds[0].mFarEnough == false) {
                int slop = ViewConfiguration.get(widget.getContext()).getScaledTouchSlop();

                if (Math.abs(event.getX() - ds[0].mX) >= slop ||
                    Math.abs(event.getY() - ds[0].mY) >= slop) {
                    ds[0].mFarEnough = true;
                }
            }

            if (ds[0].mFarEnough) {
                ds[0].mUsed = true;
                boolean cap = (event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0
                        || MetaKeyKeyListener.getMetaState(buffer,
                                MetaKeyKeyListener.META_SHIFT_ON) == 1
                        || MetaKeyKeyListener.getMetaState(buffer,
                                MetaKeyKeyListener.META_SELECTING) != 0;

                float dx;
                float dy;
                if (cap) {
                    // if we're selecting, we want the scroll to go in
                    // the direction of the drag
                    dx = event.getX() - ds[0].mX;
                    dy = event.getY() - ds[0].mY;
                } else {
                    dx = ds[0].mX - event.getX();
                    dy = ds[0].mY - event.getY();
                }
                ds[0].mX = event.getX();
                ds[0].mY = event.getY();

                int nx = widget.getScrollX() + (int) dx;
                int ny = widget.getScrollY() + (int) dy;

                int padding = widget.getTotalPaddingTop() + widget.getTotalPaddingBottom();
                Layout layout = widget.getLayout();

                ny = Math.min(ny, layout.getHeight() - (widget.getHeight() - padding));
                ny = Math.max(ny, 0);

                int oldX = widget.getScrollX();
                int oldY = widget.getScrollY();

                scrollTo(widget, layout, nx, ny);

                // If we actually scrolled, then cancel the up action.
                if (oldX != widget.getScrollX() || oldY != widget.getScrollY()) {
                    widget.cancelLongPress();
                }

                return true;
            }
        }
    }

    return false;
}

2根据业务需要滑动和点击事件,做如下处理

1)如果点击当前行的区域为空白区域,返回空集合。
2)ACTION_DOWN设置拖拽状态,ACTION_UP清除拖拽状态,设置之后再拦截

@Override
public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
    if (mTouchSlop == 0) {
        mTouchSlop = ViewConfiguration.get(widget.getContext()).getScaledTouchSlop();
    }
    ClickableSpan[] links;
    int action = event.getAction();

    switch (action) {
        case MotionEvent.ACTION_DOWN:
            mIsMoved = false;
            //设置拖拽状态
            super.onTouchEvent(widget, buffer, event);
            mDownY = event.getY();
            mDownX = event.getX();
            links = findClickableSpans(widget, buffer, event);
            if (links.length != 0) {
                Selection.setSelection(buffer,
                        buffer.getSpanStart(links[0]),
                        buffer.getSpanEnd(links[0]));
                return true;
            } else {
                Selection.removeSelection(buffer);
            }
            break;
        case MotionEvent.ACTION_MOVE:
            if (Math.abs(event.getX() - mDownX) > mTouchSlop ||
                    Math.abs(event.getY() - mDownY) > mTouchSlop) {
                mIsMoved = true;
            }
            break;
        case MotionEvent.ACTION_UP:
            //清除拖拽状态
            super.onTouchEvent(widget, buffer, event);
            links = findClickableSpans(widget, buffer, event);
            //如果滑动,则不触发点击事件
            if (!mIsMoved && links.length != 0) {
                links[0].onClick(widget);
                return true;
            } else {
                if (!mIsMoved && mClickWordListener != null) {
                    mClickWordListener.onClickEmpty();
                }
                Selection.removeSelection(buffer);
            }
            break;
        default:
    }
    return super.onTouchEvent(widget, buffer, event);
}

/**
 * 如果点击区域为空白区域,返回空集合
 */
private ClickableSpan[] findClickableSpans(TextView widget, Spannable buffer, MotionEvent event) {
    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);
    int lineMax = (int) layout.getLineMax(line);
    //点击空白区域返回空集合
    if (x > lineMax) {
        return new ClickableSpan[]{};
    }
    return buffer.getSpans(off, off, ClickableSpan.class);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章