Add keyboard support to Grid-based Recents.

Test: Checked that pressing tab, shift + tab, alt + tab, alt + shift +
tab work for task navigation in Recents on local sw600dp device. Also
checked that Recents on phones work properly.
Bug: 32101881
Change-Id: I3e4cd212d2900523ece30a85cae7fb73a9594efb
This commit is contained in:
Jiaquan He
2017-01-05 13:00:29 -08:00
parent 70c0f5c2c7
commit 21f495f07b
7 changed files with 271 additions and 24 deletions

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 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.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#61FFFFFF" />
<corners android:radius="8dp"/>
</shape>

View File

@@ -21,5 +21,6 @@
<dimen name="recents_grid_padding_task_view">20dp</dimen> <dimen name="recents_grid_padding_task_view">20dp</dimen>
<dimen name="recents_grid_task_view_header_height">44dp</dimen> <dimen name="recents_grid_task_view_header_height">44dp</dimen>
<dimen name="recents_grid_task_view_header_button_padding">8dp</dimen> <dimen name="recents_grid_task_view_header_button_padding">8dp</dimen>
<dimen name="recents_grid_task_view_focused_frame_thickness">8dp</dimen>
</resources> </resources>

View File

@@ -50,7 +50,7 @@ public class RecentsActivityLaunchState {
/** /**
* Returns the task to focus given the current launch state. * Returns the task to focus given the current launch state.
*/ */
public int getInitialFocusTaskIndex(int numTasks) { public int getInitialFocusTaskIndex(int numTasks, boolean useGridLayout) {
RecentsDebugFlags debugFlags = Recents.getDebugFlags(); RecentsDebugFlags debugFlags = Recents.getDebugFlags();
RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState(); RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState();
if (launchedFromApp) { if (launchedFromApp) {
@@ -66,6 +66,11 @@ public class RecentsActivityLaunchState {
return numTasks - 1; return numTasks - 1;
} }
if (useGridLayout) {
// If coming from another app to the grid layout, focus the front most task
return numTasks - 1;
}
// If coming from another app, focus the next task // If coming from another app, focus the next task
return Math.max(0, numTasks - 2); return Math.max(0, numTasks - 2);
} else { } else {

View File

@@ -70,6 +70,7 @@ import com.android.systemui.recents.events.activity.LaunchTaskStartedEvent;
import com.android.systemui.recents.events.activity.MultiWindowStateChangedEvent; import com.android.systemui.recents.events.activity.MultiWindowStateChangedEvent;
import com.android.systemui.recents.events.activity.PackagesChangedEvent; import com.android.systemui.recents.events.activity.PackagesChangedEvent;
import com.android.systemui.recents.events.activity.ShowStackActionButtonEvent; import com.android.systemui.recents.events.activity.ShowStackActionButtonEvent;
import com.android.systemui.recents.events.component.RecentsVisibilityChangedEvent;
import com.android.systemui.recents.events.ui.AllTaskViewsDismissedEvent; import com.android.systemui.recents.events.ui.AllTaskViewsDismissedEvent;
import com.android.systemui.recents.events.ui.DeleteTaskDataEvent; import com.android.systemui.recents.events.ui.DeleteTaskDataEvent;
import com.android.systemui.recents.events.ui.DismissAllTaskViewsEvent; import com.android.systemui.recents.events.ui.DismissAllTaskViewsEvent;
@@ -93,6 +94,7 @@ import com.android.systemui.recents.model.Task;
import com.android.systemui.recents.model.TaskStack; import com.android.systemui.recents.model.TaskStack;
import com.android.systemui.recents.views.grid.GridTaskView; import com.android.systemui.recents.views.grid.GridTaskView;
import com.android.systemui.recents.views.grid.TaskViewFocusFrame;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
@@ -206,6 +208,10 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
private int mLastWidth; private int mLastWidth;
private int mLastHeight; private int mLastHeight;
// We keep track of the task view focused by user interaction and draw a frame around it in the
// grid layout.
private TaskViewFocusFrame mTaskViewFocusFrame;
// A convenience update listener to request updating clipping of tasks // A convenience update listener to request updating clipping of tasks
private ValueAnimator.AnimatorUpdateListener mRequestUpdateClippingListener = private ValueAnimator.AnimatorUpdateListener mRequestUpdateClippingListener =
new ValueAnimator.AnimatorUpdateListener() { new ValueAnimator.AnimatorUpdateListener() {
@@ -265,6 +271,14 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
mDisplayOrientation = Utilities.getAppConfiguration(mContext).orientation; mDisplayOrientation = Utilities.getAppConfiguration(mContext).orientation;
mDisplayRect = ssp.getDisplayRect(); mDisplayRect = ssp.getDisplayRect();
// Create a frame to draw around the focused task view
if (Recents.getConfiguration().isGridEnabled) {
mTaskViewFocusFrame = new TaskViewFocusFrame(mContext, this,
mLayoutAlgorithm.mTaskGridLayoutAlgorithm);
addView(mTaskViewFocusFrame);
getViewTreeObserver().addOnGlobalFocusChangeListener(mTaskViewFocusFrame);
}
int taskBarDismissDozeDelaySeconds = getResources().getInteger( int taskBarDismissDozeDelaySeconds = getResources().getInteger(
R.integer.recents_task_bar_dismiss_delay_seconds); R.integer.recents_task_bar_dismiss_delay_seconds);
mUIDozeTrigger = new DozeTrigger(taskBarDismissDozeDelaySeconds, new Runnable() { mUIDozeTrigger = new DozeTrigger(taskBarDismissDozeDelaySeconds, new Runnable() {
@@ -878,7 +892,7 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
* *
* @return whether or not the stack will scroll as a part of this focus change * @return whether or not the stack will scroll as a part of this focus change
*/ */
private boolean setFocusedTask(int taskIndex, boolean scrollToTask, public boolean setFocusedTask(int taskIndex, boolean scrollToTask,
final boolean requestViewFocus) { final boolean requestViewFocus) {
return setFocusedTask(taskIndex, scrollToTask, requestViewFocus, 0); return setFocusedTask(taskIndex, scrollToTask, requestViewFocus, 0);
} }
@@ -888,7 +902,7 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
* *
* @return whether or not the stack will scroll as a part of this focus change * @return whether or not the stack will scroll as a part of this focus change
*/ */
private boolean setFocusedTask(int focusTaskIndex, boolean scrollToTask, public boolean setFocusedTask(int focusTaskIndex, boolean scrollToTask,
boolean requestViewFocus, int timerIndicatorDuration) { boolean requestViewFocus, int timerIndicatorDuration) {
// Find the next task to focus // Find the next task to focus
int newFocusedTaskIndex = mStack.getTaskCount() > 0 ? int newFocusedTaskIndex = mStack.getTaskCount() > 0 ?
@@ -940,6 +954,10 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
newFocusedTaskView.setFocusedState(true, requestViewFocus); newFocusedTaskView.setFocusedState(true, requestViewFocus);
} }
} }
// Any time a task view gets the focus, we move the focus frame around it.
if (mTaskViewFocusFrame != null) {
mTaskViewFocusFrame.moveGridTaskViewFocus(getChildViewForTask(newFocusedTask));
}
} }
return willScroll; return willScroll;
} }
@@ -1005,20 +1023,28 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
float stackScroll = mStackScroller.getStackScroll(); float stackScroll = mStackScroller.getStackScroll();
ArrayList<Task> tasks = mStack.getStackTasks(); ArrayList<Task> tasks = mStack.getStackTasks();
int taskCount = tasks.size(); int taskCount = tasks.size();
if (forward) { if (useGridLayout()) {
// Walk backwards and focus the next task smaller than the current stack scroll // For the grid layout, we directly set focus to the most recently used task
for (newIndex = taskCount - 1; newIndex >= 0; newIndex--) { // no matter we're moving forwards or backwards.
float taskP = mLayoutAlgorithm.getStackScrollForTask(tasks.get(newIndex)); newIndex = taskCount - 1;
if (Float.compare(taskP, stackScroll) <= 0) {
break;
}
}
} else { } else {
// Walk forwards and focus the next task larger than the current stack scroll // For the grid layout we pick a proper task to focus, according to the current
for (newIndex = 0; newIndex < taskCount; newIndex++) { // stack scroll.
float taskP = mLayoutAlgorithm.getStackScrollForTask(tasks.get(newIndex)); if (forward) {
if (Float.compare(taskP, stackScroll) >= 0) { // Walk backwards and focus the next task smaller than the current stack scroll
break; for (newIndex = taskCount - 1; newIndex >= 0; newIndex--) {
float taskP = mLayoutAlgorithm.getStackScrollForTask(tasks.get(newIndex));
if (Float.compare(taskP, stackScroll) <= 0) {
break;
}
}
} else {
// Walk forwards and focus the next task larger than the current stack scroll
for (newIndex = 0; newIndex < taskCount; newIndex++) {
float taskP = mLayoutAlgorithm.getStackScrollForTask(tasks.get(newIndex));
if (Float.compare(taskP, stackScroll) >= 0) {
break;
}
} }
} }
} }
@@ -1037,20 +1063,23 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
/** /**
* Resets the focused task. * Resets the focused task.
*/ */
void resetFocusedTask(Task task) { public void resetFocusedTask(Task task) {
if (task != null) { if (task != null) {
TaskView tv = getChildViewForTask(task); TaskView tv = getChildViewForTask(task);
if (tv != null) { if (tv != null) {
tv.setFocusedState(false, false /* requestViewFocus */); tv.setFocusedState(false, false /* requestViewFocus */);
} }
} }
if (mTaskViewFocusFrame != null) {
mTaskViewFocusFrame.moveGridTaskViewFocus(null);
}
mFocusedTask = null; mFocusedTask = null;
} }
/** /**
* Returns the focused task. * Returns the focused task.
*/ */
Task getFocusedTask() { public Task getFocusedTask() {
return mFocusedTask; return mFocusedTask;
} }
@@ -1253,6 +1282,9 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
for (int i = 0; i < taskViewCount; i++) { for (int i = 0; i < taskViewCount; i++) {
measureTaskView(mTmpTaskViews.get(i)); measureTaskView(mTmpTaskViews.get(i));
} }
if (mTaskViewFocusFrame != null) {
mTaskViewFocusFrame.measure();
}
setMeasuredDimension(width, height); setMeasuredDimension(width, height);
mLastWidth = width; mLastWidth = width;
@@ -1287,6 +1319,9 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
for (int i = 0; i < taskViewCount; i++) { for (int i = 0; i < taskViewCount; i++) {
layoutTaskView(changed, mTmpTaskViews.get(i)); layoutTaskView(changed, mTmpTaskViews.get(i));
} }
if (mTaskViewFocusFrame != null) {
mTaskViewFocusFrame.layout();
}
if (changed) { if (changed) {
if (mStackScroller.isScrollOutOfBounds()) { if (mStackScroller.isScrollOutOfBounds()) {
@@ -1339,10 +1374,19 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
// until after the enter-animation // until after the enter-animation
RecentsConfiguration config = Recents.getConfiguration(); RecentsConfiguration config = Recents.getConfiguration();
RecentsActivityLaunchState launchState = config.getLaunchState(); RecentsActivityLaunchState launchState = config.getLaunchState();
int focusedTaskIndex = launchState.getInitialFocusTaskIndex(mStack.getTaskCount());
if (focusedTaskIndex != -1) { // We set the initial focused task view iff the following conditions are satisfied:
setFocusedTask(focusedTaskIndex, false /* scrollToTask */, // 1. Recents is showing task views in stack layout.
false /* requestViewFocus */); // 2. Recents is launched with ALT + TAB.
boolean setFocusOnFirstLayout = !useGridLayout() ||
Recents.getConfiguration().getLaunchState().launchedWithAltTab;
if (setFocusOnFirstLayout) {
int focusedTaskIndex = launchState.getInitialFocusTaskIndex(mStack.getTaskCount(),
useGridLayout());
if (focusedTaskIndex != -1) {
setFocusedTask(focusedTaskIndex, false /* scrollToTask */,
false /* requestViewFocus */);
}
} }
updateStackActionButtonVisibility(); updateStackActionButtonVisibility();
} }
@@ -1443,6 +1487,11 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
// Remove the task from the ignored set // Remove the task from the ignored set
removeIgnoreTask(removedTask); removeIgnoreTask(removedTask);
// Resize the grid layout task view focus frame
if (mTaskViewFocusFrame != null) {
mTaskViewFocusFrame.resize();
}
// If requested, relayout with the given animation // If requested, relayout with the given animation
if (animation != null) { if (animation != null) {
updateLayoutAlgorithm(true /* boundScroll */); updateLayoutAlgorithm(true /* boundScroll */);
@@ -1740,10 +1789,18 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
int taskViewExitToHomeDuration = TaskStackAnimationHelper.EXIT_TO_HOME_TRANSLATION_DURATION; int taskViewExitToHomeDuration = TaskStackAnimationHelper.EXIT_TO_HOME_TRANSLATION_DURATION;
animateFreeformWorkspaceBackgroundAlpha(0, new AnimationProps(taskViewExitToHomeDuration, animateFreeformWorkspaceBackgroundAlpha(0, new AnimationProps(taskViewExitToHomeDuration,
Interpolators.FAST_OUT_SLOW_IN)); Interpolators.FAST_OUT_SLOW_IN));
// Dismiss the grid task view focus frame
if (mTaskViewFocusFrame != null) {
mTaskViewFocusFrame.moveGridTaskViewFocus(null);
}
} }
public final void onBusEvent(DismissFocusedTaskViewEvent event) { public final void onBusEvent(DismissFocusedTaskViewEvent event) {
if (mFocusedTask != null) { if (mFocusedTask != null) {
if (mTaskViewFocusFrame != null) {
mTaskViewFocusFrame.moveGridTaskViewFocus(null);
}
TaskView tv = getChildViewForTask(mFocusedTask); TaskView tv = getChildViewForTask(mFocusedTask);
if (tv != null) { if (tv != null) {
tv.dismissTask(); tv.dismissTask();
@@ -2073,6 +2130,12 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
mResetToInitialStateWhenResized = true; mResetToInitialStateWhenResized = true;
} }
public final void onBusEvent(RecentsVisibilityChangedEvent event) {
if (!event.visible && mTaskViewFocusFrame != null) {
mTaskViewFocusFrame.moveGridTaskViewFocus(null);
}
}
public void reloadOnConfigurationChange() { public void reloadOnConfigurationChange() {
mStableLayoutAlgorithm.reloadOnConfigurationChange(getContext()); mStableLayoutAlgorithm.reloadOnConfigurationChange(getContext());
mLayoutAlgorithm.reloadOnConfigurationChange(getContext()); mLayoutAlgorithm.reloadOnConfigurationChange(getContext());

View File

@@ -342,8 +342,9 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback {
mSv.invalidate(); mSv.invalidate();
} }
// Reset the focused task after the user has scrolled // Reset the focused task after the user has scrolled, but we have no scrolling
if (!mSv.mTouchExplorationEnabled) { // in grid layout and therefore we don't want to reset the focus there.
if (!mSv.mTouchExplorationEnabled && !mSv.useGridLayout()) {
mSv.resetFocusedTask(mSv.getFocusedTask()); mSv.resetFocusedTask(mSv.getFocusedTask());
} }
} else if (mActiveTaskView == null) { } else if (mActiveTaskView == null) {

View File

@@ -51,6 +51,9 @@ public class TaskGridLayoutAlgorithm {
private float mAppAspectRatio; private float mAppAspectRatio;
private Rect mSystemInsets = new Rect(); private Rect mSystemInsets = new Rect();
/** The thickness of the focused task view frame. */
private int mFocusedFrameThickness;
/** /**
* When the amount of tasks is determined, the size and position of every task view can be * When the amount of tasks is determined, the size and position of every task view can be
* decided. Each instance of TaskGridRectInfo store the task view information for a certain * decided. Each instance of TaskGridRectInfo store the task view information for a certain
@@ -137,6 +140,9 @@ public class TaskGridLayoutAlgorithm {
public void reloadOnConfigurationChange(Context context) { public void reloadOnConfigurationChange(Context context) {
Resources res = context.getResources(); Resources res = context.getResources();
mPaddingTaskView = res.getDimensionPixelSize(R.dimen.recents_grid_padding_task_view); mPaddingTaskView = res.getDimensionPixelSize(R.dimen.recents_grid_padding_task_view);
mFocusedFrameThickness = res.getDimensionPixelSize(
R.dimen.recents_grid_task_view_focused_frame_thickness);
mTaskGridRect = new Rect(); mTaskGridRect = new Rect();
mTitleBarHeight = res.getDimensionPixelSize(R.dimen.recents_grid_task_view_header_height); mTitleBarHeight = res.getDimensionPixelSize(R.dimen.recents_grid_task_view_header_height);
@@ -223,7 +229,18 @@ public class TaskGridLayoutAlgorithm {
return buttonRect; return buttonRect;
} }
public void updateTaskGridRect(int taskCount) {
if (taskCount > 0) {
TaskGridRectInfo gridInfo = mTaskGridRectInfoList[taskCount - 1];
mTaskGridRect.set(gridInfo.size);
}
}
public Rect getTaskGridRect() { public Rect getTaskGridRect() {
return mTaskGridRect; return mTaskGridRect;
} }
public int getFocusFrameThickness() {
return mFocusedFrameThickness;
}
} }

View File

@@ -0,0 +1,141 @@
/*
* Copyright (C) 2017 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.systemui.recents.views.grid;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
import com.android.systemui.R;
import com.android.systemui.recents.model.TaskStack;
import com.android.systemui.recents.views.TaskStackView;
public class TaskViewFocusFrame extends View implements OnGlobalFocusChangeListener {
private TaskStackView mSv;
private TaskGridLayoutAlgorithm mTaskGridLayoutAlgorithm;
public TaskViewFocusFrame(Context context) {
this(context, null);
}
public TaskViewFocusFrame(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TaskViewFocusFrame(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public TaskViewFocusFrame(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
setBackground(mContext.getDrawable(
R.drawable.recents_grid_task_view_focus_frame_background));
setFocusable(false);
hide();
}
public TaskViewFocusFrame(Context context, TaskStackView stackView,
TaskGridLayoutAlgorithm taskGridLayoutAlgorithm) {
this(context);
mSv = stackView;
mTaskGridLayoutAlgorithm = taskGridLayoutAlgorithm;
}
/**
* Measure the width and height of the focus frame according to the current grid task view size.
*/
public void measure() {
int thickness = mTaskGridLayoutAlgorithm.getFocusFrameThickness();
Rect rect = mTaskGridLayoutAlgorithm.getTaskGridRect();
measure(
MeasureSpec.makeMeasureSpec(rect.width() + thickness * 2, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(rect.height() + thickness * 2, MeasureSpec.EXACTLY));
}
/**
* Layout the focus frame with its size.
*/
public void layout() {
layout(0, 0, getMeasuredWidth(), getMeasuredHeight());
}
/**
* Update the current size of grid task view and the focus frame.
*/
public void resize() {
if (mSv.useGridLayout()) {
mTaskGridLayoutAlgorithm.updateTaskGridRect(mSv.getStack().getTaskCount());
measure();
requestLayout();
}
}
/**
* Move the task view focus frame to surround the newly focused view. If it's {@code null} or
* it's not an instance of GridTaskView, we hide the focus frame.
* @param newFocus The newly focused view.
*/
public void moveGridTaskViewFocus(View newFocus) {
if (mSv.useGridLayout()) {
// The frame only shows up in the grid layout. It shouldn't show up in the stack
// layout including when we're in the split screen.
if (newFocus instanceof GridTaskView) {
// If the focus goes to a GridTaskView, we show the frame and layout it.
int[] location = new int[2];
newFocus.getLocationInWindow(location);
int thickness = mTaskGridLayoutAlgorithm.getFocusFrameThickness();
setTranslationX(location[0] - thickness);
setTranslationY(location[1] - thickness);
show();
} else {
// If focus goes to other views, we hide the frame.
hide();
}
}
}
@Override
public void onGlobalFocusChanged(View oldFocus, View newFocus) {
if (!mSv.useGridLayout()) {
return;
}
if (newFocus == null) {
// We're going to touch mode, unset the focus.
moveGridTaskViewFocus(null);
return;
}
if (oldFocus == null) {
// We're returning from touch mode, set the focus to the previously focused task.
final TaskStack stack = mSv.getStack();
final int taskCount = stack.getTaskCount();
final int k = stack.indexOfStackTask(mSv.getFocusedTask());
final int taskIndexToFocus = k == -1 ? (taskCount - 1) : (k % taskCount);
mSv.setFocusedTask(taskIndexToFocus, false, true);
}
}
private void show() {
setAlpha(1f);
}
private void hide() {
setAlpha(0f);
}
}