Merge "Implement type-to-focus in the DirectoryFragment." into nyc-dev
am: 7b300ff369
* commit '7b300ff36932a4815f1f85b9ea73ec9a373dee10':
Implement type-to-focus in the DirectoryFragment.
This commit is contained in:
@@ -30,6 +30,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>
|
||||
|
||||
@@ -268,13 +268,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);
|
||||
|
||||
@@ -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)) {
|
||||
@@ -236,7 +259,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() {
|
||||
@@ -258,6 +280,7 @@ class FocusManager implements View.OnFocusChangeListener {
|
||||
}
|
||||
}
|
||||
});
|
||||
mView.smoothScrollToPosition(pos);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,4 +290,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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -391,6 +391,10 @@ public class Model {
|
||||
mUpdateListeners.add(listener);
|
||||
}
|
||||
|
||||
void removeUpdateListener(UpdateListener listener) {
|
||||
mUpdateListeners.remove(listener);
|
||||
}
|
||||
|
||||
static interface UpdateListener {
|
||||
/**
|
||||
* Called when a successful update has occurred.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user