Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d055939c authored by Andrei Stingaceanu's avatar Andrei Stingaceanu Committed by Android (Google) Code Review
Browse files

Merge "TextView - fix ClickableSpans always triggering on touch up"

parents c20cfa69 3df24c31
Loading
Loading
Loading
Loading
+30 −33
Original line number Diff line number Diff line
@@ -29,6 +29,9 @@ import android.widget.TextView;
/**
 * A movement method that traverses links in the text buffer and scrolls if necessary.
 * Supports clicking on links with DPad Center or Enter.
 *
 * <p>Note: Starting from Android 8.0 (API level 25) this class no longer handles the touch
 * clicks.
 */
public class LinkMovementMethod extends ScrollingMovementMethod {
    private static final int CLICK = 1;
@@ -98,14 +101,14 @@ public class LinkMovementMethod extends ScrollingMovementMethod {

        int padding = widget.getTotalPaddingTop() +
                      widget.getTotalPaddingBottom();
        int areatop = widget.getScrollY();
        int areabot = areatop + widget.getHeight() - padding;
        int areaTop = widget.getScrollY();
        int areaBot = areaTop + widget.getHeight() - padding;

        int linetop = layout.getLineForVertical(areatop);
        int linebot = layout.getLineForVertical(areabot);
        int lineTop = layout.getLineForVertical(areaTop);
        int lineBot = layout.getLineForVertical(areaBot);

        int first = layout.getLineStart(linetop);
        int last = layout.getLineEnd(linebot);
        int first = layout.getLineStart(lineTop);
        int last = layout.getLineEnd(lineBot);

        ClickableSpan[] candidates = buffer.getSpans(first, last, ClickableSpan.class);

@@ -141,46 +144,46 @@ public class LinkMovementMethod extends ScrollingMovementMethod {
            break;

        case UP:
            int beststart, bestend;
            int bestStart, bestEnd;

            beststart = -1;
            bestend = -1;
            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 (end > bestEnd) {
                        bestStart = buffer.getSpanStart(candidates[i]);
                        bestEnd = end;
                    }
                }
            }

            if (beststart >= 0) {
                Selection.setSelection(buffer, bestend, beststart);
            if (bestStart >= 0) {
                Selection.setSelection(buffer, bestEnd, bestStart);
                return true;
            }

            break;

        case DOWN:
            beststart = Integer.MAX_VALUE;
            bestend = Integer.MAX_VALUE;
            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 (start < bestStart) {
                        bestStart = start;
                        bestEnd = buffer.getSpanEnd(candidates[i]);
                    }
                }
            }

            if (bestend < Integer.MAX_VALUE) {
                Selection.setSelection(buffer, beststart, bestend);
            if (bestEnd < Integer.MAX_VALUE) {
                Selection.setSelection(buffer, bestStart, bestEnd);
                return true;
            }

@@ -195,8 +198,7 @@ public class LinkMovementMethod extends ScrollingMovementMethod {
                                MotionEvent event) {
        int action = event.getAction();

        if (action == MotionEvent.ACTION_UP ||
            action == MotionEvent.ACTION_DOWN) {
        if (action == MotionEvent.ACTION_DOWN) {
            int x = (int) event.getX();
            int y = (int) event.getY();

@@ -210,17 +212,12 @@ public class LinkMovementMethod extends ScrollingMovementMethod {
            int line = layout.getLineForVertical(y);
            int off = layout.getOffsetForHorizontal(line, x);

            ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);
            ClickableSpan[] links = 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) {
            if (links.length != 0) {
                Selection.setSelection(buffer,
                                           buffer.getSpanStart(link[0]),
                                           buffer.getSpanEnd(link[0]));
                }

                        buffer.getSpanStart(links[0]),
                        buffer.getSpanEnd(links[0]));
                return true;
            } else {
                Selection.removeSelection(buffer);
+22 −13
Original line number Diff line number Diff line
@@ -114,6 +114,7 @@ import android.view.ActionMode;
import android.view.Choreographer;
import android.view.ContextMenu;
import android.view.DragEvent;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
import android.view.KeyCharacterMap;
@@ -650,6 +651,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
     */
    private Editor mEditor;

    private final GestureDetector mClickableSpanOnClickGestureDetector;

    private static final int DEVICE_PROVISIONED_UNKNOWN = 0;
    private static final int DEVICE_PROVISIONED_NO = 1;
    private static final int DEVICE_PROVISIONED_YES = 2;
@@ -1488,6 +1491,24 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
        if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
            setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
        }

        mClickableSpanOnClickGestureDetector = new GestureDetector(context,
                new GestureDetector.SimpleOnGestureListener() {
                    @Override
                    public boolean onSingleTapConfirmed(MotionEvent e) {
                        if (mLinksClickable && (mMovement != null) &&
                                (mMovement instanceof LinkMovementMethod
                                || (mAutoLinkMask != 0 && isTextSelectable()))) {
                            ClickableSpan[] links = ((Spannable) mText).getSpans(
                                    getSelectionStart(), getSelectionEnd(), ClickableSpan.class);
                            if (links.length > 0) {
                                links[0].onClick(TextView.this);
                                return true;
                            }
                        }
                        return false;
                    }
                });
    }

    private int[] parseDimensionArray(TypedArray dimens) {
@@ -8515,21 +8536,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
            if (mMovement != null) {
                handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);
            }
            handled |= mClickableSpanOnClickGestureDetector.onTouchEvent(event);

            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;
                }
            }

            if (touchIsFinished && (isTextEditable() || textIsSelectable)) {
                // Show the IME, except when selecting in read-only text.
                final InputMethodManager imm = InputMethodManager.peekInstance();
+65 −33
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import android.text.Spannable;
 * TextViewTest tests {@link TextView}.
 */
public class TextViewTest extends ActivityInstrumentationTestCase2<TextViewActivity> {
    private TextView mTextView;

    public TextViewTest() {
        super(TextViewActivity.class);
@@ -37,16 +38,22 @@ public class TextViewTest extends ActivityInstrumentationTestCase2<TextViewActiv
    @SmallTest
    @Presubmit
    public void testArray() throws Exception {
        TextView tv = new TextView(getActivity());
        getActivity().runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mTextView = new TextView(getActivity());
            }
        });
        getInstrumentation().waitForIdleSync();

        char[] c = new char[] { 'H', 'e', 'l', 'l', 'o', ' ',
                                'W', 'o', 'r', 'l', 'd', '!' };

        tv.setText(c, 1, 4);
        CharSequence oldText = tv.getText();
        mTextView.setText(c, 1, 4);
        CharSequence oldText = mTextView.getText();

        tv.setText(c, 4, 5);
        CharSequence newText = tv.getText();
        mTextView.setText(c, 4, 5);
        CharSequence newText = mTextView.getText();

        assertTrue(newText == oldText);

@@ -67,12 +74,18 @@ public class TextViewTest extends ActivityInstrumentationTestCase2<TextViewActiv

    @SmallTest
    public void testProcessTextActivityResultNonEditable() {
        final TextView tv = new TextView(getActivity());
        getActivity().runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mTextView = new TextView(getActivity());
            }
        });
        getInstrumentation().waitForIdleSync();
        CharSequence originalText = "This is some text.";
        tv.setText(originalText, TextView.BufferType.SPANNABLE);
        assertEquals(originalText, tv.getText().toString());
        tv.setTextIsSelectable(true);
        Selection.setSelection((Spannable) tv.getText(), 0, tv.getText().length());
        mTextView.setText(originalText, TextView.BufferType.SPANNABLE);
        assertEquals(originalText, mTextView.getText().toString());
        mTextView.setTextIsSelectable(true);
        Selection.setSelection((Spannable) mTextView.getText(), 0, mTextView.getText().length());

        // We need to run this in the UI thread, as it will create a Toast.
        getActivity().runOnUiThread(new Runnable() {
@@ -81,60 +94,79 @@ public class TextViewTest extends ActivityInstrumentationTestCase2<TextViewActiv
                CharSequence newText = "Text is replaced.";
                Intent data = new Intent();
                data.putExtra(Intent.EXTRA_PROCESS_TEXT, newText);
                tv.onActivityResult(TextView.PROCESS_TEXT_REQUEST_CODE, Activity.RESULT_OK, data);
                mTextView.onActivityResult(TextView.PROCESS_TEXT_REQUEST_CODE, Activity.RESULT_OK, data);
            }
        });
        getInstrumentation().waitForIdleSync();

        // This is a TextView, which can't be modified. Hence no change should have been made.
        assertEquals(originalText, tv.getText().toString());
        assertEquals(originalText, mTextView.getText().toString());
    }

    @SmallTest
    public void testProcessTextActivityResultEditable() {
        EditText tv = new EditText(getActivity());
        getActivity().runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mTextView = new EditText(getActivity());
            }
        });
        getInstrumentation().waitForIdleSync();
        CharSequence originalText = "This is some text.";
        tv.setText(originalText, TextView.BufferType.SPANNABLE);
        assertEquals(originalText, tv.getText().toString());
        tv.setTextIsSelectable(true);
        Selection.setSelection(tv.getText(), 0, tv.getText().length());
        mTextView.setText(originalText, TextView.BufferType.SPANNABLE);
        assertEquals(originalText, mTextView.getText().toString());
        mTextView.setTextIsSelectable(true);
        Selection.setSelection(((EditText) mTextView).getText(), 0, mTextView.getText().length());

        CharSequence newText = "Text is replaced.";
        Intent data = new Intent();
        data.putExtra(Intent.EXTRA_PROCESS_TEXT, newText);
        tv.onActivityResult(TextView.PROCESS_TEXT_REQUEST_CODE, Activity.RESULT_OK, data);
        mTextView.onActivityResult(TextView.PROCESS_TEXT_REQUEST_CODE, Activity.RESULT_OK, data);

        assertEquals(newText, tv.getText().toString());
        assertEquals(newText, mTextView.getText().toString());
    }

    @SmallTest
    public void testProcessTextActivityResultCancel() {
        EditText tv = new EditText(getActivity());
        getActivity().runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mTextView = new EditText(getActivity());
            }
        });
        getInstrumentation().waitForIdleSync();
        CharSequence originalText = "This is some text.";
        tv.setText(originalText, TextView.BufferType.SPANNABLE);
        assertEquals(originalText, tv.getText().toString());
        tv.setTextIsSelectable(true);
        Selection.setSelection(tv.getText(), 0, tv.getText().length());
        mTextView.setText(originalText, TextView.BufferType.SPANNABLE);
        assertEquals(originalText, mTextView.getText().toString());
        mTextView.setTextIsSelectable(true);
        Selection.setSelection(((EditText) mTextView).getText(), 0, mTextView.getText().length());

        CharSequence newText = "Text is replaced.";
        Intent data = new Intent();
        data.putExtra(Intent.EXTRA_PROCESS_TEXT, newText);
        tv.onActivityResult(TextView.PROCESS_TEXT_REQUEST_CODE, Activity.RESULT_CANCELED, data);
        mTextView.onActivityResult(TextView.PROCESS_TEXT_REQUEST_CODE, Activity.RESULT_CANCELED,
                data);

        assertEquals(originalText, tv.getText().toString());
        assertEquals(originalText, mTextView.getText().toString());
    }

    @SmallTest
    public void testProcessTextActivityNoData() {
        EditText tv = new EditText(getActivity());
        getActivity().runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mTextView = new EditText(getActivity());
            }
        });
        getInstrumentation().waitForIdleSync();
        CharSequence originalText = "This is some text.";
        tv.setText(originalText, TextView.BufferType.SPANNABLE);
        assertEquals(originalText, tv.getText().toString());
        tv.setTextIsSelectable(true);
        Selection.setSelection(tv.getText(), 0, tv.getText().length());
        mTextView.setText(originalText, TextView.BufferType.SPANNABLE);
        assertEquals(originalText, mTextView.getText().toString());
        mTextView.setTextIsSelectable(true);
        Selection.setSelection(((EditText) mTextView).getText(), 0, mTextView.getText().length());

        tv.onActivityResult(TextView.PROCESS_TEXT_REQUEST_CODE, Activity.RESULT_OK, null);
        mTextView.onActivityResult(TextView.PROCESS_TEXT_REQUEST_CODE, Activity.RESULT_OK, null);

        assertEquals(originalText, tv.getText().toString());
        assertEquals(originalText, mTextView.getText().toString());
    }
}