ArrowKeyMovementMethod.java revision daa4a95a54909d53a325eb06ca22130743b5be04
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
17package android.text.method;
18
19import android.graphics.Rect;
20import android.text.CharSequenceIterator;
21import android.text.Editable;
22import android.text.Layout;
23import android.text.Selection;
24import android.text.Spannable;
25import android.text.Spanned;
26import android.text.TextWatcher;
27import android.util.Log;
28import android.util.MathUtils;
29import android.view.KeyEvent;
30import android.view.MotionEvent;
31import android.view.View;
32import android.widget.TextView;
33
34import java.text.BreakIterator;
35import java.text.CharacterIterator;
36
37/**
38 * A movement method that provides cursor movement and selection.
39 * Supports displaying the context menu on DPad Center.
40 */
41public class ArrowKeyMovementMethod extends BaseMovementMethod implements MovementMethod {
42    private static boolean isSelecting(Spannable buffer) {
43        return ((MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SHIFT_ON) == 1) ||
44                (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0));
45    }
46
47    private int getCurrentLineTop(Spannable buffer, Layout layout) {
48        return layout.getLineTop(layout.getLineForOffset(Selection.getSelectionEnd(buffer)));
49    }
50
51    private int getPageHeight(TextView widget) {
52        // This calculation does not take into account the view transformations that
53        // may have been applied to the child or its containers.  In case of scaling or
54        // rotation, the calculated page height may be incorrect.
55        final Rect rect = new Rect();
56        return widget.getGlobalVisibleRect(rect) ? rect.height() : 0;
57    }
58
59    @Override
60    protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode,
61            int movementMetaState, KeyEvent event) {
62        switch (keyCode) {
63            case KeyEvent.KEYCODE_DPAD_CENTER:
64                if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
65                    if (event.getAction() == KeyEvent.ACTION_DOWN
66                            && event.getRepeatCount() == 0
67                            && MetaKeyKeyListener.getMetaState(buffer,
68                                        MetaKeyKeyListener.META_SELECTING) != 0) {
69                        return widget.showContextMenu();
70                    }
71                }
72                break;
73        }
74        return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event);
75    }
76
77    @Override
78    protected boolean left(TextView widget, Spannable buffer) {
79        final Layout layout = widget.getLayout();
80        if (isSelecting(buffer)) {
81            return Selection.extendLeft(buffer, layout);
82        } else {
83            return Selection.moveLeft(buffer, layout);
84        }
85    }
86
87    @Override
88    protected boolean right(TextView widget, Spannable buffer) {
89        final Layout layout = widget.getLayout();
90        if (isSelecting(buffer)) {
91            return Selection.extendRight(buffer, layout);
92        } else {
93            return Selection.moveRight(buffer, layout);
94        }
95    }
96
97    @Override
98    protected boolean up(TextView widget, Spannable buffer) {
99        final Layout layout = widget.getLayout();
100        if (isSelecting(buffer)) {
101            return Selection.extendUp(buffer, layout);
102        } else {
103            return Selection.moveUp(buffer, layout);
104        }
105    }
106
107    @Override
108    protected boolean down(TextView widget, Spannable buffer) {
109        final Layout layout = widget.getLayout();
110        if (isSelecting(buffer)) {
111            return Selection.extendDown(buffer, layout);
112        } else {
113            return Selection.moveDown(buffer, layout);
114        }
115    }
116
117    @Override
118    protected boolean pageUp(TextView widget, Spannable buffer) {
119        final Layout layout = widget.getLayout();
120        final boolean selecting = isSelecting(buffer);
121        final int targetY = getCurrentLineTop(buffer, layout) - getPageHeight(widget);
122        boolean handled = false;
123        for (;;) {
124            final int previousSelectionEnd = Selection.getSelectionEnd(buffer);
125            if (selecting) {
126                Selection.extendUp(buffer, layout);
127            } else {
128                Selection.moveUp(buffer, layout);
129            }
130            if (Selection.getSelectionEnd(buffer) == previousSelectionEnd) {
131                break;
132            }
133            handled = true;
134            if (getCurrentLineTop(buffer, layout) <= targetY) {
135                break;
136            }
137        }
138        return handled;
139    }
140
141    @Override
142    protected boolean pageDown(TextView widget, Spannable buffer) {
143        final Layout layout = widget.getLayout();
144        final boolean selecting = isSelecting(buffer);
145        final int targetY = getCurrentLineTop(buffer, layout) + getPageHeight(widget);
146        boolean handled = false;
147        for (;;) {
148            final int previousSelectionEnd = Selection.getSelectionEnd(buffer);
149            if (selecting) {
150                Selection.extendDown(buffer, layout);
151            } else {
152                Selection.moveDown(buffer, layout);
153            }
154            if (Selection.getSelectionEnd(buffer) == previousSelectionEnd) {
155                break;
156            }
157            handled = true;
158            if (getCurrentLineTop(buffer, layout) >= targetY) {
159                break;
160            }
161        }
162        return handled;
163    }
164
165    @Override
166    protected boolean top(TextView widget, Spannable buffer) {
167        if (isSelecting(buffer)) {
168            Selection.extendSelection(buffer, 0);
169        } else {
170            Selection.setSelection(buffer, 0);
171        }
172        return true;
173    }
174
175    @Override
176    protected boolean bottom(TextView widget, Spannable buffer) {
177        if (isSelecting(buffer)) {
178            Selection.extendSelection(buffer, buffer.length());
179        } else {
180            Selection.setSelection(buffer, buffer.length());
181        }
182        return true;
183    }
184
185    @Override
186    protected boolean lineStart(TextView widget, Spannable buffer) {
187        final Layout layout = widget.getLayout();
188        if (isSelecting(buffer)) {
189            return Selection.extendToLeftEdge(buffer, layout);
190        } else {
191            return Selection.moveToLeftEdge(buffer, layout);
192        }
193    }
194
195    @Override
196    protected boolean lineEnd(TextView widget, Spannable buffer) {
197        final Layout layout = widget.getLayout();
198        if (isSelecting(buffer)) {
199            return Selection.extendToRightEdge(buffer, layout);
200        } else {
201            return Selection.moveToRightEdge(buffer, layout);
202        }
203    }
204
205    /** {@hide} */
206    @Override
207    protected boolean leftWord(TextView widget, Spannable buffer) {
208        mWordIterator.setCharSequence(buffer);
209        return Selection.moveToPreceding(buffer, mWordIterator, isSelecting(buffer));
210    }
211
212    /** {@hide} */
213    @Override
214    protected boolean rightWord(TextView widget, Spannable buffer) {
215        mWordIterator.setCharSequence(buffer);
216        return Selection.moveToFollowing(buffer, mWordIterator, isSelecting(buffer));
217    }
218
219    @Override
220    protected boolean home(TextView widget, Spannable buffer) {
221        return lineStart(widget, buffer);
222    }
223
224    @Override
225    protected boolean end(TextView widget, Spannable buffer) {
226        return lineEnd(widget, buffer);
227    }
228
229    @Override
230    public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
231        int initialScrollX = -1;
232        int initialScrollY = -1;
233        final int action = event.getAction();
234
235        if (action == MotionEvent.ACTION_UP) {
236            initialScrollX = Touch.getInitialScrollX(widget, buffer);
237            initialScrollY = Touch.getInitialScrollY(widget, buffer);
238        }
239
240        boolean handled = Touch.onTouchEvent(widget, buffer, event);
241
242        if (widget.isFocused() && !widget.didTouchFocusSelect()) {
243            if (action == MotionEvent.ACTION_DOWN) {
244              boolean cap = isSelecting(buffer);
245              if (cap) {
246                  int offset = widget.getOffset((int) event.getX(), (int) event.getY());
247
248                  buffer.setSpan(LAST_TAP_DOWN, offset, offset, Spannable.SPAN_POINT_POINT);
249
250                  // Disallow intercepting of the touch events, so that
251                  // users can scroll and select at the same time.
252                  // without this, users would get booted out of select
253                  // mode once the view detected it needed to scroll.
254                  widget.getParent().requestDisallowInterceptTouchEvent(true);
255              }
256            } else if (action == MotionEvent.ACTION_MOVE) {
257                boolean cap = isSelecting(buffer);
258
259                if (cap && handled) {
260                    // Before selecting, make sure we've moved out of the "slop".
261                    // handled will be true, if we're in select mode AND we're
262                    // OUT of the slop
263
264                    // Turn long press off while we're selecting. User needs to
265                    // re-tap on the selection to enable long press
266                    widget.cancelLongPress();
267
268                    // Update selection as we're moving the selection area.
269
270                    // Get the current touch position
271                    int offset = widget.getOffset((int) event.getX(), (int) event.getY());
272
273                    Selection.extendSelection(buffer, offset);
274                    return true;
275                }
276            } else if (action == MotionEvent.ACTION_UP) {
277                // If we have scrolled, then the up shouldn't move the cursor,
278                // but we do need to make sure the cursor is still visible at
279                // the current scroll offset to avoid the scroll jumping later
280                // to show it.
281                if ((initialScrollY >= 0 && initialScrollY != widget.getScrollY()) ||
282                    (initialScrollX >= 0 && initialScrollX != widget.getScrollX())) {
283                    widget.moveCursorToVisibleOffset();
284                    return true;
285                }
286
287                int offset = widget.getOffset((int) event.getX(), (int) event.getY());
288                if (isSelecting(buffer)) {
289                    buffer.removeSpan(LAST_TAP_DOWN);
290                    Selection.extendSelection(buffer, offset);
291                } else {
292                    Selection.setSelection(buffer, offset);
293                }
294
295                MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
296                MetaKeyKeyListener.resetLockedMeta(buffer);
297
298                return true;
299            }
300        }
301
302        return handled;
303    }
304
305    @Override
306    public boolean canSelectArbitrarily() {
307        return true;
308    }
309
310    @Override
311    public void initialize(TextView widget, Spannable text) {
312        Selection.setSelection(text, 0);
313    }
314
315    @Override
316    public void onTakeFocus(TextView view, Spannable text, int dir) {
317        if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) {
318            if (view.getLayout() == null) {
319                // This shouldn't be null, but do something sensible if it is.
320                Selection.setSelection(text, text.length());
321            }
322        } else {
323            Selection.setSelection(text, text.length());
324        }
325    }
326
327    public static MovementMethod getInstance() {
328        if (sInstance == null) {
329            sInstance = new ArrowKeyMovementMethod();
330        }
331
332        return sInstance;
333    }
334
335    /**
336     * Walks through cursor positions at word boundaries. Internally uses
337     * {@link BreakIterator#getWordInstance()}, and caches {@link CharSequence}
338     * for performance reasons.
339     */
340    private static class WordIterator implements Selection.PositionIterator {
341        private CharSequence mCurrent;
342        private boolean mCurrentDirty = false;
343
344        private BreakIterator mIterator;
345
346        private TextWatcher mWatcher = new TextWatcher() {
347            /** {@inheritDoc} */
348            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
349                // ignored
350            }
351
352            /** {@inheritDoc} */
353            public void onTextChanged(CharSequence s, int start, int before, int count) {
354                mCurrentDirty = true;
355            }
356
357            /** {@inheritDoc} */
358            public void afterTextChanged(Editable s) {
359                // ignored
360            }
361        };
362
363        public void setCharSequence(CharSequence incoming) {
364            if (mIterator == null) {
365                mIterator = BreakIterator.getWordInstance();
366            }
367
368            // when incoming is different object, move listeners to new sequence
369            // and mark as dirty so we reload contents.
370            if (mCurrent != incoming) {
371                if (mCurrent instanceof Editable) {
372                    ((Editable) mCurrent).removeSpan(mWatcher);
373                }
374
375                if (incoming instanceof Editable) {
376                    ((Editable) incoming).setSpan(
377                            mWatcher, 0, incoming.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
378                }
379
380                mCurrent = incoming;
381                mCurrentDirty = true;
382            }
383
384            if (mCurrentDirty) {
385                final CharacterIterator charIterator = new CharSequenceIterator(mCurrent);
386                mIterator.setText(charIterator);
387
388                mCurrentDirty = false;
389            }
390        }
391
392        private boolean isValidOffset(int offset) {
393            return offset >= 0 && offset <= mCurrent.length();
394        }
395
396        private boolean isLetterOrDigit(int offset) {
397            if (isValidOffset(offset)) {
398                return Character.isLetterOrDigit(mCurrent.charAt(offset));
399            } else {
400                return false;
401            }
402        }
403
404        /** {@inheritDoc} */
405        public int preceding(int offset) {
406            // always round cursor index into valid string index
407            offset = MathUtils.constrain(offset, 0, mCurrent.length());
408
409            do {
410                offset = mIterator.preceding(offset);
411                if (isLetterOrDigit(offset)) break;
412            } while (isValidOffset(offset));
413
414            return offset;
415        }
416
417        /** {@inheritDoc} */
418        public int following(int offset) {
419            // always round cursor index into valid string index
420            offset = MathUtils.constrain(offset, 0, mCurrent.length());
421
422            do {
423                offset = mIterator.following(offset);
424                if (isLetterOrDigit(offset - 1)) break;
425            } while (isValidOffset(offset));
426
427            return offset;
428        }
429    }
430
431    private WordIterator mWordIterator = new WordIterator();
432
433    private static final Object LAST_TAP_DOWN = new Object();
434    private static ArrowKeyMovementMethod sInstance;
435}
436