Can a TextView be selectable AND contain links?

后端 未结 8 665
长情又很酷
长情又很酷 2020-12-08 21:47

I\'ve run into a problem with TextView. I can make it selectable using setTextIsSelectable(true), but when I enable links to be clicked via s

相关标签:
8条回答
  • 2020-12-08 22:15

    TL;DR: Simply use the LinkArrowKeyMovementMethod at the end of this answer for a perfect solution.

    There is an annoying bug if you ever tried to used the top-voted answers that extends LinkMovementMethod -- when you cancel a selection by clicking some text that's not selected, the whole selection flashes to be from the very beginning to the selection end, and then becomes nothing. This is because LinkMovementMethod cannot actually handle selection as good as ArrowKeyMovementMethod.

    Another way around could have been using TextView's own workaround if you have set android:autoLink to true, as in the following source from TextView:

            final boolean textIsSelectable = isTextSelectable();
            if (touchIsFinished && mLinksClickable && mAutoLinkMask != 0 && textIsSelectable) {
                // The LinkMovementMethod which should handle taps on links has not been installed
                // on non editable text that support text selection.
                // We reproduce its behavior here to open links for these.
                ClickableSpan[] links = ((Spannable) mText).getSpans(getSelectionStart(),
                    getSelectionEnd(), ClickableSpan.class);
    
                if (links.length > 0) {
                    links[0].onClick(this);
                    handled = true;
                }
            }
    

    But I personally don't want the auto link feature (I have my own link information), so building upon @Weidian Huang 's idea, I incorporated LinkMovementMethod's functionality into ArrowKeyMovementMethod and built a new movement method:

    /**
     * @see LinkMovementMethod
     * @see ArrowKeyMovementMethod
     */
    public class LinkArrowKeyMovementMethod extends ArrowKeyMovementMethod {
    
        private static final int CLICK = 1;
        private static final int UP = 2;
        private static final int DOWN = 3;
    
        private static Object FROM_BELOW = new NoCopySpan.Concrete();
    
        private static LinkArrowKeyMovementMethod sInstance;
    
        public static LinkArrowKeyMovementMethod getInstance() {
            if (sInstance == null) {
                sInstance = new LinkArrowKeyMovementMethod();
            }
            return sInstance;
        }
    
        @Override
        public void initialize(TextView widget, Spannable text) {
            super.initialize(widget, text);
    
            text.removeSpan(FROM_BELOW);
        }
    
        @Override
        public void onTakeFocus(TextView view, Spannable text, int dir) {
            super.onTakeFocus(view, text, dir);
    
            if ((dir & View.FOCUS_BACKWARD) != 0) {
                text.setSpan(FROM_BELOW, 0, 0, Spannable.SPAN_POINT_POINT);
            } else {
                text.removeSpan(FROM_BELOW);
            }
        }
    
        @Override
        protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode,
                                            int movementMetaState, KeyEvent event) {
            switch (keyCode) {
                case KeyEvent.KEYCODE_DPAD_CENTER:
                case KeyEvent.KEYCODE_ENTER:
                    if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
                        if (event.getAction() == KeyEvent.ACTION_DOWN &&
                                event.getRepeatCount() == 0 && action(CLICK, widget, buffer)) {
                            return true;
                        }
                    }
                    break;
            }
            return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event);
        }
    
        @Override
        protected boolean up(TextView widget, Spannable buffer) {
            if (action(UP, widget, buffer)) {
                return true;
            }
    
            return super.up(widget, buffer);
        }
    
        @Override
        protected boolean down(TextView widget, Spannable buffer) {
            if (action(DOWN, widget, buffer)) {
                return true;
            }
    
            return super.down(widget, buffer);
        }
    
        @Override
        protected boolean left(TextView widget, Spannable buffer) {
            if (action(UP, widget, buffer)) {
                return true;
            }
    
            return super.left(widget, buffer);
        }
    
        @Override
        protected boolean right(TextView widget, Spannable buffer) {
            if (action(DOWN, widget, buffer)) {
                return true;
            }
    
            return super.right(widget, buffer);
        }
    
        private boolean action(int what, TextView widget, Spannable buffer) {
            Layout layout = widget.getLayout();
    
            int padding = widget.getTotalPaddingTop() +
                    widget.getTotalPaddingBottom();
            int areaTop = widget.getScrollY();
            int areaBot = areaTop + widget.getHeight() - padding;
    
            int lineTop = layout.getLineForVertical(areaTop);
            int lineBot = layout.getLineForVertical(areaBot);
    
            int first = layout.getLineStart(lineTop);
            int last = layout.getLineEnd(lineBot);
    
            ClickableSpan[] candidates = buffer.getSpans(first, last, ClickableSpan.class);
    
            int a = Selection.getSelectionStart(buffer);
            int b = Selection.getSelectionEnd(buffer);
    
            int selStart = Math.min(a, b);
            int selEnd = Math.max(a, b);
    
            if (selStart < 0) {
                if (buffer.getSpanStart(FROM_BELOW) >= 0) {
                    selStart = selEnd = buffer.length();
                }
            }
    
            if (selStart > last)
                selStart = selEnd = Integer.MAX_VALUE;
            if (selEnd < first)
                selStart = selEnd = -1;
    
            switch (what) {
                case CLICK:
                    if (selStart == selEnd) {
                        return false;
                    }
    
                    ClickableSpan[] link = buffer.getSpans(selStart, selEnd, ClickableSpan.class);
    
                    if (link.length != 1)
                        return false;
    
                    link[0].onClick(widget);
                    break;
    
                case UP:
                    int bestStart, bestEnd;
    
                    bestStart = -1;
                    bestEnd = -1;
    
                    for (int i = 0; i < candidates.length; i++) {
                        int end = buffer.getSpanEnd(candidates[i]);
    
                        if (end < selEnd || selStart == selEnd) {
                            if (end > bestEnd) {
                                bestStart = buffer.getSpanStart(candidates[i]);
                                bestEnd = end;
                            }
                        }
                    }
    
                    if (bestStart >= 0) {
                        Selection.setSelection(buffer, bestEnd, bestStart);
                        return true;
                    }
    
                    break;
    
                case DOWN:
                    bestStart = Integer.MAX_VALUE;
                    bestEnd = Integer.MAX_VALUE;
    
                    for (int i = 0; i < candidates.length; i++) {
                        int start = buffer.getSpanStart(candidates[i]);
    
                        if (start > selStart || selStart == selEnd) {
                            if (start < bestStart) {
                                bestStart = start;
                                bestEnd = buffer.getSpanEnd(candidates[i]);
                            }
                        }
                    }
    
                    if (bestEnd < Integer.MAX_VALUE) {
                        Selection.setSelection(buffer, bestStart, bestEnd);
                        return true;
                    }
    
                    break;
            }
    
            return false;
        }
    
        @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[] 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;
                }
                // Removed
                //else {
                //    Selection.removeSelection(buffer);
                //}
            }
    
            return super.onTouchEvent(widget, buffer, event);
        }
    }
    

    To use it, simply call:

    textView.setTextIsSelectable(true);
    textView.setMovementMethod(LinkArrowKeyMovementMethod.getInstance());
    

    And this worked perfectly for me.

    0 讨论(0)
  • 2020-12-08 22:21

    I figured it out. You need to subclass LinkMovementMethod and add support for text selection. It's really unfortunate that it doesn't support it natively. I just overrode the relevant methods using the equivalent ones from the source code for ArrowKeyMovementMethod. I guess that's one benefit of Android being open source!

    public class CustomMovementMethod extends LinkMovementMethod {
        @Override
        public boolean canSelectArbitrarily () {
            return true;
        }
    
        @Override
        public void initialize(TextView widget, Spannable text) {
            Selection.setSelection(text, text.length());
        }
    
        @Override
        public void onTakeFocus(TextView view, Spannable text, int dir) {
           if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) {
               if (view.getLayout() == null) {
                   // This shouldn't be null, but do something sensible if it is.
                   Selection.setSelection(text, text.length());
               }
           } else {
               Selection.setSelection(text, text.length());
           }
        }
    }
    

    To use it, just instantiate it directly, like so:

    textView.setMovementMethod(new CustomMovementMethod());
    
    0 讨论(0)
提交回复
热议问题