summaryrefslogtreecommitdiffstats
path: root/core/java/android/widget/AbsListView.java
diff options
context:
space:
mode:
authorRomain Guy <romainguy@android.com>2009-05-21 23:10:10 -0700
committerRomain Guy <romainguy@android.com>2009-05-22 01:59:59 -0700
commitd6a463a9f23b3901bf729f2f27a6bb8f78b95248 (patch)
tree1371cafd6a1c0fe8d3cd4580e7878a9adb86b183 /core/java/android/widget/AbsListView.java
parentcfcc0df2658d0ce7dc753511bb44ab8ae7a636f7 (diff)
downloadframeworks_base-d6a463a9f23b3901bf729f2f27a6bb8f78b95248.zip
frameworks_base-d6a463a9f23b3901bf729f2f27a6bb8f78b95248.tar.gz
frameworks_base-d6a463a9f23b3901bf729f2f27a6bb8f78b95248.tar.bz2
Add a new API to ListView: setGestures(int). This allows developers to enable gestures to jump inside the list or filter it. This change also introduces a new XML attribute to control this API. It also adds the ability to theme the GestureOverlayView from the gestures library. Finally, this adds a new VERSION header to the binary format used to store the letters for the recognizer.
Diffstat (limited to 'core/java/android/widget/AbsListView.java')
-rw-r--r--core/java/android/widget/AbsListView.java389
1 files changed, 363 insertions, 26 deletions
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index 1ca59b2..8a538d7 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -41,12 +41,18 @@ import android.view.ViewConfiguration;
import android.view.ViewDebug;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
+import android.view.KeyCharacterMap;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputConnectionWrapper;
import android.view.inputmethod.InputMethodManager;
import android.view.ContextMenu.ContextMenuInfo;
+import android.gesture.GestureOverlayView;
+import android.gesture.TouchThroughGestureListener;
+import android.gesture.Gesture;
+import android.gesture.LetterRecognizer;
+import android.gesture.Prediction;
import com.android.internal.R;
@@ -54,7 +60,9 @@ import java.util.ArrayList;
import java.util.List;
/**
- * Common code shared between ListView and GridView
+ * Base class that can be used to implement virtualized lists of items. A list does
+ * not have a spatial definition here. For instance, subclases of this class can
+ * display the content of the list in a grid, in a carousel, as stack, etc.
*
* @attr ref android.R.styleable#AbsListView_listSelector
* @attr ref android.R.styleable#AbsListView_drawSelectorOnTop
@@ -65,6 +73,7 @@ import java.util.List;
* @attr ref android.R.styleable#AbsListView_cacheColorHint
* @attr ref android.R.styleable#AbsListView_fastScrollEnabled
* @attr ref android.R.styleable#AbsListView_smoothScrollbar
+ * @attr ref android.R.styleable#AbsListView_gestures
*/
public abstract class AbsListView extends AdapterView<ListAdapter> implements TextWatcher,
ViewTreeObserver.OnGlobalLayoutListener, Filter.FilterListener,
@@ -93,6 +102,31 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
public static final int TRANSCRIPT_MODE_ALWAYS_SCROLL = 2;
/**
+ * Disables gestures.
+ *
+ * @see #setGestures(int)
+ * @see #GESTURES_JUMP
+ * @see #GESTURES_FILTER
+ */
+ public static final int GESTURES_NONE = 0;
+ /**
+ * When a letter gesture is recognized the list jumps to a matching position.
+ *
+ * @see #setGestures(int)
+ * @see #GESTURES_NONE
+ * @see #GESTURES_FILTER
+ */
+ public static final int GESTURES_JUMP = 1;
+ /**
+ * When a letter gesture is recognized the letter is added to the filter.
+ *
+ * @see #setGestures(int)
+ * @see #GESTURES_NONE
+ * @see #GESTURES_JUMP
+ */
+ public static final int GESTURES_FILTER = 2;
+
+ /**
* Indicates that we are not in the middle of a touch gesture
*/
static final int TOUCH_MODE_REST = -1;
@@ -427,8 +461,23 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
*/
private FastScroller mFastScroller;
- private int mTouchSlop;
+ /**
+ * Indicates the type of gestures to use: GESTURES_NONE, GESTURES_FILTER or GESTURES_NONE
+ */
+ private int mGestures;
+
+ // Used to implement the gestures overlay
+ private GestureOverlayView mGesturesOverlay;
+ private PopupWindow mGesturesPopup;
+ private ViewTreeObserver.OnGlobalLayoutListener mGesturesLayoutListener;
+ private boolean mGlobalLayoutListenerAddedGestures;
+ private boolean mInstallGesturesOverlay;
+ private TouchThroughGestureListener mGesturesListener;
+ private boolean mPreviousGesturing;
+
+ private boolean mGlobalLayoutListenerAddedFilter;
+ private int mTouchSlop;
private float mDensityScale;
private InputConnection mDefInputConnection;
@@ -535,10 +584,197 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
boolean smoothScrollbar = a.getBoolean(R.styleable.AbsListView_smoothScrollbar, true);
setSmoothScrollbarEnabled(smoothScrollbar);
-
+
+ int defaultGestures = GESTURES_NONE;
+ if (useTextFilter) {
+ defaultGestures = GESTURES_FILTER;
+ } else if (enableFastScroll) {
+ defaultGestures = GESTURES_JUMP;
+ }
+ int gestures = a.getInt(R.styleable.AbsListView_gestures, defaultGestures);
+ setGestures(gestures);
+
a.recycle();
}
+ private void initAbsListView() {
+ // Setting focusable in touch mode will set the focusable property to true
+ setFocusableInTouchMode(true);
+ setWillNotDraw(false);
+ setAlwaysDrawnWithCacheEnabled(false);
+ setScrollingCacheEnabled(true);
+
+ mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
+ mDensityScale = getContext().getResources().getDisplayMetrics().density;
+ }
+
+ /**
+ * <p>Sets the type of gestures to use with this list. When gestures are enabled,
+ * that is if the <code>gestures</code> parameter is not {@link #GESTURES_NONE},
+ * the user can draw characters on top of this view. When a character is
+ * recognized and matches a known character, the list will either:</p>
+ * <ul>
+ * <li>Jump to the appropriate position ({@link #GESTURES_JUMP})</li>
+ * <li>Add the character to the current filter ({@link #GESTURES_FILTER})</li>
+ * </ul>
+ * <p>Using {@link #GESTURES_JUMP} requires {@link #isFastScrollEnabled()} to
+ * be true. Using {@link #GESTURES_FILTER} requires {@link #isTextFilterEnabled()}
+ * to be true.</p>
+ *
+ * @param gestures The type of gestures to enable for this list:
+ * {@link #GESTURES_NONE}, {@link #GESTURES_JUMP} or {@link #GESTURES_FILTER}
+ *
+ * @see #GESTURES_NONE
+ * @see #GESTURES_JUMP
+ * @see #GESTURES_FILTER
+ * @see #getGestures()
+ */
+ public void setGestures(int gestures) {
+ switch (gestures) {
+ case GESTURES_JUMP:
+ if (!mFastScrollEnabled) {
+ throw new IllegalStateException("Jump gestures can only be used with "
+ + "fast scroll enabled");
+ }
+ break;
+ case GESTURES_FILTER:
+ if (!mTextFilterEnabled) {
+ throw new IllegalStateException("Filter gestures can only be used with "
+ + "text filtering enabled");
+ }
+ break;
+ }
+
+ final int oldGestures = mGestures;
+ mGestures = gestures;
+
+ // Install overlay later
+ if (oldGestures == GESTURES_NONE && gestures != GESTURES_NONE) {
+ mInstallGesturesOverlay = true;
+ // Uninstall overlay
+ } else if (oldGestures != GESTURES_NONE && gestures == GESTURES_NONE) {
+ uninstallGesturesOverlay();
+ }
+ }
+
+ /**
+ * Indicates what gestures are enabled on this view.
+ *
+ * @return {@link #GESTURES_NONE}, {@link #GESTURES_JUMP} or {@link #GESTURES_FILTER}
+ *
+ * @see #GESTURES_NONE
+ * @see #GESTURES_JUMP
+ * @see #GESTURES_FILTER
+ * @see #setGestures(int)
+ */
+ @ViewDebug.ExportedProperty(mapping = {
+ @ViewDebug.IntToString(from = GESTURES_NONE, to = "NONE"),
+ @ViewDebug.IntToString(from = GESTURES_JUMP, to = "JUMP"),
+ @ViewDebug.IntToString(from = GESTURES_FILTER, to = "FILTER")
+ })
+ public int getGestures() {
+ return mGestures;
+ }
+
+ private void dismissGesturesPopup() {
+ if (mGesturesPopup != null) {
+ mGesturesPopup.dismiss();
+ }
+ }
+
+ private void showGesturesPopup() {
+ // Make sure we have a window before showing the popup
+ if (getWindowVisibility() == View.VISIBLE) {
+ installGesturesOverlay();
+ positionGesturesPopup();
+ }
+ }
+
+ private void positionGesturesPopup() {
+ final int[] xy = new int[2];
+ getLocationOnScreen(xy);
+ if (!mGesturesPopup.isShowing()) {
+ mGesturesPopup.showAtLocation(this, Gravity.LEFT | Gravity.TOP, xy[0], xy[1]);
+ } else {
+ mGesturesPopup.update(xy[0], xy[1], -1, -1);
+ }
+ }
+
+ private void installGesturesOverlay() {
+ mInstallGesturesOverlay = false;
+
+ if (mGesturesPopup == null) {
+ final Context c = getContext();
+ final LayoutInflater layoutInflater = (LayoutInflater)
+ c.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mGesturesOverlay = (GestureOverlayView)
+ layoutInflater.inflate(R.layout.list_gestures_overlay, null);
+
+ final PopupWindow p = new PopupWindow(c);
+ p.setFocusable(false);
+ p.setTouchable(false);
+ p.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
+ p.setContentView(mGesturesOverlay);
+ p.setWidth(getWidth());
+ p.setHeight(getHeight());
+ p.setBackgroundDrawable(null);
+
+ if (mGesturesLayoutListener == null) {
+ mGesturesLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
+ public void onGlobalLayout() {
+ if (isShown()) {
+ showGesturesPopup();
+ } else if (mGesturesPopup.isShowing()) {
+ dismissGesturesPopup();
+ }
+ }
+ };
+ }
+ getViewTreeObserver().addOnGlobalLayoutListener(mGesturesLayoutListener);
+ mGlobalLayoutListenerAddedGestures = true;
+
+ mGesturesPopup = p;
+
+ mGesturesOverlay.removeAllOnGestureListeners();
+ mGesturesListener = new TouchThroughGestureListener(null);
+ mGesturesListener.setGestureType(TouchThroughGestureListener.MULTIPLE_STROKE);
+ mGesturesListener.addOnGestureActionListener(new GesturesProcessor());
+ mGesturesOverlay.addOnGestureListener(mGesturesListener);
+
+ mPreviousGesturing = false;
+ }
+ }
+
+ private void uninstallGesturesOverlay() {
+ dismissGesturesPopup();
+ mGesturesPopup = null;
+ if (mGesturesLayoutListener != null) {
+ getViewTreeObserver().removeGlobalOnLayoutListener(mGesturesLayoutListener);
+ }
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ if (mGestures != GESTURES_NONE) {
+ mGesturesOverlay.processEvent(ev);
+
+ final boolean isGesturing = mGesturesListener.isGesturing();
+
+ if (!isGesturing) {
+ mPreviousGesturing = isGesturing;
+ return super.dispatchTouchEvent(ev);
+ } else if (!mPreviousGesturing){
+ mPreviousGesturing = isGesturing;
+ final MotionEvent event = MotionEvent.obtain(ev);
+ event.setAction(MotionEvent.ACTION_CANCEL);
+ super.dispatchTouchEvent(event);
+ return true;
+ }
+ }
+
+ return super.dispatchTouchEvent(ev);
+ }
+
/**
* Enables fast scrolling by letting the user quickly scroll through lists by
* dragging the fast scroll thumb. The adapter attached to the list may want
@@ -712,17 +948,6 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
}
}
- private void initAbsListView() {
- // Setting focusable in touch mode will set the focusable property to true
- setFocusableInTouchMode(true);
- setWillNotDraw(false);
- setAlwaysDrawnWithCacheEnabled(false);
- setScrollingCacheEnabled(true);
-
- mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
- mDensityScale = getContext().getResources().getDisplayMetrics().density;
- }
-
private void useDefaultSelector() {
setSelector(getResources().getDrawable(
com.android.internal.R.drawable.list_selector_background));
@@ -908,11 +1133,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
}
private boolean acceptFilter() {
- if (!mTextFilterEnabled || !(getAdapter() instanceof Filterable) ||
- ((Filterable) getAdapter()).getFilter() == null) {
- return false;
- }
- return true;
+ return mTextFilterEnabled && getAdapter() instanceof Filterable &&
+ ((Filterable) getAdapter()).getFilter() != null;
}
/**
@@ -1096,6 +1318,10 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
listPadding.bottom = mSelectionBottomPadding + mPaddingBottom;
}
+ /**
+ * Subclasses should NOT override this method but
+ * {@link #layoutChildren()} instead.
+ */
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
@@ -1111,17 +1337,27 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
protected boolean setFrame(int left, int top, int right, int bottom) {
final boolean changed = super.setFrame(left, top, right, bottom);
- // Reposition the popup when the frame has changed. This includes
- // translating the widget, not just changing its dimension. The
- // filter popup needs to follow the widget.
- if (mFiltered && changed && getWindowVisibility() == View.VISIBLE && mPopup != null &&
- mPopup.isShowing()) {
- positionPopup();
+ if (changed) {
+ // Reposition the popup when the frame has changed. This includes
+ // translating the widget, not just changing its dimension. The
+ // filter popup needs to follow the widget.
+ final boolean visible = getWindowVisibility() == View.VISIBLE;
+ if (mFiltered && visible && mPopup != null && mPopup.isShowing()) {
+ positionPopup();
+ }
+
+ if (mGestures != GESTURES_NONE && visible && mGesturesPopup != null &&
+ mGesturesPopup.isShowing()) {
+ positionGesturesPopup();
+ }
}
return changed;
}
+ /**
+ * Subclasses must override this method to layout their children.
+ */
protected void layoutChildren() {
}
@@ -1324,9 +1560,17 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
mDataChanged = true;
rememberSyncState();
}
+
if (mFastScroller != null) {
mFastScroller.onSizeChanged(w, h, oldw, oldh);
}
+
+ if (mInstallGesturesOverlay) {
+ installGesturesOverlay();
+ positionGesturesPopup();
+ } else if (mGesturesPopup != null) {
+ mGesturesPopup.update(w, h);
+ }
}
/**
@@ -1510,6 +1754,13 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
final ViewTreeObserver treeObserver = getViewTreeObserver();
if (treeObserver != null) {
treeObserver.addOnTouchModeChangeListener(this);
+ if (mTextFilterEnabled && mPopup != null && !mGlobalLayoutListenerAddedFilter) {
+ treeObserver.addOnGlobalLayoutListener(this);
+ }
+ if (mGestures != GESTURES_NONE && mGesturesPopup != null &&
+ !mGlobalLayoutListenerAddedGestures) {
+ treeObserver.addOnGlobalLayoutListener(mGesturesLayoutListener);
+ }
}
}
@@ -1520,6 +1771,14 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
final ViewTreeObserver treeObserver = getViewTreeObserver();
if (treeObserver != null) {
treeObserver.removeOnTouchModeChangeListener(this);
+ if (mTextFilterEnabled && mPopup != null) {
+ treeObserver.removeGlobalOnLayoutListener(this);
+ mGlobalLayoutListenerAddedFilter = false;
+ }
+ if (mGesturesLayoutListener != null && mGesturesPopup != null) {
+ mGlobalLayoutListenerAddedGestures = false;
+ treeObserver.removeGlobalOnLayoutListener(mGesturesLayoutListener);
+ }
}
}
@@ -1534,6 +1793,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
removeCallbacks(mFlingRunnable);
// Always hide the type filter
dismissPopup();
+ dismissGesturesPopup();
if (touchMode == TOUCH_MODE_OFF) {
// Remember the last selected element
@@ -1544,6 +1804,9 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
// Show the type filter only if a filter is in effect
showPopup();
}
+ if (mGestures != GESTURES_NONE) {
+ showGesturesPopup();
+ }
// If we changed touch mode since the last time we had focus
if (touchMode != mLastTouchMode && mLastTouchMode != TOUCH_MODE_UNKNOWN) {
@@ -2775,7 +3038,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
/**
* Removes the filter window
*/
- void dismissPopup() {
+ private void dismissPopup() {
if (mPopup != null) {
mPopup.dismiss();
}
@@ -3017,6 +3280,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
p.setBackgroundDrawable(null);
mPopup = p;
getViewTreeObserver().addOnGlobalLayoutListener(this);
+ mGlobalLayoutListenerAddedFilter = true;
}
if (animateEntrance) {
mPopup.setAnimationStyle(com.android.internal.R.style.Animation_TypingFilter);
@@ -3583,4 +3847,77 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
}
}
}
+
+ private class GesturesProcessor implements
+ TouchThroughGestureListener.OnGesturePerformedListener {
+
+ private static final double SCORE_THRESHOLD = 0.1;
+
+ private LetterRecognizer mRecognizer;
+ private ArrayList<Prediction> mPredictions;
+ private final KeyCharacterMap mKeyMap;
+ private final char[] mHolder;
+
+ GesturesProcessor() {
+ mRecognizer = LetterRecognizer.getLetterRecognizer(getContext(),
+ LetterRecognizer.RECOGNIZER_LATIN_LOWERCASE);
+ if (mGestures == GESTURES_FILTER) {
+ mKeyMap = KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD);
+ mHolder = new char[1];
+ } else {
+ mKeyMap = null;
+ mHolder = null;
+ }
+ }
+
+ public void onGesturePerformed(GestureOverlayView overlay, Gesture gesture) {
+ mPredictions = mRecognizer.recognize(gesture, mPredictions);
+ if (!mPredictions.isEmpty()) {
+ final Prediction prediction = mPredictions.get(0);
+ if (prediction.score > SCORE_THRESHOLD) {
+ switch (mGestures) {
+ case GESTURES_JUMP:
+ processJump(prediction);
+ break;
+ case GESTURES_FILTER:
+ processFilter(prediction);
+ break;
+ }
+ }
+ }
+ }
+
+ private void processJump(Prediction prediction) {
+ final Object[] sections = mFastScroller.getSections();
+ if (sections != null) {
+ final String name = prediction.name;
+ final int count = sections.length;
+
+ int index = -1;
+ for (int i = 0; i < count; i++) {
+ if (name.equalsIgnoreCase((String) sections[i])) {
+ index = i;
+ break;
+ }
+ }
+
+ if (index != -1) {
+ final SectionIndexer indexer = mFastScroller.getSectionIndexer();
+ final int position = indexer.getPositionForSection(index);
+ setSelection(position);
+ }
+ }
+ }
+
+ private void processFilter(Prediction prediction) {
+ mHolder[0] = prediction.name.charAt(0);
+ final KeyEvent[] events = mKeyMap.getEvents(mHolder);
+ if (events != null) {
+ for (KeyEvent event : events) {
+ sendToTextFilter(event.getKeyCode(), event.getRepeatCount(),
+ event);
+ }
+ }
+ }
+ }
}