diff options
author | Romain Guy <romainguy@android.com> | 2009-05-21 23:10:10 -0700 |
---|---|---|
committer | Romain Guy <romainguy@android.com> | 2009-05-22 01:59:59 -0700 |
commit | d6a463a9f23b3901bf729f2f27a6bb8f78b95248 (patch) | |
tree | 1371cafd6a1c0fe8d3cd4580e7878a9adb86b183 /core/java/android/widget/AbsListView.java | |
parent | cfcc0df2658d0ce7dc753511bb44ab8ae7a636f7 (diff) | |
download | frameworks_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.java | 389 |
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); + } + } + } + } } |