FocusManager.java revision 237432ebb77eabd98d07f7fd09b808ac8d66753e
115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa/* 215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * Copyright (C) 2016 The Android Open Source Project 315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * 415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * Licensed under the Apache License, Version 2.0 (the "License"); 515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * you may not use this file except in compliance with the License. 615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * You may obtain a copy of the License at 715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * 815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * http://www.apache.org/licenses/LICENSE-2.0 915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * 1015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * Unless required by applicable law or agreed to in writing, software 1115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * distributed under the License is distributed on an "AS IS" BASIS, 1215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * See the License for the specific language governing permissions and 1415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * limitations under the License. 1515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa */ 1615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 1715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwapackage com.android.documentsui.dirlist; 1815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 19472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport static com.android.documentsui.model.DocumentInfo.getCursorString; 20472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 21472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.content.Context; 22472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.provider.DocumentsContract.Document; 23a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwaimport android.support.v7.widget.GridLayoutManager; 2415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport android.support.v7.widget.RecyclerView; 25472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.Editable; 26472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.Spannable; 27472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.method.KeyListener; 28472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.method.TextKeyListener; 29472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.method.TextKeyListener.Capitalize; 30472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.text.style.BackgroundColorSpan; 3115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport android.util.Log; 3215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport android.view.KeyEvent; 3315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport android.view.View; 34472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport android.widget.TextView; 3515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 3615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwaimport com.android.documentsui.Events; 37472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport com.android.documentsui.R; 38472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 39472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport java.util.ArrayList; 40472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwaimport java.util.List; 4115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 4215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa/** 4315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * A class that handles navigation and focus within the DirectoryFragment. 4415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa */ 4574e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwaclass FocusManager implements View.OnFocusChangeListener { 4615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa private static final String TAG = "FocusManager"; 4715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 4815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa private RecyclerView mView; 49472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private DocumentsAdapter mAdapter; 50a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa private GridLayoutManager mLayout; 5115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 52472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private TitleSearchHelper mSearchHelper; 53472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private Model mModel; 54472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 5574e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa private int mLastFocusPosition = RecyclerView.NO_POSITION; 5674e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa 57472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa public FocusManager(Context context, RecyclerView view, Model model) { 5815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa mView = view; 59472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mAdapter = (DocumentsAdapter) view.getAdapter(); 60a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa mLayout = (GridLayoutManager) view.getLayoutManager(); 61472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mModel = model; 62472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 63472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mSearchHelper = new TitleSearchHelper(context); 6415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 6515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 6615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa /** 6715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * Handles navigation (setting focus, adjusting selection if needed) arising from incoming key 6815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * events. 6915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * 7015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param doc The DocumentHolder receiving the key event. 7115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param keyCode 7215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param event 7315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @return Whether the event was handled. 7415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa */ 7515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) { 76472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Search helper gets first crack, for doing type-to-focus. 77472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (mSearchHelper.handleKey(doc, keyCode, event)) { 78472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return true; 79472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 80472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 8192db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa // Translate space/shift-space into PgDn/PgUp 8292db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa if (keyCode == KeyEvent.KEYCODE_SPACE) { 8392db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa if (event.isShiftPressed()) { 8492db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa keyCode = KeyEvent.KEYCODE_PAGE_UP; 8592db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa } else { 8692db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa keyCode = KeyEvent.KEYCODE_PAGE_DOWN; 8792db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa } 8892db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa } 8992db0b32e6f7bd6d2d593d179e2a9f759d453eb9Ben Kwa 9015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (Events.isNavigationKeyCode(keyCode)) { 9115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // Find the target item and focus it. 9215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int endPos = findTargetPosition(doc.itemView, keyCode, event); 9315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 9415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (endPos != RecyclerView.NO_POSITION) { 9515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa focusItem(endPos); 9615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 9774e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa // Swallow all navigation keystrokes. Otherwise they go to the app's global 9874e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa // key-handler, which will route them back to the DF and cause focus to be reset. 9974e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa return true; 10074e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa } 10174e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa return false; 10274e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa } 10374e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa 10474e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa @Override 10574e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa public void onFocusChange(View v, boolean hasFocus) { 10674e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa // Remember focus events on items. 10774e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa if (hasFocus && v.getParent() == mView) { 10874e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa mLastFocusPosition = mView.getChildAdapterPosition(v); 10974e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa } 11074e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa } 11174e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa 11274e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa /** 11374e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa * Requests focus on the item that last had focus. Scrolls to that item if necessary. 11474e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa */ 11574e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa public void restoreLastFocus() { 116237432ebb77eabd98d07f7fd09b808ac8d66753eBen Kwa if (mAdapter.getItemCount() == 0) { 117237432ebb77eabd98d07f7fd09b808ac8d66753eBen Kwa // Nothing to focus. 118237432ebb77eabd98d07f7fd09b808ac8d66753eBen Kwa return; 119237432ebb77eabd98d07f7fd09b808ac8d66753eBen Kwa } 120237432ebb77eabd98d07f7fd09b808ac8d66753eBen Kwa 12174e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa if (mLastFocusPosition != RecyclerView.NO_POSITION) { 12274e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa // The system takes care of situations when a view is no longer on screen, etc, 12374e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa focusItem(mLastFocusPosition); 12474e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa } else { 12574e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa // Focus the first visible item 12674e5d4173a1cd060b16c663108a1eeabeae25540Ben Kwa focusItem(mLayout.findFirstVisibleItemPosition()); 12715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 12815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 12915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 13015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa /** 1319504d764fc5b625661959ed6dcd190b9730d418dBen Kwa * @return The adapter position of the last focused item. 1329504d764fc5b625661959ed6dcd190b9730d418dBen Kwa */ 1339504d764fc5b625661959ed6dcd190b9730d418dBen Kwa public int getFocusPosition() { 1349504d764fc5b625661959ed6dcd190b9730d418dBen Kwa return mLastFocusPosition; 1359504d764fc5b625661959ed6dcd190b9730d418dBen Kwa } 1369504d764fc5b625661959ed6dcd190b9730d418dBen Kwa 1379504d764fc5b625661959ed6dcd190b9730d418dBen Kwa /** 13815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * Finds the destination position where the focus should land for a given navigation event. 13915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * 14015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param view The view that received the event. 14115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param keyCode The key code for the event. 14215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param event 14315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @return The adapter position of the destination item. Could be RecyclerView.NO_POSITION. 14415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa */ 14515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa private int findTargetPosition(View view, int keyCode, KeyEvent event) { 14615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa switch (keyCode) { 14715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_MOVE_HOME: 14815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return 0; 14915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_MOVE_END: 15015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return mAdapter.getItemCount() - 1; 15115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_PAGE_UP: 15215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_PAGE_DOWN: 15315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return findPagedTargetPosition(view, keyCode, event); 15415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 15515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 15615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // Find a navigation target based on the arrow key that the user pressed. 15715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int searchDir = -1; 15815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa switch (keyCode) { 15915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_DPAD_UP: 16015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa searchDir = View.FOCUS_UP; 16115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa break; 16215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa case KeyEvent.KEYCODE_DPAD_DOWN: 16315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa searchDir = View.FOCUS_DOWN; 16415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa break; 165a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa } 166a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa 167a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa if (inGridMode()) { 168a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa int currentPosition = mView.getChildAdapterPosition(view); 169a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa // Left and right arrow keys only work in grid mode. 170a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa switch (keyCode) { 171a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa case KeyEvent.KEYCODE_DPAD_LEFT: 172a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa if (currentPosition > 0) { 173a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa // Stop backward focus search at the first item, otherwise focus will wrap 174a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa // around to the last visible item. 175a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa searchDir = View.FOCUS_BACKWARD; 176a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa } 177a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa break; 178a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa case KeyEvent.KEYCODE_DPAD_RIGHT: 179a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa if (currentPosition < mAdapter.getItemCount() - 1) { 180a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa // Stop forward focus search at the last item, otherwise focus will wrap 181a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa // around to the first visible item. 182a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa searchDir = View.FOCUS_FORWARD; 183a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa } 184a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa break; 185a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa } 18615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 18715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 18815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (searchDir != -1) { 18967f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa // Focus search behaves badly if the parent RecyclerView is focused. However, focusable 19067f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa // shouldn't be unset on RecyclerView, otherwise focus isn't properly restored after 19167f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa // events that cause a UI rebuild (like rotating the device). Compromise: turn focusable 19267f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa // off while performing the focus search. 19367f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa // TODO: Revisit this when RV focus issues are resolved. 19467f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa mView.setFocusable(false); 19515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa View targetView = view.focusSearch(searchDir); 19667f06a3c28f72e4dff5b0c4d34327bca70c1e4a0Ben Kwa mView.setFocusable(true); 19715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // TargetView can be null, for example, if the user pressed <down> at the bottom 19815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // of the list. 19915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (targetView != null) { 20015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // Ignore navigation targets that aren't items in the RecyclerView. 20115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (targetView.getParent() == mView) { 20215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return mView.getChildAdapterPosition(targetView); 20315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 20415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 20515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 20615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 20715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return RecyclerView.NO_POSITION; 20815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 20915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 21015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa /** 21115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * Given a PgUp/PgDn event and the current view, find the position of the target view. 21215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * This returns: 21315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * <li>The position of the topmost (or bottom-most) visible item, if the current item is not 21415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * the top- or bottom-most visible item. 21515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * <li>The position of an item that is one page's worth of items up (or down) if the current 21615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * item is the top- or bottom-most visible item. 21715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * <li>The first (or last) item, if paging up (or down) would go past those limits. 21815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param view The view that received the key event. 21915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param keyCode Must be KEYCODE_PAGE_UP or KEYCODE_PAGE_DOWN. 22015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param event 22115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @return The adapter position of the target item. 22215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa */ 22315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa private int findPagedTargetPosition(View view, int keyCode, KeyEvent event) { 22415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int first = mLayout.findFirstVisibleItemPosition(); 22515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int last = mLayout.findLastVisibleItemPosition(); 22615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int current = mView.getChildAdapterPosition(view); 22715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int pageSize = last - first + 1; 22815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 22915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (keyCode == KeyEvent.KEYCODE_PAGE_UP) { 23015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (current > first) { 23115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // If the current item isn't the first item, target the first item. 23215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return first; 23315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } else { 23415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // If the current item is the first item, target the item one page up. 23515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int target = current - pageSize; 23615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return target < 0 ? 0 : target; 23715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 23815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 23915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 24015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (keyCode == KeyEvent.KEYCODE_PAGE_DOWN) { 24115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (current < last) { 24215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // If the current item isn't the last item, target the last item. 24315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return last; 24415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } else { 24515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // If the current item is the last item, target the item one page down. 24615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int target = current + pageSize; 24715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa int max = mAdapter.getItemCount() - 1; 24815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa return target < max ? target : max; 24915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 25015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 25115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 25215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa throw new IllegalArgumentException("Unsupported keyCode: " + keyCode); 25315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 25415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa 25515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa /** 25615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * Requests focus for the item in the given adapter position, scrolling the RecyclerView if 25715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * necessary. 25815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * 25915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa * @param pos 26015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa */ 26115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa private void focusItem(final int pos) { 26215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // If the item is already in view, focus it; otherwise, scroll to it and focus it. 26315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa RecyclerView.ViewHolder vh = mView.findViewHolderForAdapterPosition(pos); 26415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (vh != null) { 26515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa vh.itemView.requestFocus(); 26615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } else { 26715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // Set a one-time listener to request focus when the scroll has completed. 26815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa mView.addOnScrollListener( 26915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa new RecyclerView.OnScrollListener() { 27015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa @Override 27115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa public void onScrollStateChanged(RecyclerView view, int newState) { 27215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (newState == RecyclerView.SCROLL_STATE_IDLE) { 27315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // When scrolling stops, find the item and focus it. 27415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa RecyclerView.ViewHolder vh = 27515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa view.findViewHolderForAdapterPosition(pos); 27615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa if (vh != null) { 27715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa vh.itemView.requestFocus(); 27815de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } else { 27915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // This might happen in weird corner cases, e.g. if the user is 28015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // scrolling while a delete operation is in progress. In that 28115de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa // case, just don't attempt to focus the missing item. 28215de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa Log.w(TAG, "Unable to focus position " + pos + " after scroll"); 28315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 28415de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa view.removeOnScrollListener(this); 28515de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 28615de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 28715de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa }); 288472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mView.smoothScrollToPosition(pos); 28915de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 29015de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa } 291a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa 292a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa /** 293a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa * @return Whether the layout manager is currently in a grid-configuration. 294a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa */ 295a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa private boolean inGridMode() { 296a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa return mLayout.getSpanCount() > 1; 297a6c2f0a946ec0faf80bd59d2b3b66da8965231f1Ben Kwa } 298472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 299472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 300472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * A helper class for handling type-to-focus. Instantiate this class, and pass it KeyEvents via 301472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * the {@link #handleKey(DocumentHolder, int, KeyEvent)} method. The class internally will build 302472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * up a string from individual key events, and perform searching based on that string. When an 303472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * item is found that matches the search term, that item will be focused. This class also 304472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * highlights instances of the search term found in the view. 305472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 306472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private class TitleSearchHelper { 307472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa final private KeyListener mTextListener = new TextKeyListener(Capitalize.NONE, false); 308472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa final private Editable mSearchString = Editable.Factory.getInstance().newEditable(""); 309472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa final private Highlighter mHighlighter = new Highlighter(); 310472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa final private BackgroundColorSpan mSpan; 311472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private List<String> mIndex; 312472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private boolean mActive; 313472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 314472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa public TitleSearchHelper(Context context) { 315472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mSpan = new BackgroundColorSpan(context.getColor(R.color.accent_dark)); 316472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 317472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 318472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 319472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * Handles alphanumeric keystrokes for type-to-focus. This method builds a search term out 320472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * of individual key events, and then performs a search for the given string. 321472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * 322472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * @param doc The document holder receiving the key event. 323472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * @param keyCode 324472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * @param event 325472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * @return Whether the event was handled. 326472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 327472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) { 328472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa switch (keyCode) { 329472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa case KeyEvent.KEYCODE_ESCAPE: 330472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa case KeyEvent.KEYCODE_ENTER: 331472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (mActive) { 332472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // These keys end any active searches. 333472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa deactivate(); 334472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return true; 335472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } else { 336472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Don't handle these key events if there is no active search. 337472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return false; 338472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 339472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa case KeyEvent.KEYCODE_SPACE: 340472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // This allows users to search for files with spaces in their names, but ignores 341472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // spacebar events when a text search is not active. 342472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (!mActive) { 343472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return false; 344472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 345472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 346472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 347472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Navigation keys also end active searches. 348472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (Events.isNavigationKeyCode(keyCode)) { 349472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa deactivate(); 350472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Don't handle the keycode, so navigation still occurs. 351472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return false; 352472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 353472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 354472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Build up the search string, and perform the search. 355472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa boolean handled = mTextListener.onKeyDown(doc.itemView, mSearchString, keyCode, event); 356472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 357472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Delete is processed by the text listener, but not "handled". Check separately for it. 358472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (handled || keyCode == KeyEvent.KEYCODE_DEL) { 359472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa String searchString = mSearchString.toString(); 360472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (searchString.length() == 0) { 361472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Don't perform empty searches. 362472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return false; 363472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 364472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa activate(); 365472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa for (int pos = 0; pos < mIndex.size(); pos++) { 366472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa String title = mIndex.get(pos); 367472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (title != null && title.startsWith(searchString)) { 368472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa focusItem(pos); 369472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa break; 370472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 371472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 372472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 373472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 374472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return handled; 375472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 376472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 377472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 378472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * Activates the search helper, which changes its key handling and updates the search index 379472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * and highlights if necessary. Call this each time the search term is updated. 380472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 381472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private void activate() { 382472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (!mActive) { 383472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Install listeners. 384472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mModel.addUpdateListener(mModelListener); 385472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 386472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 387472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // If the search index was invalidated, rebuild it 388472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (mIndex == null) { 389472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa buildIndex(); 390472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 391472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 392472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // TODO: Uncomment this to enable search term highlighting in the UI. 393472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa// mHighlighter.activate(); 394472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 395472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mActive = true; 396472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 397472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 398472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 399472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * Deactivates the search helper (see {@link #activate()}). Call this when a search ends. 400472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 401472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private void deactivate() { 402472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (mActive) { 403472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Remove listeners. 404472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mModel.removeUpdateListener(mModelListener); 405472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 406472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 407472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // TODO: Uncomment this when search-term highlighting is enabled in the UI. 408472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa// mHighlighter.deactivate(); 409472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 410472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mIndex = null; 411472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mSearchString.clear(); 412472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mActive = false; 413472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 414472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 415472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 416472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * Applies title highlights to the given view. The view must have a title field that is a 417472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * spannable text field. If this condition is not met, this function does nothing. 418472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * 419472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * @param view 420472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 421472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private void applyHighlight(View view) { 422472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa TextView titleView = (TextView) view.findViewById(android.R.id.title); 423472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (titleView == null) { 424472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return; 425472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 426472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 427472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa String searchString = mSearchString.toString(); 428472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa CharSequence tmpText = titleView.getText(); 429472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (tmpText instanceof Spannable) { 430472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa Spannable title = (Spannable) tmpText; 431472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa String titleString = title.toString(); 432472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (titleString.startsWith(searchString)) { 433472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa title.setSpan(mSpan, 0, searchString.length(), 434472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 435472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } else { 436472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa title.removeSpan(mSpan); 437472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 438472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 439472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 440472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 441472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 442472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * Removes title highlights from the given view. The view must have a title field that is a 443472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * spannable text field. If this condition is not met, this function does nothing. 444472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * 445472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * @param view 446472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 447472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private void removeHighlight(View view) { 448472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa TextView titleView = (TextView) view.findViewById(android.R.id.title); 449472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (titleView == null) { 450472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa return; 451472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 452472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 453472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa CharSequence tmpText = titleView.getText(); 454472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (tmpText instanceof Spannable) { 455472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa ((Spannable) tmpText).removeSpan(mSpan); 456472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 457472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 458472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 459472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 460472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * Builds a search index for finding items by title. Queries the model and adapter, so both 461472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * must be set up before calling this method. 462472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 463472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private void buildIndex() { 464472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa int itemCount = mAdapter.getItemCount(); 465472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa List<String> index = new ArrayList<>(itemCount); 466472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa for (int i = 0; i < itemCount; i++) { 467472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa String modelId = mAdapter.getModelId(i); 468472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (modelId != null) { 469472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa index.add( 470472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa getCursorString(mModel.getItem(modelId), Document.COLUMN_DISPLAY_NAME)); 471472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } else { 472472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa index.add(""); 473472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 474472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 475472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mIndex = index; 476472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 477472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 478472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private Model.UpdateListener mModelListener = new Model.UpdateListener() { 479472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa @Override 480472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa public void onModelUpdate(Model model) { 481472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Invalidate the search index when the model updates. 482472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mIndex = null; 483472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 484472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 485472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa @Override 486472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa public void onModelUpdateFailed(Exception e) { 487472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Invalidate the search index when the model updates. 488472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mIndex = null; 489472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 490472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa }; 491472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 492472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa private class Highlighter implements RecyclerView.OnChildAttachStateChangeListener { 493472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 494472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * Starts highlighting instances of the current search term in the UI. 495472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 496472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa public void activate() { 497472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Update highlights on all views 498472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa int itemCount = mView.getChildCount(); 499472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa for (int i = 0; i < itemCount; i++) { 500472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa applyHighlight(mView.getChildAt(i)); 501472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 502472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Keep highlights up-to-date as items come in and out of view. 503472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mView.addOnChildAttachStateChangeListener(this); 504472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 505472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 506472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa /** 507472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa * Stops highlighting instances of the current search term in the UI. 508472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa */ 509472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa public void deactivate() { 510472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Remove highlights on all views 511472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa int itemCount = mView.getChildCount(); 512472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa for (int i = 0; i < itemCount; i++) { 513472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa removeHighlight(mView.getChildAt(i)); 514472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 515472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa // Stop updating highlights. 516472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa mView.removeOnChildAttachStateChangeListener(this); 517472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 518472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 519472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa @Override 520472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa public void onChildViewAttachedToWindow(View view) { 521472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa applyHighlight(view); 522472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 523472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa 524472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa @Override 525472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa public void onChildViewDetachedFromWindow(View view) { 526472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa TextView titleView = (TextView) view.findViewById(android.R.id.title); 527472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa if (titleView != null) { 528472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa removeHighlight(titleView); 529472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 530472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 531472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa }; 532472103f137c5baa7d1f7acd7330027c725c65a99Ben Kwa } 53315de7f943c3b252f36f9610f5f9ef917ce6d5d29Ben Kwa} 534