blob: 93ec8426e74f65ed87ffac96908acd5ed27b72b2 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.documentsui.dirlist;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import com.android.documentsui.Events;
/**
* A class that handles navigation and focus within the DirectoryFragment.
*/
class FocusManager implements View.OnFocusChangeListener {
private static final String TAG = "FocusManager";
private RecyclerView mView;
private RecyclerView.Adapter<?> mAdapter;
private GridLayoutManager mLayout;
private int mLastFocusPosition = RecyclerView.NO_POSITION;
public FocusManager(RecyclerView view) {
mView = view;
mAdapter = view.getAdapter();
mLayout = (GridLayoutManager) view.getLayoutManager();
}
/**
* Handles navigation (setting focus, adjusting selection if needed) arising from incoming key
* events.
*
* @param doc The DocumentHolder receiving the key event.
* @param keyCode
* @param event
* @return Whether the event was handled.
*/
public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) {
boolean extendSelection = false;
// Translate space/shift-space into PgDn/PgUp
if (keyCode == KeyEvent.KEYCODE_SPACE) {
if (event.isShiftPressed()) {
keyCode = KeyEvent.KEYCODE_PAGE_UP;
} else {
keyCode = KeyEvent.KEYCODE_PAGE_DOWN;
}
} else {
extendSelection = event.isShiftPressed();
}
if (Events.isNavigationKeyCode(keyCode)) {
// Find the target item and focus it.
int endPos = findTargetPosition(doc.itemView, keyCode, event);
if (endPos != RecyclerView.NO_POSITION) {
focusItem(endPos);
}
// Swallow all navigation keystrokes. Otherwise they go to the app's global
// key-handler, which will route them back to the DF and cause focus to be reset.
return true;
}
return false;
}
@Override
public void onFocusChange(View v, boolean hasFocus) {
// Remember focus events on items.
if (hasFocus && v.getParent() == mView) {
mLastFocusPosition = mView.getChildAdapterPosition(v);
}
}
/**
* Requests focus on the item that last had focus. Scrolls to that item if necessary.
*/
public void restoreLastFocus() {
if (mLastFocusPosition != RecyclerView.NO_POSITION) {
// The system takes care of situations when a view is no longer on screen, etc,
focusItem(mLastFocusPosition);
} else {
// Focus the first visible item
focusItem(mLayout.findFirstVisibleItemPosition());
}
}
/**
* @return The adapter position of the last focused item.
*/
public int getFocusPosition() {
return mLastFocusPosition;
}
/**
* Finds the destination position where the focus should land for a given navigation event.
*
* @param view The view that received the event.
* @param keyCode The key code for the event.
* @param event
* @return The adapter position of the destination item. Could be RecyclerView.NO_POSITION.
*/
private int findTargetPosition(View view, int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_MOVE_HOME:
return 0;
case KeyEvent.KEYCODE_MOVE_END:
return mAdapter.getItemCount() - 1;
case KeyEvent.KEYCODE_PAGE_UP:
case KeyEvent.KEYCODE_PAGE_DOWN:
return findPagedTargetPosition(view, keyCode, event);
}
// Find a navigation target based on the arrow key that the user pressed.
int searchDir = -1;
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_UP:
searchDir = View.FOCUS_UP;
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
searchDir = View.FOCUS_DOWN;
break;
}
if (inGridMode()) {
int currentPosition = mView.getChildAdapterPosition(view);
// Left and right arrow keys only work in grid mode.
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_LEFT:
if (currentPosition > 0) {
// Stop backward focus search at the first item, otherwise focus will wrap
// around to the last visible item.
searchDir = View.FOCUS_BACKWARD;
}
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
if (currentPosition < mAdapter.getItemCount() - 1) {
// Stop forward focus search at the last item, otherwise focus will wrap
// around to the first visible item.
searchDir = View.FOCUS_FORWARD;
}
break;
}
}
if (searchDir != -1) {
View targetView = view.focusSearch(searchDir);
// TargetView can be null, for example, if the user pressed <down> at the bottom
// of the list.
if (targetView != null) {
// Ignore navigation targets that aren't items in the RecyclerView.
if (targetView.getParent() == mView) {
return mView.getChildAdapterPosition(targetView);
}
}
}
return RecyclerView.NO_POSITION;
}
/**
* Given a PgUp/PgDn event and the current view, find the position of the target view.
* This returns:
* <li>The position of the topmost (or bottom-most) visible item, if the current item is not
* the top- or bottom-most visible item.
* <li>The position of an item that is one page's worth of items up (or down) if the current
* item is the top- or bottom-most visible item.
* <li>The first (or last) item, if paging up (or down) would go past those limits.
* @param view The view that received the key event.
* @param keyCode Must be KEYCODE_PAGE_UP or KEYCODE_PAGE_DOWN.
* @param event
* @return The adapter position of the target item.
*/
private int findPagedTargetPosition(View view, int keyCode, KeyEvent event) {
int first = mLayout.findFirstVisibleItemPosition();
int last = mLayout.findLastVisibleItemPosition();
int current = mView.getChildAdapterPosition(view);
int pageSize = last - first + 1;
if (keyCode == KeyEvent.KEYCODE_PAGE_UP) {
if (current > first) {
// If the current item isn't the first item, target the first item.
return first;
} else {
// If the current item is the first item, target the item one page up.
int target = current - pageSize;
return target < 0 ? 0 : target;
}
}
if (keyCode == KeyEvent.KEYCODE_PAGE_DOWN) {
if (current < last) {
// If the current item isn't the last item, target the last item.
return last;
} else {
// If the current item is the last item, target the item one page down.
int target = current + pageSize;
int max = mAdapter.getItemCount() - 1;
return target < max ? target : max;
}
}
throw new IllegalArgumentException("Unsupported keyCode: " + keyCode);
}
/**
* Requests focus for the item in the given adapter position, scrolling the RecyclerView if
* necessary.
*
* @param pos
*/
private void focusItem(final int pos) {
// If the item is already in view, focus it; otherwise, scroll to it and focus it.
RecyclerView.ViewHolder vh = mView.findViewHolderForAdapterPosition(pos);
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() {
@Override
public void onScrollStateChanged(RecyclerView view, int newState) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
// When scrolling stops, find the item and focus it.
RecyclerView.ViewHolder vh =
view.findViewHolderForAdapterPosition(pos);
if (vh != null) {
vh.itemView.requestFocus();
} else {
// This might happen in weird corner cases, e.g. if the user is
// scrolling while a delete operation is in progress. In that
// case, just don't attempt to focus the missing item.
Log.w(TAG, "Unable to focus position " + pos + " after scroll");
}
view.removeOnScrollListener(this);
}
}
});
}
}
/**
* @return Whether the layout manager is currently in a grid-configuration.
*/
private boolean inGridMode() {
return mLayout.getSpanCount() > 1;
}
}