Merge "Support Ctrl-based EditText movement."

This commit is contained in:
Jeff Sharkey
2011-03-28 19:57:10 -07:00
committed by Android (Google) Code Review
4 changed files with 285 additions and 6 deletions

View File

@@ -0,0 +1,95 @@
/*
* Copyright (C) 2011 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;
import android.util.MathUtils;
import java.text.CharacterIterator;
/** {@hide} */
public class CharSequenceIterator implements CharacterIterator {
private final CharSequence mValue;
private final int mStart;
private final int mEnd;
private int mIndex;
public CharSequenceIterator(CharSequence value) {
mValue = value;
mStart = 0;
mEnd = value.length();
mIndex = 0;
}
@Override
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
/** {@inheritDoc} */
public char current() {
if (mIndex == mEnd) {
return DONE;
}
return mValue.charAt(mIndex);
}
/** {@inheritDoc} */
public int getBeginIndex() {
return mStart;
}
/** {@inheritDoc} */
public int getEndIndex() {
return mEnd;
}
/** {@inheritDoc} */
public int getIndex() {
return mIndex;
}
/** {@inheritDoc} */
public char first() {
return setIndex(mStart);
}
/** {@inheritDoc} */
public char last() {
return setIndex(mEnd - 1);
}
/** {@inheritDoc} */
public char next() {
return setIndex(mIndex + 1);
}
/** {@inheritDoc} */
public char previous() {
return setIndex(mIndex - 1);
}
/** {@inheritDoc} */
public char setIndex(int index) {
mIndex = MathUtils.constrain(index, mStart, mEnd);
return current();
}
}

View File

@@ -16,6 +16,11 @@
package android.text;
import android.util.Log;
import java.text.BreakIterator;
import java.text.CharacterIterator;
/**
* Utility class for manipulating cursors and selections in CharSequences.
@@ -38,7 +43,7 @@ public class Selection {
else
return -1;
}
/**
* Return the offset of the selection edge or cursor, or -1 if
* there is no selection or cursor.
@@ -57,7 +62,7 @@ public class Selection {
// private static int pin(int value, int min, int max) {
// return value < min ? 0 : (value > max ? max : value);
// }
/**
* Set the selection anchor to <code>start</code> and the selection edge
* to <code>stop</code>.
@@ -69,7 +74,7 @@ public class Selection {
int ostart = getSelectionStart(text);
int oend = getSelectionEnd(text);
if (ostart != start || oend != stop) {
text.setSpan(SELECTION_START, start, start,
Spanned.SPAN_POINT_POINT|Spanned.SPAN_INTERMEDIATE);
@@ -357,6 +362,42 @@ public class Selection {
return true;
}
/** {@hide} */
public static interface PositionIterator {
public static final int DONE = BreakIterator.DONE;
public int preceding(int position);
public int following(int position);
}
/** {@hide} */
public static boolean moveToPreceding(
Spannable text, PositionIterator iter, boolean extendSelection) {
final int offset = iter.preceding(getSelectionEnd(text));
if (offset != PositionIterator.DONE) {
if (extendSelection) {
extendSelection(text, offset);
} else {
setSelection(text, offset);
}
}
return true;
}
/** {@hide} */
public static boolean moveToFollowing(
Spannable text, PositionIterator iter, boolean extendSelection) {
final int offset = iter.following(getSelectionEnd(text));
if (offset != PositionIterator.DONE) {
if (extendSelection) {
extendSelection(text, offset);
} else {
setSelection(text, offset);
}
}
return true;
}
private static int findEdge(Spannable text, Layout layout, int dir) {
int pt = getSelectionEnd(text);
int line = layout.getLineForOffset(pt);
@@ -419,7 +460,7 @@ public class Selection {
private static final class START implements NoCopySpan { }
private static final class END implements NoCopySpan { }
/*
* Public constants
*/

View File

@@ -17,14 +17,23 @@
package android.text.method;
import android.graphics.Rect;
import android.text.CharSequenceIterator;
import android.text.Editable;
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextWatcher;
import android.util.Log;
import android.util.MathUtils;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;
import java.text.BreakIterator;
import java.text.CharacterIterator;
/**
* A movement method that provides cursor movement and selection.
* Supports displaying the context menu on DPad Center.
@@ -193,6 +202,20 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme
}
}
/** {@hide} */
@Override
protected boolean leftWord(TextView widget, Spannable buffer) {
mWordIterator.setCharSequence(buffer);
return Selection.moveToPreceding(buffer, mWordIterator, isSelecting(buffer));
}
/** {@hide} */
@Override
protected boolean rightWord(TextView widget, Spannable buffer) {
mWordIterator.setCharSequence(buffer);
return Selection.moveToFollowing(buffer, mWordIterator, isSelecting(buffer));
}
@Override
protected boolean home(TextView widget, Spannable buffer) {
return lineStart(widget, buffer);
@@ -205,7 +228,8 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme
@Override
public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
int initialScrollX = -1, initialScrollY = -1;
int initialScrollX = -1;
int initialScrollY = -1;
final int action = event.getAction();
if (action == MotionEvent.ACTION_UP) {
@@ -220,7 +244,7 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme
boolean cap = isSelecting(buffer);
if (cap) {
int offset = widget.getOffset((int) event.getX(), (int) event.getY());
buffer.setSpan(LAST_TAP_DOWN, offset, offset, Spannable.SPAN_POINT_POINT);
// Disallow intercepting of the touch events, so that
@@ -308,6 +332,103 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme
return sInstance;
}
/**
* Walks through cursor positions at word boundaries. Internally uses
* {@link BreakIterator#getWordInstance()}, and caches {@link CharSequence}
* for performance reasons.
*/
private static class WordIterator implements Selection.PositionIterator {
private CharSequence mCurrent;
private boolean mCurrentDirty = false;
private BreakIterator mIterator;
private TextWatcher mWatcher = new TextWatcher() {
/** {@inheritDoc} */
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// ignored
}
/** {@inheritDoc} */
public void onTextChanged(CharSequence s, int start, int before, int count) {
mCurrentDirty = true;
}
/** {@inheritDoc} */
public void afterTextChanged(Editable s) {
// ignored
}
};
public void setCharSequence(CharSequence incoming) {
if (mIterator == null) {
mIterator = BreakIterator.getWordInstance();
}
// when incoming is different object, move listeners to new sequence
// and mark as dirty so we reload contents.
if (mCurrent != incoming) {
if (mCurrent instanceof Editable) {
((Editable) mCurrent).removeSpan(mWatcher);
}
if (incoming instanceof Editable) {
((Editable) incoming).setSpan(
mWatcher, 0, incoming.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
}
mCurrent = incoming;
mCurrentDirty = true;
}
if (mCurrentDirty) {
final CharacterIterator charIterator = new CharSequenceIterator(mCurrent);
mIterator.setText(charIterator);
mCurrentDirty = false;
}
}
private boolean isValidOffset(int offset) {
return offset >= 0 && offset < mCurrent.length();
}
private boolean isLetterOrDigit(int offset) {
if (isValidOffset(offset)) {
return Character.isLetterOrDigit(mCurrent.charAt(offset));
} else {
return false;
}
}
/** {@inheritDoc} */
public int preceding(int offset) {
// always round cursor index into valid string index
offset = MathUtils.constrain(offset, 0, mCurrent.length() - 1);
do {
offset = mIterator.preceding(offset);
if (isLetterOrDigit(offset)) break;
} while (isValidOffset(offset));
return offset;
}
/** {@inheritDoc} */
public int following(int offset) {
// always round cursor index into valid string index
offset = MathUtils.constrain(offset, 0, mCurrent.length() - 1);
do {
offset = mIterator.following(offset);
if (isLetterOrDigit(offset - 1)) break;
} while (isValidOffset(offset));
return offset;
}
}
private WordIterator mWordIterator = new WordIterator();
private static final Object LAST_TAP_DOWN = new Object();
private static ArrowKeyMovementMethod sInstance;

View File

@@ -163,6 +163,9 @@ public class BaseMovementMethod implements MovementMethod {
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);
@@ -172,6 +175,9 @@ public class BaseMovementMethod implements MovementMethod {
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);
@@ -217,12 +223,18 @@ public class BaseMovementMethod implements MovementMethod {
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;
}
@@ -349,6 +361,16 @@ public class BaseMovementMethod implements MovementMethod {
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