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.text.Layout;
     20 import android.text.NoCopySpan;
     21 import android.text.Selection;
     22 import android.text.Spannable;
     23 import android.text.style.ClickableSpan;
     24 import android.view.KeyEvent;
     25 import android.view.MotionEvent;
     26 import android.view.View;
     27 import android.widget.TextView;
     28 
     29 /**
     30  * A movement method that traverses links in the text buffer and scrolls if necessary.
     31  * Supports clicking on links with DPad Center or Enter.
     32  */
     33 public class LinkMovementMethod extends ScrollingMovementMethod {
     34     private static final int CLICK = 1;
     35     private static final int UP = 2;
     36     private static final int DOWN = 3;
     37 
     38     @Override
     39     protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode,
     40             int movementMetaState, KeyEvent event) {
     41         switch (keyCode) {
     42             case KeyEvent.KEYCODE_DPAD_CENTER:
     43             case KeyEvent.KEYCODE_ENTER:
     44                 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
     45                     if (event.getAction() == KeyEvent.ACTION_DOWN &&
     46                             event.getRepeatCount() == 0 && action(CLICK, widget, buffer)) {
     47                         return true;
     48                     }
     49                 }
     50                 break;
     51         }
     52         return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event);
     53     }
     54 
     55     @Override
     56     protected boolean up(TextView widget, Spannable buffer) {
     57         if (action(UP, widget, buffer)) {
     58             return true;
     59         }
     60 
     61         return super.up(widget, buffer);
     62     }
     63 
     64     @Override
     65     protected boolean down(TextView widget, Spannable buffer) {
     66         if (action(DOWN, widget, buffer)) {
     67             return true;
     68         }
     69 
     70         return super.down(widget, buffer);
     71     }
     72 
     73     @Override
     74     protected boolean left(TextView widget, Spannable buffer) {
     75         if (action(UP, widget, buffer)) {
     76             return true;
     77         }
     78 
     79         return super.left(widget, buffer);
     80     }
     81 
     82     @Override
     83     protected boolean right(TextView widget, Spannable buffer) {
     84         if (action(DOWN, widget, buffer)) {
     85             return true;
     86         }
     87 
     88         return super.right(widget, buffer);
     89     }
     90 
     91     private boolean action(int what, TextView widget, Spannable buffer) {
     92         Layout layout = widget.getLayout();
     93 
     94         int padding = widget.getTotalPaddingTop() +
     95                       widget.getTotalPaddingBottom();
     96         int areatop = widget.getScrollY();
     97         int areabot = areatop + widget.getHeight() - padding;
     98 
     99         int linetop = layout.getLineForVertical(areatop);
    100         int linebot = layout.getLineForVertical(areabot);
    101 
    102         int first = layout.getLineStart(linetop);
    103         int last = layout.getLineEnd(linebot);
    104 
    105         ClickableSpan[] candidates = buffer.getSpans(first, last, ClickableSpan.class);
    106 
    107         int a = Selection.getSelectionStart(buffer);
    108         int b = Selection.getSelectionEnd(buffer);
    109 
    110         int selStart = Math.min(a, b);
    111         int selEnd = Math.max(a, b);
    112 
    113         if (selStart < 0) {
    114             if (buffer.getSpanStart(FROM_BELOW) >= 0) {
    115                 selStart = selEnd = buffer.length();
    116             }
    117         }
    118 
    119         if (selStart > last)
    120             selStart = selEnd = Integer.MAX_VALUE;
    121         if (selEnd < first)
    122             selStart = selEnd = -1;
    123 
    124         switch (what) {
    125         case CLICK:
    126             if (selStart == selEnd) {
    127                 return false;
    128             }
    129 
    130             ClickableSpan[] link = buffer.getSpans(selStart, selEnd, ClickableSpan.class);
    131 
    132             if (link.length != 1)
    133                 return false;
    134 
    135             link[0].onClick(widget);
    136             break;
    137 
    138         case UP:
    139             int beststart, bestend;
    140 
    141             beststart = -1;
    142             bestend = -1;
    143 
    144             for (int i = 0; i < candidates.length; i++) {
    145                 int end = buffer.getSpanEnd(candidates[i]);
    146 
    147                 if (end < selEnd || selStart == selEnd) {
    148                     if (end > bestend) {
    149                         beststart = buffer.getSpanStart(candidates[i]);
    150                         bestend = end;
    151                     }
    152                 }
    153             }
    154 
    155             if (beststart >= 0) {
    156                 Selection.setSelection(buffer, bestend, beststart);
    157                 return true;
    158             }
    159 
    160             break;
    161 
    162         case DOWN:
    163             beststart = Integer.MAX_VALUE;
    164             bestend = Integer.MAX_VALUE;
    165 
    166             for (int i = 0; i < candidates.length; i++) {
    167                 int start = buffer.getSpanStart(candidates[i]);
    168 
    169                 if (start > selStart || selStart == selEnd) {
    170                     if (start < beststart) {
    171                         beststart = start;
    172                         bestend = buffer.getSpanEnd(candidates[i]);
    173                     }
    174                 }
    175             }
    176 
    177             if (bestend < Integer.MAX_VALUE) {
    178                 Selection.setSelection(buffer, beststart, bestend);
    179                 return true;
    180             }
    181 
    182             break;
    183         }
    184 
    185         return false;
    186     }
    187 
    188     @Override
    189     public boolean onTouchEvent(TextView widget, Spannable buffer,
    190                                 MotionEvent event) {
    191         int action = event.getAction();
    192 
    193         if (action == MotionEvent.ACTION_UP ||
    194             action == MotionEvent.ACTION_DOWN) {
    195             int x = (int) event.getX();
    196             int y = (int) event.getY();
    197 
    198             x -= widget.getTotalPaddingLeft();
    199             y -= widget.getTotalPaddingTop();
    200 
    201             x += widget.getScrollX();
    202             y += widget.getScrollY();
    203 
    204             Layout layout = widget.getLayout();
    205             int line = layout.getLineForVertical(y);
    206             int off = layout.getOffsetForHorizontal(line, x);
    207 
    208             ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);
    209 
    210             if (link.length != 0) {
    211                 if (action == MotionEvent.ACTION_UP) {
    212                     link[0].onClick(widget);
    213                 } else if (action == MotionEvent.ACTION_DOWN) {
    214                     Selection.setSelection(buffer,
    215                                            buffer.getSpanStart(link[0]),
    216                                            buffer.getSpanEnd(link[0]));
    217                 }
    218 
    219                 return true;
    220             } else {
    221                 Selection.removeSelection(buffer);
    222             }
    223         }
    224 
    225         return super.onTouchEvent(widget, buffer, event);
    226     }
    227 
    228     @Override
    229     public void initialize(TextView widget, Spannable text) {
    230         Selection.removeSelection(text);
    231         text.removeSpan(FROM_BELOW);
    232     }
    233 
    234     @Override
    235     public void onTakeFocus(TextView view, Spannable text, int dir) {
    236         Selection.removeSelection(text);
    237 
    238         if ((dir & View.FOCUS_BACKWARD) != 0) {
    239             text.setSpan(FROM_BELOW, 0, 0, Spannable.SPAN_POINT_POINT);
    240         } else {
    241             text.removeSpan(FROM_BELOW);
    242         }
    243     }
    244 
    245     public static MovementMethod getInstance() {
    246         if (sInstance == null)
    247             sInstance = new LinkMovementMethod();
    248 
    249         return sInstance;
    250     }
    251 
    252     private static LinkMovementMethod sInstance;
    253     private static Object FROM_BELOW = new NoCopySpan.Concrete();
    254 }
    255