Android: ClickableSpan in clickable TextView

后端 未结 7 1717
借酒劲吻你
借酒劲吻你 2020-11-27 02:53

I have a textview that can contain clickable links. When one of this links is clicked, I want to start an activity. This works fine, but it should also be possible to click

相关标签:
7条回答
  • 2020-11-27 03:05

    Found a workaround that is quite straight forward. Define ClickableSpan on all the text areas that are not part of the links and handle the click on them as if the text view was clicked:

    TextView tv = (TextView)findViewById(R.id.textview01);      
    Spannable span = Spannable.Factory.getInstance().newSpannable("test link span");   
    span.setSpan(new ClickableSpan() {  
        @Override
        public void onClick(View v) {  
            Log.d("main", "link clicked");
            Toast.makeText(Main.this, "link clicked", Toast.LENGTH_SHORT).show(); 
        } }, 5, 9, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    
    // All the rest will have the same spannable.
    ClickableSpan cs = new ClickableSpan() {  
        @Override
        public void onClick(View v) {  
            Log.d("main", "textview clicked");
            Toast.makeText(Main.this, "textview clicked", Toast.LENGTH_SHORT).show(); 
        } };
    
    // set the "test " spannable.
    span.setSpan(cs, 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    
    // set the " span" spannable
    span.setSpan(cs, 6, span.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    
    tv.setText(span);
    
    tv.setMovementMethod(LinkMovementMethod.getInstance());
    

    Hope this helps (I know this thread is old, but in case anyone sees it now...).

    0 讨论(0)
  • Solved something very similar in a very nice way. I wanted to have text that has a link which is clickable!! and i wanted to be able to press the text Where there is no link and have a on click listener in it. I took the LinkMovementMethod from grepcode and changed it a little Copy and past this class and copy the bottom and it will work :

    import android.text.Layout;
    import android.text.NoCopySpan;
    import android.text.Selection;
    import android.text.Spannable;
    import android.text.method.MovementMethod;
    import android.text.method.ScrollingMovementMethod;
    import android.text.style.ClickableSpan;
    import android.view.KeyEvent;
    import android.view.MotionEvent;
    import android.view.View;
    import android.widget.TextView;
    
    public class
            CustomLinkMovementMethod
            extends ScrollingMovementMethod
    {
        private static final int CLICK = 1;
        private static final int UP = 2;
        private static final int DOWN = 3;
    
    public abstract interface TextClickedListener {
        public abstract void onTextClicked();
    }
    TextClickedListener listener = null;
    public void setOnTextClickListener(TextClickedListener listen){
        listener = listen;
    }
    @Override
    public boolean onKeyDown(TextView widget, Spannable buffer,
                             int keyCode, KeyEvent event) {
        switch (keyCode) {
            case KeyEvent.KEYCODE_DPAD_CENTER:
            case KeyEvent.KEYCODE_ENTER:
                if (event.getRepeatCount() == 0) {
                    if (action(CLICK, widget, buffer)) {
                        return true;
                    }
                }
        }
    
        return super.onKeyDown(widget, buffer, keyCode, 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) {
        boolean handled = false;
    
        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;
    }
    
    public boolean onKeyUp(TextView widget, Spannable buffer,
                           int keyCode, KeyEvent event) {
        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[] link = buffer.getSpans(off, off, ClickableSpan.class);
    
            if (link.length != 0) {
                if (action == MotionEvent.ACTION_UP) {
                    link[0].onClick(widget);
                } else if (action == MotionEvent.ACTION_DOWN) {
                    Selection.setSelection(buffer,
                            buffer.getSpanStart(link[0]),
                            buffer.getSpanEnd(link[0]));
                }
    
                return true;
            } else {
                Selection.removeSelection(buffer);
    
                if (action == MotionEvent.ACTION_UP) {
                    if(listener != null)
                        listener.onTextClicked();
                }
            }
        }
    
        return super.onTouchEvent(widget, buffer, event);
    }
    
    
    
    
    
    public void initialize(TextView widget, Spannable text) {
        Selection.removeSelection(text);
        text.removeSpan(FROM_BELOW);
    }
    
    public void onTakeFocus(TextView view, Spannable text, int dir) {
        Selection.removeSelection(text);
    
        if ((dir & View.FOCUS_BACKWARD) != 0) {
            text.setSpan(FROM_BELOW, 0, 0, Spannable.SPAN_POINT_POINT);
        } else {
            text.removeSpan(FROM_BELOW);
        }
    }
    
    public static MovementMethod getInstance() {
        if (sInstance == null)
            sInstance = new CustomLinkMovementMethod();
    
        return sInstance;
    }
    
    private static CustomLinkMovementMethod sInstance;
    private static Object FROM_BELOW = new NoCopySpan.Concrete();
    

    }

    Then in your code where the text view is add:

     CustomLinkMovementMethod link = (CustomLinkMovementMethod)CustomLinkMovementMethod.getInstance();
            link.setOnTextClickListener(new CustomLinkMovementMethod.TextClickedListener() {
                @Override
                public void onTextClicked() {
                    Toast.makeText(UserProfileActivity.this, "text Pressed", Toast.LENGTH_LONG).show();
    
                }
            });
            YOUR_TEXTVIEW.setMovementMethod(link);
    
    0 讨论(0)
  • 2020-11-27 03:09

    The code is work for me and that is from source code of LinkMovementMethod

    tv.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    TextView tv = (TextView) v;
                    if (event.action == MotionEvent.ACTION_UP) {
                        int x = (int) event.getX();
                        int y = (int) event.getY();
    
                        Layout layout = tv.getLayout();
                        int line = layout.getLineForVertical(y);
                        int off = layout.getOffsetForHorizontal(line, x);
    
                        ClickableSpan[] link = contentSpan.getSpans(off, off, ClickableSpan.class);
    
                        if (link.length != 0) {
                            link[0].onClick(tv);
                        } else {
                           //do other click
                        }
                    }
                    return true;
                }
            });
    
    0 讨论(0)
  • 2020-11-27 03:10

    I think that this involves subclassing TextView and changing its behavior, unfortunately. Have you thought about trying to put a background behind the TextView and attaching an onClickListener to it?

    0 讨论(0)
  • 2020-11-27 03:16

    This is a quite easy solution.. This worked for me

    textView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            ClassroomLog.log(TAG, "Textview Click listener ");
            if (textView.getSelectionStart() == -1 && textView.getSelectionEnd() == -1) {
                // do your code here this will only call if its not a hyperlink
            }
        }
    });
    
    0 讨论(0)
  • 2020-11-27 03:17

    Matthew suggested subclassing TextView and with that hint a came up with a rather ugly workaround. But it works:

    I've created a "ClickPreventableTextView" which I use when I have clickablespans in a TextView that should be clickable as a whole.

    In its onTouchEvent method this class calls the onTouchEvent method of MovementMethod before calling onTouchEvent on its base TextView class. So it is guaranted, that the Listener of the clickablespan will be invoked first. And I can prevent invoking the OnClickListener for the whole TextView

    /**
     * TextView that allows to insert clickablespans while whole textview is still clickable<br>
     * If a click an a clickablespan occurs, click handler of whole textview will <b>not</b> be invoked
     * In your span onclick handler you first have to check whether {@link ignoreSpannableClick} returns true, if so just return from click handler
     * otherwise call {@link preventNextClick} and handle the click event
     * @author Lukas
     *
     */
    public class ClickPreventableTextView extends TextView implements OnClickListener {
    private boolean preventClick;
    private OnClickListener clickListener;
    private boolean ignoreSpannableClick;
    
    public ClickPreventableTextView(Context context) {
        super(context);
    }
    
    public ClickPreventableTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    
    public ClickPreventableTextView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }
    
    public boolean onTouchEvent(MotionEvent event) {
        if (getMovementMethod() != null)
            getMovementMethod().onTouchEvent(this, (Spannable)getText(), event);
        this.ignoreSpannableClick = true;
        boolean ret = super.onTouchEvent(event);
        this.ignoreSpannableClick = false;
        return ret;
    }
    
    /**
     * Returns true if click event for a clickable span should be ignored
     * @return true if click event should be ignored
     */
    public boolean ignoreSpannableClick() {
        return ignoreSpannableClick;
    }
    
    /**
     * Call after handling click event for clickable span
     */
    public void preventNextClick() {
        preventClick = true;
    }
    
    @Override
    public void setOnClickListener(OnClickListener listener) {
        this.clickListener = listener;
        super.setOnClickListener(this);
    }
    
    @Override
    public void onClick(View v) {
        if (preventClick) {
            preventClick = false;
        } else if (clickListener != null)
            clickListener.onClick(v);
    }
    }
    

    The listener for the clickable span now looks like that

        span.setSpan(new ClickableSpan() {  
            @Override
            public void onClick(View v) {  
                Log.d("main", "link clicked");
                if (widget instanceof ClickPreventableTextView) {
                    if (((ClickPreventableTextView)widget).ignoreSpannableClick())
                        return;
                    ((ClickPreventableTextView)widget).preventNextClick();
                }
    
                Toast.makeText(Main.this, "link clicked", Toast.LENGTH_SHORT).show(); 
            } }, 5, 9, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    

    For me the main disadvantage is, that now getMovementMethod().onTouchEvent will be called twice (TextView calls that method in it's onTouchEvent method). I don't know if this has any side effects, atm it works as expected.

    0 讨论(0)
提交回复
热议问题