Loading packages/DocumentsUI/res/values/colors.xml +1 −0 Original line number Diff line number Diff line Loading @@ -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> Loading packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java +3 −3 Original line number Diff line number Diff line Loading @@ -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); Loading packages/DocumentsUI/src/com/android/documentsui/dirlist/FocusManager.java +265 −7 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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); } /** Loading @@ -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()) { Loading @@ -60,8 +85,6 @@ class FocusManager implements View.OnFocusChangeListener { } else { keyCode = KeyEvent.KEYCODE_PAGE_DOWN; } } else { extendSelection = event.isShiftPressed(); } if (Events.isNavigationKeyCode(keyCode)) { Loading Loading @@ -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() { Loading @@ -258,6 +280,7 @@ class FocusManager implements View.OnFocusChangeListener { } } }); mView.smoothScrollToPosition(pos); } } Loading @@ -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); } } }; } } packages/DocumentsUI/src/com/android/documentsui/dirlist/GridDocumentHolder.java +1 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); } Loading packages/DocumentsUI/src/com/android/documentsui/dirlist/ListDocumentHolder.java +1 −1 Original line number Diff line number Diff line Loading @@ -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) { Loading Loading
packages/DocumentsUI/res/values/colors.xml +1 −0 Original line number Diff line number Diff line Loading @@ -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> Loading
packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java +3 −3 Original line number Diff line number Diff line Loading @@ -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); Loading
packages/DocumentsUI/src/com/android/documentsui/dirlist/FocusManager.java +265 −7 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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); } /** Loading @@ -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()) { Loading @@ -60,8 +85,6 @@ class FocusManager implements View.OnFocusChangeListener { } else { keyCode = KeyEvent.KEYCODE_PAGE_DOWN; } } else { extendSelection = event.isShiftPressed(); } if (Events.isNavigationKeyCode(keyCode)) { Loading Loading @@ -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() { Loading @@ -258,6 +280,7 @@ class FocusManager implements View.OnFocusChangeListener { } } }); mView.smoothScrollToPosition(pos); } } Loading @@ -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); } } }; } }
packages/DocumentsUI/src/com/android/documentsui/dirlist/GridDocumentHolder.java +1 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); } Loading
packages/DocumentsUI/src/com/android/documentsui/dirlist/ListDocumentHolder.java +1 −1 Original line number Diff line number Diff line Loading @@ -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) { Loading