Files
Flexera-Launcher/src/com/android/launcher3/FocusHelper.java
T
Adam Cohen 2e6da1539b Refactoring DeviceProfile
-> Pulling out the parts of device profile which can (and need to be)
   initialized and accessed without access to an Activity context,
   ie. the invariant bits.
-> The invariant bits are stored in InvariantDeviceProfile which is
   initialized statically from LauncherAppState.
-> The DeviceProfile contains the Activity context-dependent bits,
   and we will create one of these for each Activity instance, and
   this instance is accessed through the Launcher activity.
-> It's possible that we can continue to refactor this such that
   all appropriate dimensions can be computed without an Activity
   context (by only specifying orientation). This would be an
   extension of this CL and allow us to know exactly how launcher
   will look in both orientations from any context.

Sets the stage for some improvements around b/19514688

Change-Id: Ia7daccf14d8ca2b9cb340b8780b684769e9f1892
2015-05-21 16:16:40 -07:00

460 lines
20 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.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);
}
}
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 (LauncherAppState.isDogfoodBuild()) {
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 countX = cellLayout.getCountX();
final int countY = cellLayout.getCountY();
final int iconIndex = itemContainer.indexOfChild(v);
final FolderPagedView pagedView = (FolderPagedView) cellLayout.getParent();
final int pageIndex = pagedView.indexOfChild(cellLayout);
final int pageCount = pagedView.getPageCount();
Launcher launcher = (Launcher) v.getContext();
int[][] matrix = FocusLogic.createSparseMatrix(cellLayout);
// Process focus.
int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX,
countY, matrix, iconIndex, pageIndex, pageCount,
launcher.getDeviceProfile().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 : countX - 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(countX - 1, countY - 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.getAdjacentChildInNextPage(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 hot seat (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;
}
DeviceProfile profile = ((Launcher) v.getContext()).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 ShortcutAndWidgetContainer hotseatParent = (ShortcutAndWidgetContainer) v.getParent();
final CellLayout hotseatLayout = (CellLayout) hotseatParent.getParent();
Hotseat hotseat = (Hotseat) hotseatLayout.getParent();
Workspace workspace = (Workspace) v.getRootView().findViewById(R.id.workspace);
int pageIndex = workspace.getNextPage();
int pageCount = workspace.getChildCount();
int countX = -1;
int countY = -1;
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.createSparseMatrix(iconLayout, hotseatLayout,
true /* hotseat horizontal */, hotseat.getAllAppsButtonRank(),
iconRank == hotseat.getAllAppsButtonRank() /* include all apps icon */);
iconIndex += iconParent.getChildCount();
countX = iconLayout.getCountX();
countY = iconLayout.getCountY() + hotseatLayout.getCountY();
parent = iconParent;
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT &&
profile.isVerticalBarLayout()) {
matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout,
false /* hotseat horizontal */, hotseat.getAllAppsButtonRank(),
iconRank == hotseat.getAllAppsButtonRank() /* include all apps icon */);
iconIndex += iconParent.getChildCount();
countX = iconLayout.getCountX() + hotseatLayout.getCountX();
countY = iconLayout.getCountY();
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);
countX = hotseatLayout.getCountX();
countY = hotseatLayout.getCountY();
parent = hotseatParent;
}
// Process the focus.
int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX,
countY, matrix, iconIndex, pageIndex, pageCount, profile.isLayoutRtl);
View newIcon = null;
if (newIconIndex == 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);
}
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) 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.search_drop_target_bar);
final Hotseat hotseat = (Hotseat) dragLayer.findViewById(R.id.hotseat);
final int iconIndex = parent.indexOfChild(v);
final int pageIndex = workspace.indexOfChild(iconLayout);
final int pageCount = workspace.getChildCount();
int countX = iconLayout.getCountX();
int countY = iconLayout.getCountY();
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.createSparseMatrix(iconLayout, hotseatLayout, true /* horizontal */,
hotseat.getAllAppsButtonRank(),
!hotseat.hasIcons() /* ignore all apps icon, unless there are no other icons */);
countY = countY + 1;
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT &&
profile.isVerticalBarLayout()) {
matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, false /* horizontal */,
hotseat.getAllAppsButtonRank(),
!hotseat.hasIcons() /* ignore all apps icon, unless there are no other icons */);
countX = countX + 1;
} else if (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
workspace.removeWorkspaceItem(v);
return consume;
} else {
matrix = FocusLogic.createSparseMatrix(iconLayout);
}
// Process the focus.
int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX,
countY, matrix, iconIndex, pageIndex, pageCount, profile.isLayoutRtl);
View newIcon = null;
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);
workspace.snapToPage(newPageIndex);
if (parent != null) {
workspace.snapToPage(newPageIndex);
iconLayout = (CellLayout) parent.getParent();
matrix = FocusLogic.createSparseMatrix(iconLayout,
iconLayout.getCountX(), row);
newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX + 1, countY,
matrix, FocusLogic.PIVOT, newPageIndex, pageCount, profile.isLayoutRtl);
newIcon = parent.getChildAt(newIconIndex);
}
break;
case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM:
parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
newIcon = parent.getChildAt(0);
workspace.snapToPage(pageIndex - 1);
break;
case FocusLogic.PREVIOUS_PAGE_LAST_ITEM:
parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
newIcon = parent.getChildAt(parent.getChildCount() - 1);
workspace.snapToPage(pageIndex - 1);
break;
case FocusLogic.NEXT_PAGE_FIRST_ITEM:
parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1);
newIcon = parent.getChildAt(0);
workspace.snapToPage(pageIndex + 1);
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;
}
workspace.snapToPage(newPageIndex);
row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY;
parent = getCellLayoutChildrenForIndex(workspace, newPageIndex);
if (parent != null) {
workspace.snapToPage(newPageIndex);
iconLayout = (CellLayout) parent.getParent();
matrix = FocusLogic.createSparseMatrix(iconLayout, -1, row);
newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX + 1, countY,
matrix, FocusLogic.PIVOT, newPageIndex, pageCount, profile.isLayoutRtl);
newIcon = parent.getChildAt(newIconIndex);
}
break;
case FocusLogic.CURRENT_PAGE_FIRST_ITEM:
newIcon = parent.getChildAt(0);
break;
case FocusLogic.CURRENT_PAGE_LAST_ITEM:
newIcon = parent.getChildAt(parent.getChildCount() - 1);
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.
*/
private 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;
}
}
}