From 21f495f07b1b9ace99986a110d01a763847687b1 Mon Sep 17 00:00:00 2001 From: Jiaquan He Date: Thu, 5 Jan 2017 13:00:29 -0800 Subject: [PATCH] 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 --- ...cents_grid_task_view_focus_frame_background.xml | 19 +++ packages/SystemUI/res/values/dimens_grid.xml | 1 + .../recents/RecentsActivityLaunchState.java | 7 +- .../systemui/recents/views/TaskStackView.java | 105 ++++++++++++--- .../recents/views/TaskStackViewTouchHandler.java | 5 +- .../views/grid/TaskGridLayoutAlgorithm.java | 17 +++ .../recents/views/grid/TaskViewFocusFrame.java | 141 +++++++++++++++++++++ 7 files changed, 271 insertions(+), 24 deletions(-) create mode 100644 packages/SystemUI/res/drawable/recents_grid_task_view_focus_frame_background.xml create mode 100644 packages/SystemUI/src/com/android/systemui/recents/views/grid/TaskViewFocusFrame.java diff --git a/packages/SystemUI/res/drawable/recents_grid_task_view_focus_frame_background.xml b/packages/SystemUI/res/drawable/recents_grid_task_view_focus_frame_background.xml new file mode 100644 index 000000000000..a85beb8b68c8 --- /dev/null +++ b/packages/SystemUI/res/drawable/recents_grid_task_view_focus_frame_background.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/values/dimens_grid.xml b/packages/SystemUI/res/values/dimens_grid.xml index 94ffdb18a80d..0b9836ffcebb 100644 --- a/packages/SystemUI/res/values/dimens_grid.xml +++ b/packages/SystemUI/res/values/dimens_grid.xml @@ -21,5 +21,6 @@ 20dp 44dp 8dp + 8dp diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivityLaunchState.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivityLaunchState.java index 914035bc525c..a7f6b70d281e 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivityLaunchState.java +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivityLaunchState.java @@ -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 { diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java index 87a68461eff0..bc2c4249dd84 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java @@ -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 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()); diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java index aeb85d064e84..003138fa75ae 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java @@ -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) { diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/grid/TaskGridLayoutAlgorithm.java b/packages/SystemUI/src/com/android/systemui/recents/views/grid/TaskGridLayoutAlgorithm.java index 6fc4ad74d0f8..365613336fc9 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/grid/TaskGridLayoutAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/grid/TaskGridLayoutAlgorithm.java @@ -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; + } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/grid/TaskViewFocusFrame.java b/packages/SystemUI/src/com/android/systemui/recents/views/grid/TaskViewFocusFrame.java new file mode 100644 index 000000000000..86ed583b07aa --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/views/grid/TaskViewFocusFrame.java @@ -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); + } +} -- 2.11.0