diff options
7 files changed, 276 insertions, 14 deletions
diff --git a/packages/DocumentsUI/res/values/colors.xml b/packages/DocumentsUI/res/values/colors.xml index c868d340e901..23b6c9a99382 100644 --- a/packages/DocumentsUI/res/values/colors.xml +++ b/packages/DocumentsUI/res/values/colors.xml @@ -25,6 +25,7 @@ <color name="primary_dark">@*android:color/primary_dark_material_dark</color> <color name="primary">@*android:color/material_blue_grey_900</color> <color name="accent">@*android:color/accent_material_light</color> + <color name="accent_dark">@*android:color/accent_material_dark</color> <color name="action_mode">@color/material_grey_400</color> <color name="band_select_background">#88ffffff</color> diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java index f8735b2f99a1..b32104d6f469 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java @@ -267,13 +267,13 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi mSelectionManager.addCallback(selectionListener); - // Make sure this is done after the RecyclerView is set up. - mFocusManager = new FocusManager(mRecView); - mModel = new Model(); mModel.addUpdateListener(mAdapter); mModel.addUpdateListener(mModelUpdateListener); + // Make sure this is done after the RecyclerView is set up. + mFocusManager = new FocusManager(context, mRecView, mModel); + mType = getArguments().getInt(EXTRA_TYPE); mTuner = FragmentTuner.pick(getContext(), state); diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/FocusManager.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/FocusManager.java index 93ec8426e74f..8dec4ed93e90 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/FocusManager.java +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/FocusManager.java @@ -16,13 +16,28 @@ package com.android.documentsui.dirlist; +import static com.android.documentsui.model.DocumentInfo.getCursorString; + +import android.content.Context; +import android.provider.DocumentsContract.Document; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; +import android.text.Editable; +import android.text.Spannable; +import android.text.method.KeyListener; +import android.text.method.TextKeyListener; +import android.text.method.TextKeyListener.Capitalize; +import android.text.style.BackgroundColorSpan; import android.util.Log; import android.view.KeyEvent; import android.view.View; +import android.widget.TextView; import com.android.documentsui.Events; +import com.android.documentsui.R; + +import java.util.ArrayList; +import java.util.List; /** * A class that handles navigation and focus within the DirectoryFragment. @@ -31,15 +46,21 @@ class FocusManager implements View.OnFocusChangeListener { private static final String TAG = "FocusManager"; private RecyclerView mView; - private RecyclerView.Adapter<?> mAdapter; + private DocumentsAdapter mAdapter; private GridLayoutManager mLayout; + private TitleSearchHelper mSearchHelper; + private Model mModel; + private int mLastFocusPosition = RecyclerView.NO_POSITION; - public FocusManager(RecyclerView view) { + public FocusManager(Context context, RecyclerView view, Model model) { mView = view; - mAdapter = view.getAdapter(); + mAdapter = (DocumentsAdapter) view.getAdapter(); mLayout = (GridLayoutManager) view.getLayoutManager(); + mModel = model; + + mSearchHelper = new TitleSearchHelper(context); } /** @@ -52,7 +73,11 @@ class FocusManager implements View.OnFocusChangeListener { * @return Whether the event was handled. */ public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) { - boolean extendSelection = false; + // Search helper gets first crack, for doing type-to-focus. + if (mSearchHelper.handleKey(doc, keyCode, event)) { + return true; + } + // Translate space/shift-space into PgDn/PgUp if (keyCode == KeyEvent.KEYCODE_SPACE) { if (event.isShiftPressed()) { @@ -60,8 +85,6 @@ class FocusManager implements View.OnFocusChangeListener { } else { keyCode = KeyEvent.KEYCODE_PAGE_DOWN; } - } else { - extendSelection = event.isShiftPressed(); } if (Events.isNavigationKeyCode(keyCode)) { @@ -229,7 +252,6 @@ class FocusManager implements View.OnFocusChangeListener { if (vh != null) { vh.itemView.requestFocus(); } else { - mView.smoothScrollToPosition(pos); // Set a one-time listener to request focus when the scroll has completed. mView.addOnScrollListener( new RecyclerView.OnScrollListener() { @@ -251,6 +273,7 @@ class FocusManager implements View.OnFocusChangeListener { } } }); + mView.smoothScrollToPosition(pos); } } @@ -260,4 +283,239 @@ class FocusManager implements View.OnFocusChangeListener { private boolean inGridMode() { return mLayout.getSpanCount() > 1; } + + /** + * A helper class for handling type-to-focus. Instantiate this class, and pass it KeyEvents via + * the {@link #handleKey(DocumentHolder, int, KeyEvent)} method. The class internally will build + * up a string from individual key events, and perform searching based on that string. When an + * item is found that matches the search term, that item will be focused. This class also + * highlights instances of the search term found in the view. + */ + private class TitleSearchHelper { + final private KeyListener mTextListener = new TextKeyListener(Capitalize.NONE, false); + final private Editable mSearchString = Editable.Factory.getInstance().newEditable(""); + final private Highlighter mHighlighter = new Highlighter(); + final private BackgroundColorSpan mSpan; + private List<String> mIndex; + private boolean mActive; + + public TitleSearchHelper(Context context) { + mSpan = new BackgroundColorSpan(context.getColor(R.color.accent_dark)); + } + + /** + * Handles alphanumeric keystrokes for type-to-focus. This method builds a search term out + * of individual key events, and then performs a search for the given string. + * + * @param doc The document holder receiving the key event. + * @param keyCode + * @param event + * @return Whether the event was handled. + */ + public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_ESCAPE: + case KeyEvent.KEYCODE_ENTER: + if (mActive) { + // These keys end any active searches. + deactivate(); + return true; + } else { + // Don't handle these key events if there is no active search. + return false; + } + case KeyEvent.KEYCODE_SPACE: + // This allows users to search for files with spaces in their names, but ignores + // spacebar events when a text search is not active. + if (!mActive) { + return false; + } + } + + // Navigation keys also end active searches. + if (Events.isNavigationKeyCode(keyCode)) { + deactivate(); + // Don't handle the keycode, so navigation still occurs. + return false; + } + + // Build up the search string, and perform the search. + boolean handled = mTextListener.onKeyDown(doc.itemView, mSearchString, keyCode, event); + + // Delete is processed by the text listener, but not "handled". Check separately for it. + if (handled || keyCode == KeyEvent.KEYCODE_DEL) { + String searchString = mSearchString.toString(); + if (searchString.length() == 0) { + // Don't perform empty searches. + return false; + } + activate(); + for (int pos = 0; pos < mIndex.size(); pos++) { + String title = mIndex.get(pos); + if (title != null && title.startsWith(searchString)) { + focusItem(pos); + break; + } + } + } + + return handled; + } + + /** + * Activates the search helper, which changes its key handling and updates the search index + * and highlights if necessary. Call this each time the search term is updated. + */ + private void activate() { + if (!mActive) { + // Install listeners. + mModel.addUpdateListener(mModelListener); + } + + // If the search index was invalidated, rebuild it + if (mIndex == null) { + buildIndex(); + } + + // TODO: Uncomment this to enable search term highlighting in the UI. +// mHighlighter.activate(); + + mActive = true; + } + + /** + * Deactivates the search helper (see {@link #activate()}). Call this when a search ends. + */ + private void deactivate() { + if (mActive) { + // Remove listeners. + mModel.removeUpdateListener(mModelListener); + } + + // TODO: Uncomment this when search-term highlighting is enabled in the UI. +// mHighlighter.deactivate(); + + mIndex = null; + mSearchString.clear(); + mActive = false; + } + + /** + * Applies title highlights to the given view. The view must have a title field that is a + * spannable text field. If this condition is not met, this function does nothing. + * + * @param view + */ + private void applyHighlight(View view) { + TextView titleView = (TextView) view.findViewById(android.R.id.title); + if (titleView == null) { + return; + } + + String searchString = mSearchString.toString(); + CharSequence tmpText = titleView.getText(); + if (tmpText instanceof Spannable) { + Spannable title = (Spannable) tmpText; + String titleString = title.toString(); + if (titleString.startsWith(searchString)) { + title.setSpan(mSpan, 0, searchString.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } else { + title.removeSpan(mSpan); + } + } + } + + /** + * Removes title highlights from the given view. The view must have a title field that is a + * spannable text field. If this condition is not met, this function does nothing. + * + * @param view + */ + private void removeHighlight(View view) { + TextView titleView = (TextView) view.findViewById(android.R.id.title); + if (titleView == null) { + return; + } + + CharSequence tmpText = titleView.getText(); + if (tmpText instanceof Spannable) { + ((Spannable) tmpText).removeSpan(mSpan); + } + } + + /** + * Builds a search index for finding items by title. Queries the model and adapter, so both + * must be set up before calling this method. + */ + private void buildIndex() { + int itemCount = mAdapter.getItemCount(); + List<String> index = new ArrayList<>(itemCount); + for (int i = 0; i < itemCount; i++) { + String modelId = mAdapter.getModelId(i); + if (modelId != null) { + index.add( + getCursorString(mModel.getItem(modelId), Document.COLUMN_DISPLAY_NAME)); + } else { + index.add(""); + } + } + mIndex = index; + } + + private Model.UpdateListener mModelListener = new Model.UpdateListener() { + @Override + public void onModelUpdate(Model model) { + // Invalidate the search index when the model updates. + mIndex = null; + } + + @Override + public void onModelUpdateFailed(Exception e) { + // Invalidate the search index when the model updates. + mIndex = null; + } + }; + + private class Highlighter implements RecyclerView.OnChildAttachStateChangeListener { + /** + * Starts highlighting instances of the current search term in the UI. + */ + public void activate() { + // Update highlights on all views + int itemCount = mView.getChildCount(); + for (int i = 0; i < itemCount; i++) { + applyHighlight(mView.getChildAt(i)); + } + // Keep highlights up-to-date as items come in and out of view. + mView.addOnChildAttachStateChangeListener(this); + } + + /** + * Stops highlighting instances of the current search term in the UI. + */ + public void deactivate() { + // Remove highlights on all views + int itemCount = mView.getChildCount(); + for (int i = 0; i < itemCount; i++) { + removeHighlight(mView.getChildAt(i)); + } + // Stop updating highlights. + mView.removeOnChildAttachStateChangeListener(this); + } + + @Override + public void onChildViewAttachedToWindow(View view) { + applyHighlight(view); + } + + @Override + public void onChildViewDetachedFromWindow(View view) { + TextView titleView = (TextView) view.findViewById(android.R.id.title); + if (titleView != null) { + removeHighlight(titleView); + } + } + }; + } } diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/GridDocumentHolder.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/GridDocumentHolder.java index 055adc6a2fb1..8eaed17e8676 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/GridDocumentHolder.java +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/GridDocumentHolder.java @@ -32,7 +32,6 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; -import com.android.documentsui.IconUtils; import com.android.documentsui.R; import com.android.documentsui.RootCursorWrapper; import com.android.documentsui.Shared; @@ -107,7 +106,7 @@ final class GridDocumentHolder extends DocumentHolder { if (mHideTitles) { mTitle.setVisibility(View.GONE); } else { - mTitle.setText(docDisplayName); + mTitle.setText(docDisplayName, TextView.BufferType.SPANNABLE); mTitle.setVisibility(View.VISIBLE); } diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/ListDocumentHolder.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/ListDocumentHolder.java index 8c3b53c75da6..be6413bfbf6c 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/ListDocumentHolder.java +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/ListDocumentHolder.java @@ -103,7 +103,7 @@ final class ListDocumentHolder extends DocumentHolder { final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId); mIconHelper.loadThumbnail(uri, docMimeType, docFlags, docIcon, mIconThumb, mIconMime, null); - mTitle.setText(docDisplayName); + mTitle.setText(docDisplayName, TextView.BufferType.SPANNABLE); mTitle.setVisibility(View.VISIBLE); if (docSummary != null) { diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/Model.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/Model.java index 075b3ea9cdfe..490f94eacfc2 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/Model.java +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/Model.java @@ -400,6 +400,10 @@ public class Model implements SiblingProvider { mUpdateListeners.add(listener); } + void removeUpdateListener(UpdateListener listener) { + mUpdateListeners.remove(listener); + } + static interface UpdateListener { /** * Called when a successful update has occurred. diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java index 69a67111ca38..dd27790ace33 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java @@ -182,7 +182,7 @@ final class ModelBackedDocumentsAdapter extends DocumentsAdapter { @Override public void unhide(SparseArray<String> ids) { - if (DEBUG) Log.d(TAG, "Un-iding ids: " + ids); + if (DEBUG) Log.d(TAG, "Unhiding ids: " + ids); // An ArrayList can shrink at runtime...and in fact // it does when we clear it completely. |