解決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);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章