summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/DocumentsUI/res/values/colors.xml1
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java6
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/dirlist/FocusManager.java272
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/dirlist/GridDocumentHolder.java3
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/dirlist/ListDocumentHolder.java2
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/dirlist/Model.java4
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java2
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.