Merge "Implement type-to-focus in the DirectoryFragment." into nyc-dev
This commit is contained in:
@@ -30,6 +30,7 @@
|
|||||||
<color name="primary_dark">@*android:color/primary_dark_material_dark</color>
|
<color name="primary_dark">@*android:color/primary_dark_material_dark</color>
|
||||||
<color name="primary">@*android:color/material_blue_grey_900</color>
|
<color name="primary">@*android:color/material_blue_grey_900</color>
|
||||||
<color name="accent">@*android:color/accent_material_light</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="action_mode">@color/material_grey_400</color>
|
||||||
|
|
||||||
<color name="band_select_background">#88ffffff</color>
|
<color name="band_select_background">#88ffffff</color>
|
||||||
|
|||||||
@@ -268,13 +268,13 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi
|
|||||||
|
|
||||||
mSelectionManager.addCallback(selectionListener);
|
mSelectionManager.addCallback(selectionListener);
|
||||||
|
|
||||||
// Make sure this is done after the RecyclerView is set up.
|
|
||||||
mFocusManager = new FocusManager(mRecView);
|
|
||||||
|
|
||||||
mModel = new Model();
|
mModel = new Model();
|
||||||
mModel.addUpdateListener(mAdapter);
|
mModel.addUpdateListener(mAdapter);
|
||||||
mModel.addUpdateListener(mModelUpdateListener);
|
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);
|
mType = getArguments().getInt(EXTRA_TYPE);
|
||||||
|
|
||||||
mTuner = FragmentTuner.pick(getContext(), state);
|
mTuner = FragmentTuner.pick(getContext(), state);
|
||||||
|
|||||||
@@ -16,13 +16,28 @@
|
|||||||
|
|
||||||
package com.android.documentsui.dirlist;
|
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.GridLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
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.util.Log;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.android.documentsui.Events;
|
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.
|
* 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 static final String TAG = "FocusManager";
|
||||||
|
|
||||||
private RecyclerView mView;
|
private RecyclerView mView;
|
||||||
private RecyclerView.Adapter<?> mAdapter;
|
private DocumentsAdapter mAdapter;
|
||||||
private GridLayoutManager mLayout;
|
private GridLayoutManager mLayout;
|
||||||
|
|
||||||
|
private TitleSearchHelper mSearchHelper;
|
||||||
|
private Model mModel;
|
||||||
|
|
||||||
private int mLastFocusPosition = RecyclerView.NO_POSITION;
|
private int mLastFocusPosition = RecyclerView.NO_POSITION;
|
||||||
|
|
||||||
public FocusManager(RecyclerView view) {
|
public FocusManager(Context context, RecyclerView view, Model model) {
|
||||||
mView = view;
|
mView = view;
|
||||||
mAdapter = view.getAdapter();
|
mAdapter = (DocumentsAdapter) view.getAdapter();
|
||||||
mLayout = (GridLayoutManager) view.getLayoutManager();
|
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.
|
* @return Whether the event was handled.
|
||||||
*/
|
*/
|
||||||
public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) {
|
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
|
// Translate space/shift-space into PgDn/PgUp
|
||||||
if (keyCode == KeyEvent.KEYCODE_SPACE) {
|
if (keyCode == KeyEvent.KEYCODE_SPACE) {
|
||||||
if (event.isShiftPressed()) {
|
if (event.isShiftPressed()) {
|
||||||
@@ -60,8 +85,6 @@ class FocusManager implements View.OnFocusChangeListener {
|
|||||||
} else {
|
} else {
|
||||||
keyCode = KeyEvent.KEYCODE_PAGE_DOWN;
|
keyCode = KeyEvent.KEYCODE_PAGE_DOWN;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
extendSelection = event.isShiftPressed();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Events.isNavigationKeyCode(keyCode)) {
|
if (Events.isNavigationKeyCode(keyCode)) {
|
||||||
@@ -236,7 +259,6 @@ class FocusManager implements View.OnFocusChangeListener {
|
|||||||
if (vh != null) {
|
if (vh != null) {
|
||||||
vh.itemView.requestFocus();
|
vh.itemView.requestFocus();
|
||||||
} else {
|
} else {
|
||||||
mView.smoothScrollToPosition(pos);
|
|
||||||
// Set a one-time listener to request focus when the scroll has completed.
|
// Set a one-time listener to request focus when the scroll has completed.
|
||||||
mView.addOnScrollListener(
|
mView.addOnScrollListener(
|
||||||
new RecyclerView.OnScrollListener() {
|
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() {
|
private boolean inGridMode() {
|
||||||
return mLayout.getSpanCount() > 1;
|
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.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.android.documentsui.IconUtils;
|
|
||||||
import com.android.documentsui.R;
|
import com.android.documentsui.R;
|
||||||
import com.android.documentsui.RootCursorWrapper;
|
import com.android.documentsui.RootCursorWrapper;
|
||||||
import com.android.documentsui.Shared;
|
import com.android.documentsui.Shared;
|
||||||
@@ -107,7 +106,7 @@ final class GridDocumentHolder extends DocumentHolder {
|
|||||||
if (mHideTitles) {
|
if (mHideTitles) {
|
||||||
mTitle.setVisibility(View.GONE);
|
mTitle.setVisibility(View.GONE);
|
||||||
} else {
|
} else {
|
||||||
mTitle.setText(docDisplayName);
|
mTitle.setText(docDisplayName, TextView.BufferType.SPANNABLE);
|
||||||
mTitle.setVisibility(View.VISIBLE);
|
mTitle.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ final class ListDocumentHolder extends DocumentHolder {
|
|||||||
final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId);
|
final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId);
|
||||||
mIconHelper.loadThumbnail(uri, docMimeType, docFlags, docIcon, mIconThumb, mIconMime, null);
|
mIconHelper.loadThumbnail(uri, docMimeType, docFlags, docIcon, mIconThumb, mIconMime, null);
|
||||||
|
|
||||||
mTitle.setText(docDisplayName);
|
mTitle.setText(docDisplayName, TextView.BufferType.SPANNABLE);
|
||||||
mTitle.setVisibility(View.VISIBLE);
|
mTitle.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
if (docSummary != null) {
|
if (docSummary != null) {
|
||||||
|
|||||||
@@ -391,6 +391,10 @@ public class Model {
|
|||||||
mUpdateListeners.add(listener);
|
mUpdateListeners.add(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void removeUpdateListener(UpdateListener listener) {
|
||||||
|
mUpdateListeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
static interface UpdateListener {
|
static interface UpdateListener {
|
||||||
/**
|
/**
|
||||||
* Called when a successful update has occurred.
|
* Called when a successful update has occurred.
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ final class ModelBackedDocumentsAdapter extends DocumentsAdapter {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void unhide(SparseArray<String> ids) {
|
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
|
// An ArrayList can shrink at runtime...and in fact
|
||||||
// it does when we clear it completely.
|
// it does when we clear it completely.
|
||||||
|
|||||||
Reference in New Issue
Block a user