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_task_view_header_height">44dp</dimen>
<dimen name="recents_grid_task_view_header_button_padding">8dp</dimen>
<dimen name="recents_grid_task_view_focused_frame_thickness">8dp</dimen>
</resources>

View File

@@ -50,7 +50,7 @@ public class RecentsActivityLaunchState {
/**
* 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();
RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState();
if (launchedFromApp) {
@@ -66,6 +66,11 @@ public class RecentsActivityLaunchState {
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
return Math.max(0, numTasks - 2);
} 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.PackagesChangedEvent;
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.DeleteTaskDataEvent;
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.views.grid.GridTaskView;
import com.android.systemui.recents.views.grid.TaskViewFocusFrame;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -206,6 +208,10 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
private int mLastWidth;
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
private ValueAnimator.AnimatorUpdateListener mRequestUpdateClippingListener =
new ValueAnimator.AnimatorUpdateListener() {
@@ -265,6 +271,14 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
mDisplayOrientation = Utilities.getAppConfiguration(mContext).orientation;
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(
R.integer.recents_task_bar_dismiss_delay_seconds);
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
*/
private boolean setFocusedTask(int taskIndex, boolean scrollToTask,
public boolean setFocusedTask(int taskIndex, boolean scrollToTask,
final boolean requestViewFocus) {
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
*/
private boolean setFocusedTask(int focusTaskIndex, boolean scrollToTask,
public boolean setFocusedTask(int focusTaskIndex, boolean scrollToTask,
boolean requestViewFocus, int timerIndicatorDuration) {
// Find the next task to focus
int newFocusedTaskIndex = mStack.getTaskCount() > 0 ?
@@ -940,6 +954,10 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
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;
}
@@ -1005,20 +1023,28 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
float stackScroll = mStackScroller.getStackScroll();
ArrayList<Task> tasks = mStack.getStackTasks();
int taskCount = tasks.size();
if (forward) {
// Walk backwards and focus the next task smaller than the current stack scroll
for (newIndex = taskCount - 1; newIndex >= 0; newIndex--) {
float taskP = mLayoutAlgorithm.getStackScrollForTask(tasks.get(newIndex));
if (Float.compare(taskP, stackScroll) <= 0) {
break;
}
}
if (useGridLayout()) {
// For the grid layout, we directly set focus to the most recently used task
// no matter we're moving forwards or backwards.
newIndex = taskCount - 1;
} 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;
// For the grid layout we pick a proper task to focus, according to the current
// stack scroll.
if (forward) {
// Walk backwards and focus the next task smaller than the current stack scroll
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.
*/
void resetFocusedTask(Task task) {
public void resetFocusedTask(Task task) {
if (task != null) {
TaskView tv = getChildViewForTask(task);
if (tv != null) {
tv.setFocusedState(false, false /* requestViewFocus */);
}
}
if (mTaskViewFocusFrame != null) {
mTaskViewFocusFrame.moveGridTaskViewFocus(null);
}
mFocusedTask = null;
}
/**
* Returns the focused task.
*/
Task getFocusedTask() {
public Task getFocusedTask() {
return mFocusedTask;
}
@@ -1253,6 +1282,9 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
for (int i = 0; i < taskViewCount; i++) {
measureTaskView(mTmpTaskViews.get(i));
}
if (mTaskViewFocusFrame != null) {
mTaskViewFocusFrame.measure();
}
setMeasuredDimension(width, height);
mLastWidth = width;
@@ -1287,6 +1319,9 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
for (int i = 0; i < taskViewCount; i++) {
layoutTaskView(changed, mTmpTaskViews.get(i));
}
if (mTaskViewFocusFrame != null) {
mTaskViewFocusFrame.layout();
}
if (changed) {
if (mStackScroller.isScrollOutOfBounds()) {
@@ -1339,10 +1374,19 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
// until after the enter-animation
RecentsConfiguration config = Recents.getConfiguration();
RecentsActivityLaunchState launchState = config.getLaunchState();
int focusedTaskIndex = launchState.getInitialFocusTaskIndex(mStack.getTaskCount());
if (focusedTaskIndex != -1) {
setFocusedTask(focusedTaskIndex, false /* scrollToTask */,
false /* requestViewFocus */);
// We set the initial focused task view iff the following conditions are satisfied:
// 1. Recents is showing task views in stack layout.
// 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();
}
@@ -1443,6 +1487,11 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
// Remove the task from the ignored set
removeIgnoreTask(removedTask);
// Resize the grid layout task view focus frame
if (mTaskViewFocusFrame != null) {
mTaskViewFocusFrame.resize();
}
// If requested, relayout with the given animation
if (animation != null) {
updateLayoutAlgorithm(true /* boundScroll */);
@@ -1740,10 +1789,18 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
int taskViewExitToHomeDuration = TaskStackAnimationHelper.EXIT_TO_HOME_TRANSLATION_DURATION;
animateFreeformWorkspaceBackgroundAlpha(0, new AnimationProps(taskViewExitToHomeDuration,
Interpolators.FAST_OUT_SLOW_IN));
// Dismiss the grid task view focus frame
if (mTaskViewFocusFrame != null) {
mTaskViewFocusFrame.moveGridTaskViewFocus(null);
}
}
public final void onBusEvent(DismissFocusedTaskViewEvent event) {
if (mFocusedTask != null) {
if (mTaskViewFocusFrame != null) {
mTaskViewFocusFrame.moveGridTaskViewFocus(null);
}
TaskView tv = getChildViewForTask(mFocusedTask);
if (tv != null) {
tv.dismissTask();
@@ -2073,6 +2130,12 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
mResetToInitialStateWhenResized = true;
}
public final void onBusEvent(RecentsVisibilityChangedEvent event) {
if (!event.visible && mTaskViewFocusFrame != null) {
mTaskViewFocusFrame.moveGridTaskViewFocus(null);
}
}
public void reloadOnConfigurationChange() {
mStableLayoutAlgorithm.reloadOnConfigurationChange(getContext());
mLayoutAlgorithm.reloadOnConfigurationChange(getContext());

View File

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

View File

@@ -51,6 +51,9 @@ public class TaskGridLayoutAlgorithm {
private float mAppAspectRatio;
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
* 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) {
Resources res = context.getResources();
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();
mTitleBarHeight = res.getDimensionPixelSize(R.dimen.recents_grid_task_view_header_height);
@@ -223,7 +229,18 @@ public class TaskGridLayoutAlgorithm {
return buttonRect;
}
public void updateTaskGridRect(int taskCount) {
if (taskCount > 0) {
TaskGridRectInfo gridInfo = mTaskGridRectInfoList[taskCount - 1];
mTaskGridRect.set(gridInfo.size);
}
}
public Rect getTaskGridRect() {
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);
}
}