/* * Copyright (C) 2006 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.text.method; import android.graphics.Rect; import android.text.Layout; import android.text.Selection; import android.text.Spannable; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.widget.TextView; /** * A movement method that provides cursor movement and selection. * Supports displaying the context menu on DPad Center. */ public class ArrowKeyMovementMethod extends BaseMovementMethod implements MovementMethod { private static boolean isSelecting(Spannable buffer) { return ((MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SHIFT_ON) == 1) || (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0)); } private static int getCurrentLineTop(Spannable buffer, Layout layout) { return layout.getLineTop(layout.getLineForOffset(Selection.getSelectionEnd(buffer))); } private static int getPageHeight(TextView widget) { // This calculation does not take into account the view transformations that // may have been applied to the child or its containers. In case of scaling or // rotation, the calculated page height may be incorrect. final Rect rect = new Rect(); return widget.getGlobalVisibleRect(rect) ? rect.height() : 0; } @Override protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode, int movementMetaState, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_CENTER: if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0 && MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING, event) != 0) { return widget.showContextMenu(); } } break; } return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event); } @Override protected boolean left(TextView widget, Spannable buffer) { final Layout layout = widget.getLayout(); if (isSelecting(buffer)) { return Selection.extendLeft(buffer, layout); } else { return Selection.moveLeft(buffer, layout); } } @Override protected boolean right(TextView widget, Spannable buffer) { final Layout layout = widget.getLayout(); if (isSelecting(buffer)) { return Selection.extendRight(buffer, layout); } else { return Selection.moveRight(buffer, layout); } } @Override protected boolean up(TextView widget, Spannable buffer) { final Layout layout = widget.getLayout(); if (isSelecting(buffer)) { return Selection.extendUp(buffer, layout); } else { return Selection.moveUp(buffer, layout); } } @Override protected boolean down(TextView widget, Spannable buffer) { final Layout layout = widget.getLayout(); if (isSelecting(buffer)) { return Selection.extendDown(buffer, layout); } else { return Selection.moveDown(buffer, layout); } } @Override protected boolean pageUp(TextView widget, Spannable buffer) { final Layout layout = widget.getLayout(); final boolean selecting = isSelecting(buffer); final int targetY = getCurrentLineTop(buffer, layout) - getPageHeight(widget); boolean handled = false; for (;;) { final int previousSelectionEnd = Selection.getSelectionEnd(buffer); if (selecting) { Selection.extendUp(buffer, layout); } else { Selection.moveUp(buffer, layout); } if (Selection.getSelectionEnd(buffer) == previousSelectionEnd) { break; } handled = true; if (getCurrentLineTop(buffer, layout) <= targetY) { break; } } return handled; } @Override protected boolean pageDown(TextView widget, Spannable buffer) { final Layout layout = widget.getLayout(); final boolean selecting = isSelecting(buffer); final int targetY = getCurrentLineTop(buffer, layout) + getPageHeight(widget); boolean handled = false; for (;;) { final int previousSelectionEnd = Selection.getSelectionEnd(buffer); if (selecting) { Selection.extendDown(buffer, layout); } else { Selection.moveDown(buffer, layout); } if (Selection.getSelectionEnd(buffer) == previousSelectionEnd) { break; } handled = true; if (getCurrentLineTop(buffer, layout) >= targetY) { break; } } return handled; } @Override protected boolean top(TextView widget, Spannable buffer) { if (isSelecting(buffer)) { Selection.extendSelection(buffer, 0); } else { Selection.setSelection(buffer, 0); } return true; } @Override protected boolean bottom(TextView widget, Spannable buffer) { if (isSelecting(buffer)) { Selection.extendSelection(buffer, buffer.length()); } else { Selection.setSelection(buffer, buffer.length()); } return true; } @Override protected boolean lineStart(TextView widget, Spannable buffer) { final Layout layout = widget.getLayout(); if (isSelecting(buffer)) { return Selection.extendToLeftEdge(buffer, layout); } else { return Selection.moveToLeftEdge(buffer, layout); } } @Override protected boolean lineEnd(TextView widget, Spannable buffer) { final Layout layout = widget.getLayout(); if (isSelecting(buffer)) { return Selection.extendToRightEdge(buffer, layout); } else { return Selection.moveToRightEdge(buffer, layout); } } /** {@hide} */ @Override protected boolean leftWord(TextView widget, Spannable buffer) { final int selectionEnd = widget.getSelectionEnd(); final WordIterator wordIterator = widget.getWordIterator(); wordIterator.setCharSequence(buffer, selectionEnd, selectionEnd); return Selection.moveToPreceding(buffer, wordIterator, isSelecting(buffer)); } /** {@hide} */ @Override protected boolean rightWord(TextView widget, Spannable buffer) { final int selectionEnd = widget.getSelectionEnd(); final WordIterator wordIterator = widget.getWordIterator(); wordIterator.setCharSequence(buffer, selectionEnd, selectionEnd); return Selection.moveToFollowing(buffer, wordIterator, isSelecting(buffer)); } @Override protected boolean home(TextView widget, Spannable buffer) { return lineStart(widget, buffer); } @Override protected boolean end(TextView widget, Spannable buffer) { return lineEnd(widget, buffer); } @Override public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { int initialScrollX = -1; int initialScrollY = -1; final int action = event.getAction(); if (action == MotionEvent.ACTION_UP) { initialScrollX = Touch.getInitialScrollX(widget, buffer); initialScrollY = Touch.getInitialScrollY(widget, buffer); } boolean wasTouchSelecting = isSelecting(buffer); boolean handled = Touch.onTouchEvent(widget, buffer, event); if (widget.didTouchFocusSelect()) { return handled; } if (action == MotionEvent.ACTION_DOWN) { // For touch events, the code should run only when selection is active. if (isSelecting(buffer)) { if (!widget.isFocused()) { if (!widget.requestFocus()) { return handled; } } int offset = widget.getOffsetForPosition(event.getX(), 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 if (widget.isFocused()) { if (action == MotionEvent.ACTION_MOVE) { if (isSelecting(buffer) && handled) { final int startOffset = buffer.getSpanStart(LAST_TAP_DOWN); // Before selecting, make sure we've moved out of the "slop". // handled will be true, if we're in select mode AND we're // OUT of the slop // Turn long press off while we're selecting. User needs to // re-tap on the selection to enable long press widget.cancelLongPress(); // Update selection as we're moving the selection area. // Get the current touch position final int offset = widget.getOffsetForPosition(event.getX(), event.getY()); Selection.setSelection(buffer, Math.min(startOffset, offset), Math.max(startOffset, offset)); return true; } } else if (action == MotionEvent.ACTION_UP) { // If we have scrolled, then the up shouldn't move the cursor, // but we do need to make sure the cursor is still visible at // the current scroll offset to avoid the scroll jumping later // to show it. if ((initialScrollY >= 0 && initialScrollY != widget.getScrollY()) || (initialScrollX >= 0 && initialScrollX != widget.getScrollX())) { widget.moveCursorToVisibleOffset(); return true; } if (wasTouchSelecting) { final int startOffset = buffer.getSpanStart(LAST_TAP_DOWN); final int endOffset = widget.getOffsetForPosition(event.getX(), event.getY()); Selection.setSelection(buffer, Math.min(startOffset, endOffset), Math.max(startOffset, endOffset)); buffer.removeSpan(LAST_TAP_DOWN); } MetaKeyKeyListener.adjustMetaAfterKeypress(buffer); MetaKeyKeyListener.resetLockedMeta(buffer); return true; } } return handled; } @Override public boolean canSelectArbitrarily() { return true; } @Override public void initialize(TextView widget, Spannable text) { Selection.setSelection(text, 0); } @Override public void onTakeFocus(TextView view, Spannable text, int dir) { if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) { if (view.getLayout() == null) { // This shouldn't be null, but do something sensible if it is. Selection.setSelection(text, text.length()); } } else { Selection.setSelection(text, text.length()); } } public static MovementMethod getInstance() { if (sInstance == null) { sInstance = new ArrowKeyMovementMethod(); } return sInstance; } private static final Object LAST_TAP_DOWN = new Object(); private static ArrowKeyMovementMethod sInstance; }