Home | History | Annotate | Download | only in method
      1 /*
      2  * Copyright (C) 2006 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.text.method;
     18 
     19 import android.os.Build;
     20 import android.text.Layout;
     21 import android.text.NoCopySpan;
     22 import android.text.Selection;
     23 import android.text.Spannable;
     24 import android.text.style.ClickableSpan;
     25 import android.view.KeyEvent;
     26 import android.view.MotionEvent;
     27 import android.view.View;
     28 import android.view.textclassifier.TextLinks.TextLinkSpan;
     29 import android.widget.TextView;
     30 
     31 /**
     32  * A movement method that traverses links in the text buffer and scrolls if necessary.
     33  * Supports clicking on links with DPad Center or Enter.
     34  */
     35 public class LinkMovementMethod extends ScrollingMovementMethod {
     36     private static final int CLICK = 1;
     37     private static final int UP = 2;
     38     private static final int DOWN = 3;
     39 
     40     private static final int HIDE_FLOATING_TOOLBAR_DELAY_MS = 200;
     41 
     42     @Override
     43     public boolean canSelectArbitrarily() {
     44         return true;
     45     }
     46 
     47     @Override
     48     protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode,
     49             int movementMetaState, KeyEvent event) {
     50         switch (keyCode) {
     51             case KeyEvent.KEYCODE_DPAD_CENTER:
     52             case KeyEvent.KEYCODE_ENTER:
     53                 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
     54                     if (event.getAction() == KeyEvent.ACTION_DOWN &&
     55                             event.getRepeatCount() == 0 && action(CLICK, widget, buffer)) {
     56                         return true;
     57                     }
     58                 }
     59                 break;
     60         }
     61         return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event);
     62     }
     63 
     64     @Override
     65     protected boolean up(TextView widget, Spannable buffer) {
     66         if (action(UP, widget, buffer)) {
     67             return true;
     68         }
     69 
     70         return super.up(widget, buffer);
     71     }
     72 
     73     @Override
     74     protected boolean down(TextView widget, Spannable buffer) {
     75         if (action(DOWN, widget, buffer)) {
     76             return true;
     77         }
     78 
     79         return super.down(widget, buffer);
     80     }
     81 
     82     @Override
     83     protected boolean left(TextView widget, Spannable buffer) {
     84         if (action(UP, widget, buffer)) {
     85             return true;
     86         }
     87 
     88         return super.left(widget, buffer);
     89     }
     90 
     91     @Override
     92     protected boolean right(TextView widget, Spannable buffer) {
     93         if (action(DOWN, widget, buffer)) {
     94             return true;
     95         }
     96 
     97         return super.right(widget, buffer);
     98     }
     99 
    100     private boolean action(int what, TextView widget, Spannable buffer) {
    101         Layout layout = widget.getLayout();
    102 
    103         int padding = widget.getTotalPaddingTop() +
    104                       widget.getTotalPaddingBottom();
    105         int areaTop = widget.getScrollY();
    106         int areaBot = areaTop + widget.getHeight() - padding;
    107 
    108         int lineTop = layout.getLineForVertical(areaTop);
    109         int lineBot = layout.getLineForVertical(areaBot);
    110 
    111         int first = layout.getLineStart(lineTop);
    112         int last = layout.getLineEnd(lineBot);
    113 
    114         ClickableSpan[] candidates = buffer.getSpans(first, last, ClickableSpan.class);
    115 
    116         int a = Selection.getSelectionStart(buffer);
    117         int b = Selection.getSelectionEnd(buffer);
    118 
    119         int selStart = Math.min(a, b);
    120         int selEnd = Math.max(a, b);
    121 
    122         if (selStart < 0) {
    123             if (buffer.getSpanStart(FROM_BELOW) >= 0) {
    124                 selStart = selEnd = buffer.length();
    125             }
    126         }
    127 
    128         if (selStart > last)
    129             selStart = selEnd = Integer.MAX_VALUE;
    130         if (selEnd < first)
    131             selStart = selEnd = -1;
    132 
    133         switch (what) {
    134             case CLICK:
    135                 if (selStart == selEnd) {
    136                     return false;
    137                 }
    138 
    139                 ClickableSpan[] links = buffer.getSpans(selStart, selEnd, ClickableSpan.class);
    140 
    141                 if (links.length != 1) {
    142                     return false;
    143                 }
    144 
    145                 ClickableSpan link = links[0];
    146                 if (link instanceof TextLinkSpan) {
    147                     ((TextLinkSpan) link).onClick(widget, TextLinkSpan.INVOCATION_METHOD_KEYBOARD);
    148                 } else {
    149                     link.onClick(widget);
    150                 }
    151                 break;
    152 
    153             case UP:
    154                 int bestStart, bestEnd;
    155 
    156                 bestStart = -1;
    157                 bestEnd = -1;
    158 
    159                 for (int i = 0; i < candidates.length; i++) {
    160                     int end = buffer.getSpanEnd(candidates[i]);
    161 
    162                     if (end < selEnd || selStart == selEnd) {
    163                         if (end > bestEnd) {
    164                             bestStart = buffer.getSpanStart(candidates[i]);
    165                             bestEnd = end;
    166                         }
    167                     }
    168                 }
    169 
    170                 if (bestStart >= 0) {
    171                     Selection.setSelection(buffer, bestEnd, bestStart);
    172                     return true;
    173                 }
    174 
    175                 break;
    176 
    177             case DOWN:
    178                 bestStart = Integer.MAX_VALUE;
    179                 bestEnd = Integer.MAX_VALUE;
    180 
    181                 for (int i = 0; i < candidates.length; i++) {
    182                     int start = buffer.getSpanStart(candidates[i]);
    183 
    184                     if (start > selStart || selStart == selEnd) {
    185                         if (start < bestStart) {
    186                             bestStart = start;
    187                             bestEnd = buffer.getSpanEnd(candidates[i]);
    188                         }
    189                     }
    190                 }
    191 
    192                 if (bestEnd < Integer.MAX_VALUE) {
    193                     Selection.setSelection(buffer, bestStart, bestEnd);
    194                     return true;
    195                 }
    196 
    197                 break;
    198         }
    199 
    200         return false;
    201     }
    202 
    203     @Override
    204     public boolean onTouchEvent(TextView widget, Spannable buffer,
    205                                 MotionEvent event) {
    206         int action = event.getAction();
    207 
    208         if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
    209             int x = (int) event.getX();
    210             int y = (int) event.getY();
    211 
    212             x -= widget.getTotalPaddingLeft();
    213             y -= widget.getTotalPaddingTop();
    214 
    215             x += widget.getScrollX();
    216             y += widget.getScrollY();
    217 
    218             Layout layout = widget.getLayout();
    219             int line = layout.getLineForVertical(y);
    220             int off = layout.getOffsetForHorizontal(line, x);
    221 
    222             ClickableSpan[] links = buffer.getSpans(off, off, ClickableSpan.class);
    223 
    224             if (links.length != 0) {
    225                 ClickableSpan link = links[0];
    226                 if (action == MotionEvent.ACTION_UP) {
    227                     if (link instanceof TextLinkSpan) {
    228                         ((TextLinkSpan) link).onClick(
    229                                 widget, TextLinkSpan.INVOCATION_METHOD_TOUCH);
    230                     } else {
    231                         link.onClick(widget);
    232                     }
    233                 } else if (action == MotionEvent.ACTION_DOWN) {
    234                     if (widget.getContext().getApplicationInfo().targetSdkVersion
    235                             >= Build.VERSION_CODES.P) {
    236                         // Selection change will reposition the toolbar. Hide it for a few ms for a
    237                         // smoother transition.
    238                         widget.hideFloatingToolbar(HIDE_FLOATING_TOOLBAR_DELAY_MS);
    239                     }
    240                     Selection.setSelection(buffer,
    241                             buffer.getSpanStart(link),
    242                             buffer.getSpanEnd(link));
    243                 }
    244                 return true;
    245             } else {
    246                 Selection.removeSelection(buffer);
    247             }
    248         }
    249 
    250         return super.onTouchEvent(widget, buffer, event);
    251     }
    252 
    253     @Override
    254     public void initialize(TextView widget, Spannable text) {
    255         Selection.removeSelection(text);
    256         text.removeSpan(FROM_BELOW);
    257     }
    258 
    259     @Override
    260     public void onTakeFocus(TextView view, Spannable text, int dir) {
    261         Selection.removeSelection(text);
    262 
    263         if ((dir & View.FOCUS_BACKWARD) != 0) {
    264             text.setSpan(FROM_BELOW, 0, 0, Spannable.SPAN_POINT_POINT);
    265         } else {
    266             text.removeSpan(FROM_BELOW);
    267         }
    268     }
    269 
    270     public static MovementMethod getInstance() {
    271         if (sInstance == null)
    272             sInstance = new LinkMovementMethod();
    273 
    274         return sInstance;
    275     }
    276 
    277     private static LinkMovementMethod sInstance;
    278     private static Object FROM_BELOW = new NoCopySpan.Concrete();
    279 }
    280