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

Commit d11f6e64 authored by Gilles Debunne's avatar Gilles Debunne
Browse files

Double and one and a half tap removed from TextView.

Text selection is now triggered by a long press. This behavior should be shared
across applications and is already implemented in the browser.

New UI guidelines say that double tap should be used for zooming only.

Change-Id: I9fae692cf3f6e565350f666fa3d5a86884439349
parent f35c96bb
Loading
Loading
Loading
Loading
+35 −231
Original line number Diff line number Diff line
@@ -17,13 +17,11 @@
package android.text.method;

import android.text.Layout;
import android.text.NoCopySpan;
import android.text.Selection;
import android.text.Spannable;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.TextView;
import android.widget.TextView.CursorController;

@@ -38,13 +36,18 @@ public class ArrowKeyMovementMethod implements MovementMethod {
     */
    protected CursorController mCursorController;

    private boolean isCap(Spannable buffer) {
        return ((MetaKeyKeyListener.getMetaState(buffer, KeyEvent.META_SHIFT_ON) == 1) ||
                (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0));
    }

    private boolean isAlt(Spannable buffer) {
        return MetaKeyKeyListener.getMetaState(buffer, KeyEvent.META_ALT_ON) == 1;
    }

    private boolean up(TextView widget, Spannable buffer) {
        boolean cap = (MetaKeyKeyListener.getMetaState(buffer,
                        KeyEvent.META_SHIFT_ON) == 1) ||
                      (MetaKeyKeyListener.getMetaState(buffer,
                        MetaKeyKeyListener.META_SELECTING) != 0);
        boolean alt = MetaKeyKeyListener.getMetaState(buffer,
                        KeyEvent.META_ALT_ON) == 1;
        boolean cap = isCap(buffer);
        boolean alt = isAlt(buffer);
        Layout layout = widget.getLayout();

        if (cap) {
@@ -65,12 +68,8 @@ public class ArrowKeyMovementMethod implements MovementMethod {
    }

    private boolean down(TextView widget, Spannable buffer) {
        boolean cap = (MetaKeyKeyListener.getMetaState(buffer,
                        KeyEvent.META_SHIFT_ON) == 1) ||
                      (MetaKeyKeyListener.getMetaState(buffer,
                        MetaKeyKeyListener.META_SELECTING) != 0);
        boolean alt = MetaKeyKeyListener.getMetaState(buffer,
                        KeyEvent.META_ALT_ON) == 1;
        boolean cap = isCap(buffer);
        boolean alt = isAlt(buffer);
        Layout layout = widget.getLayout();

        if (cap) {
@@ -91,12 +90,8 @@ public class ArrowKeyMovementMethod implements MovementMethod {
    }

    private boolean left(TextView widget, Spannable buffer) {
        boolean cap = (MetaKeyKeyListener.getMetaState(buffer,
                        KeyEvent.META_SHIFT_ON) == 1) ||
                      (MetaKeyKeyListener.getMetaState(buffer,
                        MetaKeyKeyListener.META_SELECTING) != 0);
        boolean alt = MetaKeyKeyListener.getMetaState(buffer,
                        KeyEvent.META_ALT_ON) == 1;
        boolean cap = isCap(buffer);
        boolean alt = isAlt(buffer);
        Layout layout = widget.getLayout();

        if (cap) {
@@ -115,12 +110,8 @@ public class ArrowKeyMovementMethod implements MovementMethod {
    }

    private boolean right(TextView widget, Spannable buffer) {
        boolean cap = (MetaKeyKeyListener.getMetaState(buffer,
                        KeyEvent.META_SHIFT_ON) == 1) ||
                      (MetaKeyKeyListener.getMetaState(buffer,
                        MetaKeyKeyListener.META_SELECTING) != 0);
        boolean alt = MetaKeyKeyListener.getMetaState(buffer,
                        KeyEvent.META_ALT_ON) == 1;
        boolean cap = isCap(buffer);
        boolean alt = isAlt(buffer);
        Layout layout = widget.getLayout();

        if (cap) {
@@ -169,12 +160,11 @@ public class ArrowKeyMovementMethod implements MovementMethod {
            break;

        case KeyEvent.KEYCODE_DPAD_CENTER:
            if (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0) {
                if (widget.showContextMenu()) {
            if ((MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0) &&
                (widget.showContextMenu())) {
                    handled = true;
            }
        }
        }

        if (handled) {
            MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
@@ -190,8 +180,7 @@ public class ArrowKeyMovementMethod implements MovementMethod {

    public boolean onKeyOther(TextView view, Spannable text, KeyEvent event) {
        int code = event.getKeyCode();
        if (code != KeyEvent.KEYCODE_UNKNOWN
                && event.getAction() == KeyEvent.ACTION_MULTIPLE) {
        if (code != KeyEvent.KEYCODE_UNKNOWN && event.getAction() == KeyEvent.ACTION_MULTIPLE) {
            int repeat = event.getRepeatCount();
            boolean handled = false;
            while ((--repeat) > 0) {
@@ -228,54 +217,20 @@ public class ArrowKeyMovementMethod implements MovementMethod {

        if (widget.isFocused() && !widget.didTouchFocusSelect()) {
            if (event.getAction() == MotionEvent.ACTION_DOWN) {
              boolean cap = (MetaKeyKeyListener.getMetaState(buffer,
                              KeyEvent.META_SHIFT_ON) == 1) ||
                            (MetaKeyKeyListener.getMetaState(buffer,
                              MetaKeyKeyListener.META_SELECTING) != 0);
              int x = (int) event.getX();
              int y = (int) event.getY();
              int offset = widget.getOffset(x, y);

              boolean cap = isCap(buffer);
              if (cap) {
                  buffer.setSpan(LAST_TAP_DOWN, offset, offset,
                                 Spannable.SPAN_POINT_POINT);
                  int offset = widget.getOffset((int) event.getX(), (int) event.getY());
                  
                  buffer.setSpan(LAST_TAP_DOWN, offset, offset, Spannable.SPAN_POINT_POINT);

                  // Disallow intercepting of the touch events, so that
                  // users can scroll and select at the same time.
                  // without this, users would get booted out of select
                  // mode once the view detected it needed to scroll.
                  widget.getParent().requestDisallowInterceptTouchEvent(true);
              } else {
                  OnePointFiveTapState[] tap = buffer.getSpans(0, buffer.length(),
                      OnePointFiveTapState.class);

                  if (tap.length > 0) {
                      if (event.getEventTime() - tap[0].mWhen <=
                          ViewConfiguration.getDoubleTapTimeout() &&
                          sameWord(buffer, offset, Selection.getSelectionEnd(buffer))) {

                          tap[0].active = true;
                          MetaKeyKeyListener.startSelecting(widget, buffer);
                          widget.getParent().requestDisallowInterceptTouchEvent(true);
                          buffer.setSpan(LAST_TAP_DOWN, offset, offset,
                              Spannable.SPAN_POINT_POINT);
                      }

                      tap[0].mWhen = event.getEventTime();
                  } else {
                      OnePointFiveTapState newtap = new OnePointFiveTapState();
                      newtap.mWhen = event.getEventTime();
                      newtap.active = false;
                      buffer.setSpan(newtap, 0, buffer.length(),
                          Spannable.SPAN_INCLUSIVE_INCLUSIVE);
                  }
              }
            } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
                boolean cap = (MetaKeyKeyListener.getMetaState(buffer,
                                KeyEvent.META_SHIFT_ON) == 1) ||
                              (MetaKeyKeyListener.getMetaState(buffer,
                                MetaKeyKeyListener.META_SELECTING) != 0);

                boolean cap = isCap(buffer);

                if (cap && handled) {
                    // Before selecting, make sure we've moved out of the "slop".
@@ -289,39 +244,9 @@ public class ArrowKeyMovementMethod implements MovementMethod {
                    // Update selection as we're moving the selection area.

                    // Get the current touch position
                    int x = (int) event.getX();
                    int y = (int) event.getY();
                    int offset = widget.getOffset(x, y);

                    final OnePointFiveTapState[] tap = buffer.getSpans(0, buffer.length(),
                            OnePointFiveTapState.class);

                    if (tap.length > 0 && tap[0].active) {
                        // Get the last down touch position (the position at which the
                        // user started the selection)
                        int lastDownOffset = buffer.getSpanStart(LAST_TAP_DOWN);

                        // Compute the selection boundaries
                        int spanstart;
                        int spanend;
                        if (offset >= lastDownOffset) {
                            // Expand from word start of the original tap to new word
                            // end, since we are selecting "forwards"
                            spanstart = findWordStart(buffer, lastDownOffset);
                            spanend = findWordEnd(buffer, offset);
                        } else {
                            // Expand to from new word start to word end of the original
                            // tap since we are selecting "backwards".
                            // The spanend will always need to be associated with the touch
                            // up position, so that refining the selection with the
                            // trackball will work as expected.
                            spanstart = findWordEnd(buffer, lastDownOffset);
                            spanend = findWordStart(buffer, offset);
                        }
                        Selection.setSelection(buffer, spanstart, spanend);
                    } else {
                    int offset = widget.getOffset((int) event.getX(), (int) event.getY());

                    Selection.extendSelection(buffer, offset);
                    }
                    return true;
                }
            } else if (event.getAction() == MotionEvent.ACTION_UP) {
@@ -335,65 +260,12 @@ public class ArrowKeyMovementMethod implements MovementMethod {
                    return true;
                }

                int x = (int) event.getX();
                int y = (int) event.getY();
                int off = widget.getOffset(x, y);

                // XXX should do the same adjust for x as we do for the line.

                OnePointFiveTapState[] onepointfivetap = buffer.getSpans(0, buffer.length(),
                    OnePointFiveTapState.class);
                if (onepointfivetap.length > 0 && onepointfivetap[0].active &&
                    Selection.getSelectionStart(buffer) == Selection.getSelectionEnd(buffer)) {
                    // If we've set select mode, because there was a onepointfivetap,
                    // but there was no ensuing swipe gesture, undo the select mode
                    // and remove reference to the last onepointfivetap.
                    MetaKeyKeyListener.stopSelecting(widget, buffer);
                    for (int i=0; i < onepointfivetap.length; i++) {
                        buffer.removeSpan(onepointfivetap[i]);
                    }
                    buffer.removeSpan(LAST_TAP_DOWN);
                }
                boolean cap = (MetaKeyKeyListener.getMetaState(buffer,
                                KeyEvent.META_SHIFT_ON) == 1) ||
                              (MetaKeyKeyListener.getMetaState(buffer,
                                MetaKeyKeyListener.META_SELECTING) != 0);

                DoubleTapState[] tap = buffer.getSpans(0, buffer.length(),
                                                       DoubleTapState.class);
                boolean doubletap = false;

                if (tap.length > 0) {
                    if (event.getEventTime() - tap[0].mWhen <=
                        ViewConfiguration.getDoubleTapTimeout() &&
                        sameWord(buffer, off, Selection.getSelectionEnd(buffer))) {

                        doubletap = true;
                    }

                    tap[0].mWhen = event.getEventTime();
                } else {
                    DoubleTapState newtap = new DoubleTapState();
                    newtap.mWhen = event.getEventTime();
                    buffer.setSpan(newtap, 0, buffer.length(),
                                   Spannable.SPAN_INCLUSIVE_INCLUSIVE);
                }

                if (cap) {
                int offset = widget.getOffset((int) event.getX(), (int) event.getY());
                if (isCap(buffer)) {
                    buffer.removeSpan(LAST_TAP_DOWN);
                    if (onepointfivetap.length > 0 && onepointfivetap[0].active) {
                        // If we selecting something with the onepointfivetap-and
                        // swipe gesture, stop it on finger up.
                        MetaKeyKeyListener.stopSelecting(widget, buffer);
                    } else {
                        Selection.extendSelection(buffer, off);
                    }
                } else if (doubletap) {
                    Selection.setSelection(buffer,
                                           findWordStart(buffer, off),
                                           findWordEnd(buffer, off));
                    Selection.extendSelection(buffer, offset);
                } else {
                    Selection.setSelection(buffer, off);
                    Selection.setSelection(buffer, offset);
                }

                MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
@@ -438,75 +310,6 @@ public class ArrowKeyMovementMethod implements MovementMethod {
        mCursorController = cursorController;
    }

    private static class DoubleTapState implements NoCopySpan {
        long mWhen;
    }

    /* We check for a onepointfive tap. This is similar to
    *  doubletap gesture (where a finger goes down, up, down, up, in a short
    *  time period), except in the onepointfive tap, a users finger only needs
    *  to go down, up, down in a short time period. We detect this type of tap
    *  to implement the onepointfivetap-and-swipe selection gesture.
    *  This gesture allows users to select a segment of text without going
    *  through the "select text" option in the context menu.
    */
    private static class OnePointFiveTapState implements NoCopySpan {
        long mWhen;
        boolean active;
    }

    private static boolean sameWord(CharSequence text, int one, int two) {
        int start = findWordStart(text, one);
        int end = findWordEnd(text, one);

        if (end == start) {
            return false;
        }

        return start == findWordStart(text, two) &&
               end == findWordEnd(text, two);
    }

    // TODO: Unify with TextView.getWordForDictionary()
    private static int findWordStart(CharSequence text, int start) {
        for (; start > 0; start--) {
            char c = text.charAt(start - 1);
            int type = Character.getType(c);

            if (c != '\'' &&
                type != Character.UPPERCASE_LETTER &&
                type != Character.LOWERCASE_LETTER &&
                type != Character.TITLECASE_LETTER &&
                type != Character.MODIFIER_LETTER &&
                type != Character.DECIMAL_DIGIT_NUMBER) {
                break;
            }
        }

        return start;
    }

    // TODO: Unify with TextView.getWordForDictionary()
    private static int findWordEnd(CharSequence text, int end) {
        int len = text.length();

        for (; end < len; end++) {
            char c = text.charAt(end);
            int type = Character.getType(c);

            if (c != '\'' &&
                type != Character.UPPERCASE_LETTER &&
                type != Character.LOWERCASE_LETTER &&
                type != Character.TITLECASE_LETTER &&
                type != Character.MODIFIER_LETTER &&
                type != Character.DECIMAL_DIGIT_NUMBER) {
                break;
            }
        }

        return end;
    }

    public boolean canSelectArbitrarily() {
        return true;
    }
@@ -543,8 +346,9 @@ public class ArrowKeyMovementMethod implements MovementMethod {
    }

    public static MovementMethod getInstance() {
        if (sInstance == null)
        if (sInstance == null) {
            sInstance = new ArrowKeyMovementMethod();
        }

        return sInstance;
    }