Files
lawnchair/src/com/android/launcher3/FocusHelper.java
Sunny Goyal e598699dc5 Remove custom focus logic
Using the system logic for focus handling so that partially visible contents are
properly handled, eg in overview only hotseat is visible
This does changes the focus logic on workspace when both icons and widgets or different sizes
are present, we may consider reviving (parts of) this logic in the future

Bug: 80189843
Change-Id: I874e284e0a62b579c24d36db9b74da3de7a7e367
2018-05-23 10:54:18 -07:00

567 lines
25 KiB
Java

/*
* Copyright (C) 2015 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.launcher3;
import android.util.Log;
import android.view.KeyEvent;
import android.view.SoundEffectConstants;
import android.view.View;
import android.view.ViewGroup;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.folder.Folder;
import com.android.launcher3.folder.FolderPagedView;
import com.android.launcher3.util.FocusLogic;
import com.android.launcher3.util.Thunk;
/**
* A keyboard listener we set on all the workspace icons.
*/
class IconKeyEventListener implements View.OnKeyListener {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
return FocusHelper.handleIconKeyEvent(v, keyCode, event);
}
}
/**
* A keyboard listener we set on all the hotseat buttons.
*/
class HotseatIconKeyEventListener implements View.OnKeyListener {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
return FocusHelper.handleHotseatButtonKeyEvent(v, keyCode, event);
}
}
/**
* A keyboard listener we set on full screen pages (e.g. custom content).
*/
class FullscreenKeyEventListener implements View.OnKeyListener {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
|| keyCode == KeyEvent.KEYCODE_PAGE_DOWN || keyCode == KeyEvent.KEYCODE_PAGE_UP) {
// Handle the key event just like a workspace icon would in these cases. In this case,
// it will basically act as if there is a single icon in the top left (so you could
// think of the fullscreen page as a focusable fullscreen widget).
return FocusHelper.handleIconKeyEvent(v, keyCode, event);
}
return false;
}
}
/**
* TODO: Reevaluate if this is still required
*/
public class FocusHelper {
private static final String TAG = "FocusHelper";
private static final boolean DEBUG = false;
/**
* Handles key events in paged folder.
*/
public static class PagedFolderKeyEventListener implements View.OnKeyListener {
private final Folder mFolder;
public PagedFolderKeyEventListener(Folder folder) {
mFolder = folder;
}
@Override
public boolean onKey(View v, int keyCode, KeyEvent e) {
boolean consume = FocusLogic.shouldConsume(keyCode);
if (e.getAction() == KeyEvent.ACTION_UP) {
return consume;
}
if (DEBUG) {
Log.v(TAG, String.format("Handle ALL Folders keyevent=[%s].",
KeyEvent.keyCodeToString(keyCode)));
}
if (!(v.getParent() instanceof ShortcutAndWidgetContainer)) {
if (FeatureFlags.IS_DOGFOOD_BUILD) {
throw new IllegalStateException("Parent of the focused item is not supported.");
} else {
return false;
}
}
// Initialize variables.
final ShortcutAndWidgetContainer itemContainer = (ShortcutAndWidgetContainer) v.getParent();
final CellLayout cellLayout = (CellLayout) itemContainer.getParent();
final int iconIndex = itemContainer.indexOfChild(v);
final FolderPagedView pagedView = (FolderPagedView) cellLayout.getParent();
final int pageIndex = pagedView.indexOfChild(cellLayout);
final int pageCount = pagedView.getPageCount();
final boolean isLayoutRtl = Utilities.isRtl(v.getResources());
int[][] matrix = FocusLogic.createSparseMatrix(cellLayout);
// Process focus.
int newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, iconIndex, pageIndex,
pageCount, isLayoutRtl);
if (newIconIndex == FocusLogic.NOOP) {
handleNoopKey(keyCode, v);
return consume;
}
ShortcutAndWidgetContainer newParent = null;
View child = null;
switch (newIconIndex) {
case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN:
case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN:
newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1);
if (newParent != null) {
int row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY;
pagedView.snapToPage(pageIndex - 1);
child = newParent.getChildAt(
((newIconIndex == FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN)
^ newParent.invertLayoutHorizontally()) ? 0 : matrix.length - 1,
row);
}
break;
case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM:
newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1);
if (newParent != null) {
pagedView.snapToPage(pageIndex - 1);
child = newParent.getChildAt(0, 0);
}
break;
case FocusLogic.PREVIOUS_PAGE_LAST_ITEM:
newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1);
if (newParent != null) {
pagedView.snapToPage(pageIndex - 1);
child = newParent.getChildAt(matrix.length - 1, matrix[0].length - 1);
}
break;
case FocusLogic.NEXT_PAGE_FIRST_ITEM:
newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex + 1);
if (newParent != null) {
pagedView.snapToPage(pageIndex + 1);
child = newParent.getChildAt(0, 0);
}
break;
case FocusLogic.NEXT_PAGE_LEFT_COLUMN:
case FocusLogic.NEXT_PAGE_RIGHT_COLUMN:
newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex + 1);
if (newParent != null) {
pagedView.snapToPage(pageIndex + 1);
child = FocusLogic.getAdjacentChildInNextFolderPage(
newParent, v, newIconIndex);
}
break;
case FocusLogic.CURRENT_PAGE_FIRST_ITEM:
child = cellLayout.getChildAt(0, 0);
break;
case FocusLogic.CURRENT_PAGE_LAST_ITEM:
child = pagedView.getLastItem();
break;
default: // Go to some item on the current page.
child = itemContainer.getChildAt(newIconIndex);
break;
}
if (child != null) {
child.requestFocus();
playSoundEffect(keyCode, v);
} else {
handleNoopKey(keyCode, v);
}
return consume;
}
public void handleNoopKey(int keyCode, View v) {
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
mFolder.mFolderName.requestFocus();
playSoundEffect(keyCode, v);
}
}
}
/**
* Handles key events in the workspace hotseat (bottom of the screen).
* <p>Currently we don't special case for the phone UI in different orientations, even though
* the hotseat is on the side in landscape mode. This is to ensure that accessibility
* consistency is maintained across rotations.
*/
static boolean handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e) {
boolean consume = FocusLogic.shouldConsume(keyCode);
if (e.getAction() == KeyEvent.ACTION_UP || !consume) {
return consume;
}
final Launcher launcher = Launcher.getLauncher(v.getContext());
final DeviceProfile profile = launcher.getDeviceProfile();
if (DEBUG) {
Log.v(TAG, String.format(
"Handle HOTSEAT BUTTONS keyevent=[%s] on hotseat buttons, isVertical=%s",
KeyEvent.keyCodeToString(keyCode), profile.isVerticalBarLayout()));
}
// Initialize the variables.
final Workspace workspace = (Workspace) v.getRootView().findViewById(R.id.workspace);
final ShortcutAndWidgetContainer hotseatParent = (ShortcutAndWidgetContainer) v.getParent();
final CellLayout hotseatLayout = (CellLayout) hotseatParent.getParent();
final ItemInfo itemInfo = (ItemInfo) v.getTag();
int pageIndex = workspace.getNextPage();
int pageCount = workspace.getChildCount();
int iconIndex = hotseatParent.indexOfChild(v);
int iconRank = ((CellLayout.LayoutParams) hotseatLayout.getShortcutsAndWidgets()
.getChildAt(iconIndex).getLayoutParams()).cellX;
final CellLayout iconLayout = (CellLayout) workspace.getChildAt(pageIndex);
if (iconLayout == null) {
// This check is to guard against cases where key strokes rushes in when workspace
// child creation/deletion is still in flux. (e.g., during drop or fling
// animation.)
return consume;
}
final ViewGroup iconParent = iconLayout.getShortcutsAndWidgets();
ViewGroup parent = null;
int[][] matrix = null;
if (keyCode == KeyEvent.KEYCODE_DPAD_UP &&
!profile.isVerticalBarLayout()) {
matrix = FocusLogic.createSparseMatrixWithHotseat(iconLayout, hotseatLayout, profile);
iconIndex += iconParent.getChildCount();
parent = iconParent;
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT &&
profile.isVerticalBarLayout()) {
matrix = FocusLogic.createSparseMatrixWithHotseat(iconLayout, hotseatLayout, profile);
iconIndex += iconParent.getChildCount();
parent = iconParent;
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT &&
profile.isVerticalBarLayout()) {
keyCode = KeyEvent.KEYCODE_PAGE_DOWN;
} else {
// For other KEYCODE_DPAD_LEFT and KEYCODE_DPAD_RIGHT navigation, do not use the
// matrix extended with hotseat.
matrix = FocusLogic.createSparseMatrix(hotseatLayout);
parent = hotseatParent;
}
// Process the focus.
int newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, iconIndex, pageIndex,
pageCount, Utilities.isRtl(v.getResources()));
View newIcon = null;
switch (newIconIndex) {
case FocusLogic.NEXT_PAGE_FIRST_ITEM:
parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1);
newIcon = parent.getChildAt(0);
// TODO(hyunyoungs): handle cases where the child is not an icon but
// a folder or a widget.
workspace.snapToPage(pageIndex + 1);
break;
case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM:
parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
newIcon = parent.getChildAt(0);
// TODO(hyunyoungs): handle cases where the child is not an icon but
// a folder or a widget.
workspace.snapToPage(pageIndex - 1);
break;
case FocusLogic.PREVIOUS_PAGE_LAST_ITEM:
parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
newIcon = parent.getChildAt(parent.getChildCount() - 1);
// TODO(hyunyoungs): handle cases where the child is not an icon but
// a folder or a widget.
workspace.snapToPage(pageIndex - 1);
break;
case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN:
case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN:
// Go to the previous page but keep the focus on the same hotseat icon.
workspace.snapToPage(pageIndex - 1);
break;
case FocusLogic.NEXT_PAGE_LEFT_COLUMN:
case FocusLogic.NEXT_PAGE_RIGHT_COLUMN:
// Go to the next page but keep the focus on the same hotseat icon.
workspace.snapToPage(pageIndex + 1);
break;
}
if (parent == iconParent && newIconIndex >= iconParent.getChildCount()) {
newIconIndex -= iconParent.getChildCount();
}
if (parent != null) {
if (newIcon == null && newIconIndex >= 0) {
newIcon = parent.getChildAt(newIconIndex);
}
if (newIcon != null) {
newIcon.requestFocus();
playSoundEffect(keyCode, v);
}
}
return consume;
}
/**
* Handles key events in a workspace containing icons.
*/
static boolean handleIconKeyEvent(View v, int keyCode, KeyEvent e) {
boolean consume = FocusLogic.shouldConsume(keyCode);
if (e.getAction() == KeyEvent.ACTION_UP || !consume) {
return consume;
}
Launcher launcher = Launcher.getLauncher(v.getContext());
DeviceProfile profile = launcher.getDeviceProfile();
if (DEBUG) {
Log.v(TAG, String.format("Handle WORKSPACE ICONS keyevent=[%s] isVerticalBar=%s",
KeyEvent.keyCodeToString(keyCode), profile.isVerticalBarLayout()));
}
// Initialize the variables.
ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent();
CellLayout iconLayout = (CellLayout) parent.getParent();
final Workspace workspace = (Workspace) iconLayout.getParent();
final ViewGroup dragLayer = (ViewGroup) workspace.getParent();
final ViewGroup tabs = (ViewGroup) dragLayer.findViewById(R.id.drop_target_bar);
final Hotseat hotseat = (Hotseat) dragLayer.findViewById(R.id.hotseat);
final ItemInfo itemInfo = (ItemInfo) v.getTag();
final int iconIndex = parent.indexOfChild(v);
final int pageIndex = workspace.indexOfChild(iconLayout);
final int pageCount = workspace.getChildCount();
CellLayout hotseatLayout = (CellLayout) hotseat.getChildAt(0);
ShortcutAndWidgetContainer hotseatParent = hotseatLayout.getShortcutsAndWidgets();
int[][] matrix;
// KEYCODE_DPAD_DOWN in portrait (KEYCODE_DPAD_RIGHT in landscape) is the only key allowed
// to take a user to the hotseat. For other dpad navigation, do not use the matrix extended
// with the hotseat.
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN && !profile.isVerticalBarLayout()) {
matrix = FocusLogic.createSparseMatrixWithHotseat(iconLayout, hotseatLayout, profile);
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT &&
profile.isVerticalBarLayout()) {
matrix = FocusLogic.createSparseMatrixWithHotseat(iconLayout, hotseatLayout, profile);
} else {
matrix = FocusLogic.createSparseMatrix(iconLayout);
}
// Process the focus.
int newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, iconIndex, pageIndex,
pageCount, Utilities.isRtl(v.getResources()));
boolean isRtl = Utilities.isRtl(v.getResources());
View newIcon = null;
CellLayout workspaceLayout = (CellLayout) workspace.getChildAt(pageIndex);
switch (newIconIndex) {
case FocusLogic.NOOP:
if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
newIcon = tabs;
}
break;
case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN:
case FocusLogic.NEXT_PAGE_RIGHT_COLUMN:
int newPageIndex = pageIndex - 1;
if (newIconIndex == FocusLogic.NEXT_PAGE_RIGHT_COLUMN) {
newPageIndex = pageIndex + 1;
}
int row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY;
parent = getCellLayoutChildrenForIndex(workspace, newPageIndex);
if (parent != null) {
iconLayout = (CellLayout) parent.getParent();
matrix = FocusLogic.createSparseMatrixWithPivotColumn(iconLayout,
iconLayout.getCountX(), row);
newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, FocusLogic.PIVOT,
newPageIndex, pageCount, Utilities.isRtl(v.getResources()));
if (newIconIndex == FocusLogic.NEXT_PAGE_FIRST_ITEM) {
newIcon = handleNextPageFirstItem(workspace, hotseatLayout, pageIndex,
isRtl);
} else if (newIconIndex == FocusLogic.PREVIOUS_PAGE_LAST_ITEM) {
newIcon = handlePreviousPageLastItem(workspace, hotseatLayout, pageIndex,
isRtl);
} else {
newIcon = parent.getChildAt(newIconIndex);
}
}
break;
case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM:
workspaceLayout = (CellLayout) workspace.getChildAt(pageIndex - 1);
newIcon = getFirstFocusableIconInReadingOrder(workspaceLayout, isRtl);
if (newIcon == null) {
// Check the hotseat if no focusable item was found on the workspace.
newIcon = getFirstFocusableIconInReadingOrder(hotseatLayout, isRtl);
workspace.snapToPage(pageIndex - 1);
}
break;
case FocusLogic.PREVIOUS_PAGE_LAST_ITEM:
newIcon = handlePreviousPageLastItem(workspace, hotseatLayout, pageIndex, isRtl);
break;
case FocusLogic.NEXT_PAGE_FIRST_ITEM:
newIcon = handleNextPageFirstItem(workspace, hotseatLayout, pageIndex, isRtl);
break;
case FocusLogic.NEXT_PAGE_LEFT_COLUMN:
case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN:
newPageIndex = pageIndex + 1;
if (newIconIndex == FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN) {
newPageIndex = pageIndex - 1;
}
row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY;
parent = getCellLayoutChildrenForIndex(workspace, newPageIndex);
if (parent != null) {
iconLayout = (CellLayout) parent.getParent();
matrix = FocusLogic.createSparseMatrixWithPivotColumn(iconLayout, -1, row);
newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, FocusLogic.PIVOT,
newPageIndex, pageCount, Utilities.isRtl(v.getResources()));
if (newIconIndex == FocusLogic.NEXT_PAGE_FIRST_ITEM) {
newIcon = handleNextPageFirstItem(workspace, hotseatLayout, pageIndex,
isRtl);
} else if (newIconIndex == FocusLogic.PREVIOUS_PAGE_LAST_ITEM) {
newIcon = handlePreviousPageLastItem(workspace, hotseatLayout, pageIndex,
isRtl);
} else {
newIcon = parent.getChildAt(newIconIndex);
}
}
break;
case FocusLogic.CURRENT_PAGE_FIRST_ITEM:
newIcon = getFirstFocusableIconInReadingOrder(workspaceLayout, isRtl);
if (newIcon == null) {
// Check the hotseat if no focusable item was found on the workspace.
newIcon = getFirstFocusableIconInReadingOrder(hotseatLayout, isRtl);
}
break;
case FocusLogic.CURRENT_PAGE_LAST_ITEM:
newIcon = getFirstFocusableIconInReverseReadingOrder(workspaceLayout, isRtl);
if (newIcon == null) {
// Check the hotseat if no focusable item was found on the workspace.
newIcon = getFirstFocusableIconInReverseReadingOrder(hotseatLayout, isRtl);
}
break;
default:
// current page, some item.
if (0 <= newIconIndex && newIconIndex < parent.getChildCount()) {
newIcon = parent.getChildAt(newIconIndex);
} else if (parent.getChildCount() <= newIconIndex &&
newIconIndex < parent.getChildCount() + hotseatParent.getChildCount()) {
newIcon = hotseatParent.getChildAt(newIconIndex - parent.getChildCount());
}
break;
}
if (newIcon != null) {
newIcon.requestFocus();
playSoundEffect(keyCode, v);
}
return consume;
}
//
// Helper methods.
//
/**
* Private helper method to get the CellLayoutChildren given a CellLayout index.
*/
@Thunk static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex(
ViewGroup container, int i) {
CellLayout parent = (CellLayout) container.getChildAt(i);
return parent.getShortcutsAndWidgets();
}
/**
* Helper method to be used for playing sound effects.
*/
@Thunk static void playSoundEffect(int keyCode, View v) {
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_LEFT:
v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
case KeyEvent.KEYCODE_PAGE_DOWN:
case KeyEvent.KEYCODE_MOVE_END:
v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
break;
case KeyEvent.KEYCODE_DPAD_UP:
case KeyEvent.KEYCODE_PAGE_UP:
case KeyEvent.KEYCODE_MOVE_HOME:
v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
break;
default:
break;
}
}
private static View handlePreviousPageLastItem(Workspace workspace, CellLayout hotseatLayout,
int pageIndex, boolean isRtl) {
if (pageIndex - 1 < 0) {
return null;
}
CellLayout workspaceLayout = (CellLayout) workspace.getChildAt(pageIndex - 1);
View newIcon = getFirstFocusableIconInReverseReadingOrder(workspaceLayout, isRtl);
if (newIcon == null) {
// Check the hotseat if no focusable item was found on the workspace.
newIcon = getFirstFocusableIconInReverseReadingOrder(hotseatLayout,isRtl);
workspace.snapToPage(pageIndex - 1);
}
return newIcon;
}
private static View handleNextPageFirstItem(Workspace workspace, CellLayout hotseatLayout,
int pageIndex, boolean isRtl) {
if (pageIndex + 1 >= workspace.getPageCount()) {
return null;
}
CellLayout workspaceLayout = (CellLayout) workspace.getChildAt(pageIndex + 1);
View newIcon = getFirstFocusableIconInReadingOrder(workspaceLayout, isRtl);
if (newIcon == null) {
// Check the hotseat if no focusable item was found on the workspace.
newIcon = getFirstFocusableIconInReadingOrder(hotseatLayout, isRtl);
workspace.snapToPage(pageIndex + 1);
}
return newIcon;
}
private static View getFirstFocusableIconInReadingOrder(CellLayout cellLayout, boolean isRtl) {
View icon;
int countX = cellLayout.getCountX();
for (int y = 0; y < cellLayout.getCountY(); y++) {
int increment = isRtl ? -1 : 1;
for (int x = isRtl ? countX - 1 : 0; 0 <= x && x < countX; x += increment) {
if ((icon = cellLayout.getChildAt(x, y)) != null && icon.isFocusable()) {
return icon;
}
}
}
return null;
}
private static View getFirstFocusableIconInReverseReadingOrder(CellLayout cellLayout,
boolean isRtl) {
View icon;
int countX = cellLayout.getCountX();
for (int y = cellLayout.getCountY() - 1; y >= 0; y--) {
int increment = isRtl ? 1 : -1;
for (int x = isRtl ? 0 : countX - 1; 0 <= x && x < countX; x += increment) {
if ((icon = cellLayout.getChildAt(x, y)) != null && icon.isFocusable()) {
return icon;
}
}
}
return null;
}
}