/* * Copyright (C) 2010 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.text.Layout; import android.text.Spannable; import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; import android.widget.TextView; /** * Base classes for movement methods. */ public class BaseMovementMethod implements MovementMethod { @Override public boolean canSelectArbitrarily() { return false; } @Override public void initialize(TextView widget, Spannable text) { } @Override public boolean onKeyDown(TextView widget, Spannable text, int keyCode, KeyEvent event) { final int movementMetaState = getMovementMetaState(text, event); boolean handled = handleMovementKey(widget, text, keyCode, movementMetaState, event); if (handled) { MetaKeyKeyListener.adjustMetaAfterKeypress(text); MetaKeyKeyListener.resetLockedMeta(text); } return handled; } @Override public boolean onKeyOther(TextView widget, Spannable text, KeyEvent event) { final int movementMetaState = getMovementMetaState(text, event); final int keyCode = event.getKeyCode(); if (keyCode != KeyEvent.KEYCODE_UNKNOWN && event.getAction() == KeyEvent.ACTION_MULTIPLE) { final int repeat = event.getRepeatCount(); boolean handled = false; for (int i = 0; i < repeat; i++) { if (!handleMovementKey(widget, text, keyCode, movementMetaState, event)) { break; } handled = true; } if (handled) { MetaKeyKeyListener.adjustMetaAfterKeypress(text); MetaKeyKeyListener.resetLockedMeta(text); } return handled; } return false; } @Override public boolean onKeyUp(TextView widget, Spannable text, int keyCode, KeyEvent event) { return false; } @Override public void onTakeFocus(TextView widget, Spannable text, int direction) { } @Override public boolean onTouchEvent(TextView widget, Spannable text, MotionEvent event) { return false; } @Override public boolean onTrackballEvent(TextView widget, Spannable text, MotionEvent event) { return false; } @Override public boolean onGenericMotionEvent(TextView widget, Spannable text, MotionEvent event) { if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { switch (event.getAction()) { case MotionEvent.ACTION_SCROLL: { final float vscroll; final float hscroll; if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) { vscroll = 0; hscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL); } else { vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL); hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL); } boolean handled = false; if (hscroll < 0) { handled |= scrollLeft(widget, text, (int)Math.ceil(-hscroll)); } else if (hscroll > 0) { handled |= scrollRight(widget, text, (int)Math.ceil(hscroll)); } if (vscroll < 0) { handled |= scrollUp(widget, text, (int)Math.ceil(-vscroll)); } else if (vscroll > 0) { handled |= scrollDown(widget, text, (int)Math.ceil(vscroll)); } return handled; } } } return false; } /** * Gets the meta state used for movement using the modifiers tracked by the text * buffer as well as those present in the key event. * * The movement meta state excludes the state of locked modifiers or the SHIFT key * since they are not used by movement actions (but they may be used for selection). * * @param buffer The text buffer. * @param event The key event. * @return The keyboard meta states used for movement. */ protected int getMovementMetaState(Spannable buffer, KeyEvent event) { // We ignore locked modifiers and SHIFT. int metaState = MetaKeyKeyListener.getMetaState(buffer, event) & ~(MetaKeyKeyListener.META_ALT_LOCKED | MetaKeyKeyListener.META_SYM_LOCKED); return KeyEvent.normalizeMetaState(metaState) & ~KeyEvent.META_SHIFT_MASK; } /** * Performs a movement key action. * The default implementation decodes the key down and invokes movement actions * such as {@link #down} and {@link #up}. * {@link #onKeyDown(TextView, Spannable, int, KeyEvent)} calls this method once * to handle an {@link KeyEvent#ACTION_DOWN}. * {@link #onKeyOther(TextView, Spannable, KeyEvent)} calls this method repeatedly * to handle each repetition of an {@link KeyEvent#ACTION_MULTIPLE}. * * @param widget The text view. * @param buffer The text buffer. * @param event The key event. * @param keyCode The key code. * @param movementMetaState The keyboard meta states used for movement. * @param event The key event. * @return True if the event was handled. */ protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode, int movementMetaState, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { return left(widget, buffer); } else if (KeyEvent.metaStateHasModifiers(movementMetaState, KeyEvent.META_CTRL_ON)) { return leftWord(widget, buffer); } else if (KeyEvent.metaStateHasModifiers(movementMetaState, KeyEvent.META_ALT_ON)) { return lineStart(widget, buffer); } break; case KeyEvent.KEYCODE_DPAD_RIGHT: if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { return right(widget, buffer); } else if (KeyEvent.metaStateHasModifiers(movementMetaState, KeyEvent.META_CTRL_ON)) { return rightWord(widget, buffer); } else if (KeyEvent.metaStateHasModifiers(movementMetaState, KeyEvent.META_ALT_ON)) { return lineEnd(widget, buffer); } break; case KeyEvent.KEYCODE_DPAD_UP: if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { return up(widget, buffer); } else if (KeyEvent.metaStateHasModifiers(movementMetaState, KeyEvent.META_ALT_ON)) { return top(widget, buffer); } break; case KeyEvent.KEYCODE_DPAD_DOWN: if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { return down(widget, buffer); } else if (KeyEvent.metaStateHasModifiers(movementMetaState, KeyEvent.META_ALT_ON)) { return bottom(widget, buffer); } break; case KeyEvent.KEYCODE_PAGE_UP: if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { return pageUp(widget, buffer); } else if (KeyEvent.metaStateHasModifiers(movementMetaState, KeyEvent.META_ALT_ON)) { return top(widget, buffer); } break; case KeyEvent.KEYCODE_PAGE_DOWN: if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { return pageDown(widget, buffer); } else if (KeyEvent.metaStateHasModifiers(movementMetaState, KeyEvent.META_ALT_ON)) { return bottom(widget, buffer); } break; case KeyEvent.KEYCODE_MOVE_HOME: if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { return home(widget, buffer); } else if (KeyEvent.metaStateHasModifiers(movementMetaState, KeyEvent.META_CTRL_ON)) { return top(widget, buffer); } break; case KeyEvent.KEYCODE_MOVE_END: if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { return end(widget, buffer); } else if (KeyEvent.metaStateHasModifiers(movementMetaState, KeyEvent.META_CTRL_ON)) { return bottom(widget, buffer); } break; } return false; } /** * Performs a left movement action. * Moves the cursor or scrolls left by one character. * * @param widget The text view. * @param buffer The text buffer. * @return True if the event was handled. */ protected boolean left(TextView widget, Spannable buffer) { return false; } /** * Performs a right movement action. * Moves the cursor or scrolls right by one character. * * @param widget The text view. * @param buffer The text buffer. * @return True if the event was handled. */ protected boolean right(TextView widget, Spannable buffer) { return false; } /** * Performs an up movement action. * Moves the cursor or scrolls up by one line. * * @param widget The text view. * @param buffer The text buffer. * @return True if the event was handled. */ protected boolean up(TextView widget, Spannable buffer) { return false; } /** * Performs a down movement action. * Moves the cursor or scrolls down by one line. * * @param widget The text view. * @param buffer The text buffer. * @return True if the event was handled. */ protected boolean down(TextView widget, Spannable buffer) { return false; } /** * Performs a page-up movement action. * Moves the cursor or scrolls up by one page. * * @param widget The text view. * @param buffer The text buffer. * @return True if the event was handled. */ protected boolean pageUp(TextView widget, Spannable buffer) { return false; } /** * Performs a page-down movement action. * Moves the cursor or scrolls down by one page. * * @param widget The text view. * @param buffer The text buffer. * @return True if the event was handled. */ protected boolean pageDown(TextView widget, Spannable buffer) { return false; } /** * Performs a top movement action. * Moves the cursor or scrolls to the top of the buffer. * * @param widget The text view. * @param buffer The text buffer. * @return True if the event was handled. */ protected boolean top(TextView widget, Spannable buffer) { return false; } /** * Performs a bottom movement action. * Moves the cursor or scrolls to the bottom of the buffer. * * @param widget The text view. * @param buffer The text buffer. * @return True if the event was handled. */ protected boolean bottom(TextView widget, Spannable buffer) { return false; } /** * Performs a line-start movement action. * Moves the cursor or scrolls to the start of the line. * * @param widget The text view. * @param buffer The text buffer. * @return True if the event was handled. */ protected boolean lineStart(TextView widget, Spannable buffer) { return false; } /** * Performs a line-end movement action. * Moves the cursor or scrolls to the end of the line. * * @param widget The text view. * @param buffer The text buffer. * @return True if the event was handled. */ protected boolean lineEnd(TextView widget, Spannable buffer) { return false; } /** {@hide} */ protected boolean leftWord(TextView widget, Spannable buffer) { return false; } /** {@hide} */ protected boolean rightWord(TextView widget, Spannable buffer) { return false; } /** * Performs a home movement action. * Moves the cursor or scrolls to the start of the line or to the top of the * document depending on whether the insertion point is being moved or * the document is being scrolled. * * @param widget The text view. * @param buffer The text buffer. * @return True if the event was handled. */ protected boolean home(TextView widget, Spannable buffer) { return false; } /** * Performs an end movement action. * Moves the cursor or scrolls to the start of the line or to the top of the * document depending on whether the insertion point is being moved or * the document is being scrolled. * * @param widget The text view. * @param buffer The text buffer. * @return True if the event was handled. */ protected boolean end(TextView widget, Spannable buffer) { return false; } private int getTopLine(TextView widget) { return widget.getLayout().getLineForVertical(widget.getScrollY()); } private int getBottomLine(TextView widget) { return widget.getLayout().getLineForVertical(widget.getScrollY() + getInnerHeight(widget)); } private int getInnerWidth(TextView widget) { return widget.getWidth() - widget.getTotalPaddingLeft() - widget.getTotalPaddingRight(); } private int getInnerHeight(TextView widget) { return widget.getHeight() - widget.getTotalPaddingTop() - widget.getTotalPaddingBottom(); } private int getCharacterWidth(TextView widget) { return (int) Math.ceil(widget.getPaint().getFontSpacing()); } private int getScrollBoundsLeft(TextView widget) { final Layout layout = widget.getLayout(); final int topLine = getTopLine(widget); final int bottomLine = getBottomLine(widget); if (topLine > bottomLine) { return 0; } int left = Integer.MAX_VALUE; for (int line = topLine; line <= bottomLine; line++) { final int lineLeft = (int) Math.floor(layout.getLineLeft(line)); if (lineLeft < left) { left = lineLeft; } } return left; } private int getScrollBoundsRight(TextView widget) { final Layout layout = widget.getLayout(); final int topLine = getTopLine(widget); final int bottomLine = getBottomLine(widget); if (topLine > bottomLine) { return 0; } int right = Integer.MIN_VALUE; for (int line = topLine; line <= bottomLine; line++) { final int lineRight = (int) Math.ceil(layout.getLineRight(line)); if (lineRight > right) { right = lineRight; } } return right; } /** * Performs a scroll left action. * Scrolls left by the specified number of characters. * * @param widget The text view. * @param buffer The text buffer. * @param amount The number of characters to scroll by. Must be at least 1. * @return True if the event was handled. * @hide */ protected boolean scrollLeft(TextView widget, Spannable buffer, int amount) { final int minScrollX = getScrollBoundsLeft(widget); int scrollX = widget.getScrollX(); if (scrollX > minScrollX) { scrollX = Math.max(scrollX - getCharacterWidth(widget) * amount, minScrollX); widget.scrollTo(scrollX, widget.getScrollY()); return true; } return false; } /** * Performs a scroll right action. * Scrolls right by the specified number of characters. * * @param widget The text view. * @param buffer The text buffer. * @param amount The number of characters to scroll by. Must be at least 1. * @return True if the event was handled. * @hide */ protected boolean scrollRight(TextView widget, Spannable buffer, int amount) { final int maxScrollX = getScrollBoundsRight(widget) - getInnerWidth(widget); int scrollX = widget.getScrollX(); if (scrollX < maxScrollX) { scrollX = Math.min(scrollX + getCharacterWidth(widget) * amount, maxScrollX); widget.scrollTo(scrollX, widget.getScrollY()); return true; } return false; } /** * Performs a scroll up action. * Scrolls up by the specified number of lines. * * @param widget The text view. * @param buffer The text buffer. * @param amount The number of lines to scroll by. Must be at least 1. * @return True if the event was handled. * @hide */ protected boolean scrollUp(TextView widget, Spannable buffer, int amount) { final Layout layout = widget.getLayout(); final int top = widget.getScrollY(); int topLine = layout.getLineForVertical(top); if (layout.getLineTop(topLine) == top) { // If the top line is partially visible, bring it all the way // into view; otherwise, bring the previous line into view. topLine -= 1; } if (topLine >= 0) { topLine = Math.max(topLine - amount + 1, 0); Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(topLine)); return true; } return false; } /** * Performs a scroll down action. * Scrolls down by the specified number of lines. * * @param widget The text view. * @param buffer The text buffer. * @param amount The number of lines to scroll by. Must be at least 1. * @return True if the event was handled. * @hide */ protected boolean scrollDown(TextView widget, Spannable buffer, int amount) { final Layout layout = widget.getLayout(); final int innerHeight = getInnerHeight(widget); final int bottom = widget.getScrollY() + innerHeight; int bottomLine = layout.getLineForVertical(bottom); if (layout.getLineTop(bottomLine + 1) < bottom + 1) { // Less than a pixel of this line is out of view, // so we must have tried to make it entirely in view // and now want the next line to be in view instead. bottomLine += 1; } final int limit = layout.getLineCount() - 1; if (bottomLine <= limit) { bottomLine = Math.min(bottomLine + amount - 1, limit); Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(bottomLine + 1) - innerHeight); return true; } return false; } /** * Performs a scroll page up action. * Scrolls up by one page. * * @param widget The text view. * @param buffer The text buffer. * @return True if the event was handled. * @hide */ protected boolean scrollPageUp(TextView widget, Spannable buffer) { final Layout layout = widget.getLayout(); final int top = widget.getScrollY() - getInnerHeight(widget); int topLine = layout.getLineForVertical(top); if (topLine >= 0) { Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(topLine)); return true; } return false; } /** * Performs a scroll page up action. * Scrolls down by one page. * * @param widget The text view. * @param buffer The text buffer. * @return True if the event was handled. * @hide */ protected boolean scrollPageDown(TextView widget, Spannable buffer) { final Layout layout = widget.getLayout(); final int innerHeight = getInnerHeight(widget); final int bottom = widget.getScrollY() + innerHeight + innerHeight; int bottomLine = layout.getLineForVertical(bottom); if (bottomLine <= layout.getLineCount() - 1) { Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(bottomLine + 1) - innerHeight); return true; } return false; } /** * Performs a scroll to top action. * Scrolls to the top of the document. * * @param widget The text view. * @param buffer The text buffer. * @return True if the event was handled. * @hide */ protected boolean scrollTop(TextView widget, Spannable buffer) { final Layout layout = widget.getLayout(); if (getTopLine(widget) >= 0) { Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(0)); return true; } return false; } /** * Performs a scroll to bottom action. * Scrolls to the bottom of the document. * * @param widget The text view. * @param buffer The text buffer. * @return True if the event was handled. * @hide */ protected boolean scrollBottom(TextView widget, Spannable buffer) { final Layout layout = widget.getLayout(); final int lineCount = layout.getLineCount(); if (getBottomLine(widget) <= lineCount - 1) { Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(lineCount) - getInnerHeight(widget)); return true; } return false; } /** * Performs a scroll to line start action. * Scrolls to the start of the line. * * @param widget The text view. * @param buffer The text buffer. * @return True if the event was handled. * @hide */ protected boolean scrollLineStart(TextView widget, Spannable buffer) { final int minScrollX = getScrollBoundsLeft(widget); int scrollX = widget.getScrollX(); if (scrollX > minScrollX) { widget.scrollTo(minScrollX, widget.getScrollY()); return true; } return false; } /** * Performs a scroll to line end action. * Scrolls to the end of the line. * * @param widget The text view. * @param buffer The text buffer. * @return True if the event was handled. * @hide */ protected boolean scrollLineEnd(TextView widget, Spannable buffer) { final int maxScrollX = getScrollBoundsRight(widget) - getInnerWidth(widget); int scrollX = widget.getScrollX(); if (scrollX < maxScrollX) { widget.scrollTo(maxScrollX, widget.getScrollY()); return true; } return false; } }