Merge "Add the KeyboardQuickSwitchView (1/2)" into tm-qpr-dev
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
index bd11c1e..1642fd4 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -17,11 +17,14 @@
file, they need to be loaded at runtime. -->
<com.android.quickstep.views.TaskView
xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ xmlns:launcher="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:defaultFocusHighlightEnabled="false"
- android:focusable="true">
+ android:focusable="true"
+ launcher:borderColor="?androidprv:attr/colorAccentSecondaryVariant">
<com.android.quickstep.views.TaskThumbnailView
android:id="@+id/snapshot"
diff --git a/quickstep/res/layout/task_grouped.xml b/quickstep/res/layout/task_grouped.xml
index ec03c69..a8d5b50 100644
--- a/quickstep/res/layout/task_grouped.xml
+++ b/quickstep/res/layout/task_grouped.xml
@@ -22,11 +22,14 @@
<com.android.quickstep.views.GroupedTaskView
xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ xmlns:launcher="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:defaultFocusHighlightEnabled="false"
- android:focusable="true">
+ android:focusable="true"
+ launcher:borderColor="?androidprv:attr/colorAccentSecondaryVariant">
<com.android.quickstep.views.TaskThumbnailView
android:id="@+id/snapshot"
diff --git a/quickstep/res/values/attrs.xml b/quickstep/res/values/attrs.xml
index 336fb57..f1d4dab 100644
--- a/quickstep/res/values/attrs.xml
+++ b/quickstep/res/values/attrs.xml
@@ -19,4 +19,13 @@
<attr name="android:textSize"/>
<attr name="android:fontFamily"/>
</declare-styleable>
+
+ <!--
+ TaskView specific attributes. These attributes are used to customize a TaskView view in
+ XML files.
+ -->
+ <declare-styleable name="TaskView">
+ <!-- Border color for a keyboard quick switch task views -->
+ <attr name="borderColor" format="color" />
+ </declare-styleable>
</resources>
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 3846a9c..fb04cc0 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -328,4 +328,7 @@
<!-- Note: keep this value in sync with the WindowManager/Shell dimens.xml -->
<!-- starting_surface_exit_animation_window_shift_length -->
<dimen name="starting_surface_exit_animation_window_shift_length">20dp</dimen>
+
+ <!-- Keyboard Quick Switch -->
+ <dimen name="keyboard_quick_switch_border_width">4dp</dimen>
</resources>
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 335482c..0b6e5d3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -29,6 +29,7 @@
import android.os.RemoteException;
import android.util.Log;
import android.view.TaskTransitionSpec;
+import android.view.View;
import android.view.WindowManagerGlobal;
import androidx.annotation.NonNull;
@@ -49,6 +50,7 @@
import com.android.launcher3.util.MultiPropertyFactory;
import com.android.launcher3.util.OnboardingPrefs;
import com.android.quickstep.RecentsAnimationCallbacks;
+import com.android.quickstep.util.GroupTask;
import com.android.quickstep.views.RecentsView;
import java.io.PrintWriter;
@@ -380,6 +382,17 @@
}
@Override
+ public RecentsView getRecentsView() {
+ return mLauncher.getOverviewPanel();
+ }
+
+ @Override
+ public void launchSplitTasks(View taskView, GroupTask groupTask) {
+ super.launchSplitTasks(taskView, groupTask);
+ mLauncher.launchSplitTasks(taskView, groupTask);
+ }
+
+ @Override
public void dumpLogs(String prefix, PrintWriter pw) {
super.dumpLogs(prefix, pw);
@@ -399,9 +412,4 @@
mTaskbarLauncherStateController.dumpLogs(prefix + "\t", pw);
}
-
- @Override
- public RecentsView getRecentsView() {
- return mLauncher.getOverviewPanel();
- }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index bfdf156..a388388 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -29,6 +29,7 @@
import com.android.launcher3.model.data.ItemInfoWithIcon;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.SplitConfigurationOptions;
+import com.android.quickstep.util.GroupTask;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.TaskView;
import com.android.quickstep.views.TaskView.TaskIdAttributeContainer;
@@ -228,4 +229,11 @@
}
);
}
+
+ /**
+ * Launches the focused task in splitscreen.
+ *
+ * No-op if the view is not yet open.
+ */
+ public void launchSplitTasks(View taskview, GroupTask groupTask) { }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
index d91b650..ec64128 100644
--- a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
@@ -27,17 +27,45 @@
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
+import androidx.annotation.NonNull;
+
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
import com.android.launcher3.util.TouchController;
import com.android.launcher3.views.BaseDragLayer;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
/** Root drag layer for the Taskbar overlay window. */
public class TaskbarOverlayDragLayer extends
BaseDragLayer<TaskbarOverlayContext> implements
ViewTreeObserver.OnComputeInternalInsetsListener {
+ private final List<OnClickListener> mOnClickListeners = new CopyOnWriteArrayList<>();
+ private final TouchController mClickListenerTouchController = new TouchController() {
+ @Override
+ public boolean onControllerTouchEvent(MotionEvent ev) {
+ if (ev.getActionMasked() == MotionEvent.ACTION_UP) {
+ for (OnClickListener listener : mOnClickListeners) {
+ listener.onClick(TaskbarOverlayDragLayer.this);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+ for (int i = 0; i < getChildCount(); i++) {
+ if (isEventOverView(getChildAt(i), ev)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ };
+
TaskbarOverlayDragLayer(Context context) {
super(context, null, 1);
setClipChildren(false);
@@ -58,7 +86,10 @@
@Override
public void recreateControllers() {
- mControllers = new TouchController[]{mActivity.getDragController()};
+ mControllers = mOnClickListeners.isEmpty()
+ ? new TouchController[]{mActivity.getDragController()}
+ : new TouchController[] {
+ mActivity.getDragController(), mClickListenerTouchController};
}
@Override
@@ -99,6 +130,51 @@
}
/**
+ * Adds the given callback to clicks to this drag layer.
+ * <p>
+ * Clicks are only accepted on this drag layer if they fall within this drag layer's bounds and
+ * outside the bounds of all child views.
+ * <p>
+ * If the click falls within the bounds of a child view, then this callback does not run and
+ * that child can optionally handle it.
+ */
+ private void addOnClickListener(@NonNull OnClickListener listener) {
+ boolean wasEmpty = mOnClickListeners.isEmpty();
+ mOnClickListeners.add(listener);
+ if (wasEmpty) {
+ recreateControllers();
+ }
+ }
+
+ /**
+ * Removes the given on click callback.
+ * <p>
+ * No-op if the callback was never added.
+ */
+ private void removeOnClickListener(@NonNull OnClickListener listener) {
+ boolean wasEmpty = mOnClickListeners.isEmpty();
+ mOnClickListeners.remove(listener);
+ if (!wasEmpty && mOnClickListeners.isEmpty()) {
+ recreateControllers();
+ }
+ }
+
+ /**
+ * Queues the given callback on the next click on this drag layer.
+ * <p>
+ * Once run, this callback is immediately removed.
+ */
+ public void runOnClickOnce(@NonNull OnClickListener listener) {
+ addOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ listener.onClick(v);
+ removeOnClickListener(this);
+ }
+ });
+ }
+
+ /**
* Taskbar automatically stashes when opening all apps, but we don't report the insets as
* changing to avoid moving the underlying app. But internally, the apps view should still
* layout according to the stashed insets rather than the unstashed insets. So this method
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index fd986e6..80ce369 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -50,6 +50,7 @@
import static com.android.launcher3.util.DisplayController.CHANGE_ACTIVE_SCREEN;
import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.launcher3.util.SplitConfigurationOptions.DEFAULT_SPLIT_RATIO;
import static com.android.quickstep.util.SplitAnimationTimings.TABLET_HOME_TO_SPLIT;
import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY;
@@ -143,6 +144,7 @@
import com.android.launcher3.util.PendingRequestArgs;
import com.android.launcher3.util.PendingSplitSelectInfo;
import com.android.launcher3.util.RunnableList;
+import com.android.launcher3.util.SplitConfigurationOptions;
import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource;
import com.android.launcher3.util.TouchController;
@@ -152,6 +154,7 @@
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.TaskUtils;
import com.android.quickstep.TouchInteractionService.TISBinder;
+import com.android.quickstep.util.GroupTask;
import com.android.quickstep.util.LauncherUnfoldAnimationController;
import com.android.quickstep.util.ProxyScreenStatusProvider;
import com.android.quickstep.util.QuickstepOnboardingPrefs;
@@ -1206,6 +1209,32 @@
getDeviceProfile().toSmallString());
}
+ /**
+ * Launches the given {@link GroupTask} in splitscreen.
+ *
+ * If the second split task is missing, launches the first task normally.
+ */
+ public void launchSplitTasks(View taskView, GroupTask groupTask) {
+ if (groupTask.task2 == null) {
+ UI_HELPER_EXECUTOR.execute(() ->
+ ActivityManagerWrapper.getInstance().startActivityFromRecents(
+ groupTask.task1.key,
+ getActivityLaunchOptions(taskView, null).options));
+ return;
+ }
+ mSplitSelectStateController.launchTasks(
+ groupTask.task1.key.id,
+ groupTask.task2.key.id,
+ SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT,
+ /* callback= */ success -> {},
+ /* freezeTaskList= */ true,
+ groupTask.mSplitBounds == null
+ ? DEFAULT_SPLIT_RATIO
+ : groupTask.mSplitBounds.appsStackedVertically
+ ? groupTask.mSplitBounds.topTaskPercent
+ : groupTask.mSplitBounds.leftTaskPercent);
+ }
+
private static final class LauncherTaskViewController extends
TaskViewTouchController<Launcher> {
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
index 5a09e02..b5240fd 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
@@ -24,8 +24,10 @@
import android.os.Build;
import android.os.SystemClock;
import android.os.Trace;
+import android.view.View;
import androidx.annotation.BinderThread;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
@@ -48,7 +50,7 @@
public class OverviewCommandHelper {
public static final int TYPE_SHOW = 1;
- public static final int TYPE_SHOW_NEXT_FOCUS = 2;
+ public static final int TYPE_KEYBOARD_INPUT = 2;
public static final int TYPE_HIDE = 3;
public static final int TYPE_TOGGLE = 4;
public static final int TYPE_HOME = 5;
@@ -66,6 +68,13 @@
private final TaskAnimationManager mTaskAnimationManager;
private final ArrayList<CommandInfo> mPendingCommands = new ArrayList<>();
+ /**
+ * Index of the TaskView that should be focused when launching Overview. Persisted so that we
+ * do not lose the focus across multiple calls of
+ * {@link OverviewCommandHelper#executeCommand(CommandInfo)} for the same command
+ */
+ private int mTaskFocusIndexOverride = -1;
+
public OverviewCommandHelper(TouchInteractionService service,
OverviewComponentObserver observer,
TaskAnimationManager taskAnimationManager) {
@@ -179,6 +188,7 @@
// already visible
return true;
case TYPE_HIDE: {
+ mTaskFocusIndexOverride = -1;
int currentPage = recents.getNextPage();
TaskView tv = (currentPage >= 0 && currentPage < recents.getTaskViewCount())
? (TaskView) recents.getPageAt(currentPage)
@@ -194,15 +204,9 @@
}
final Runnable completeCallback = () -> {
- if (cmd.type == TYPE_SHOW_NEXT_FOCUS) {
- RecentsView rv = activityInterface.getVisibleRecentsView();
- // When the overview is launched via alt tab (cmd type is TYPE_SHOW_NEXT_FOCUS),
- // the touch mode somehow is not change to false by the Android framework.
- // The subsequent tab to go through tasks in overview can only be dispatched to
- // focuses views, while focus can only be requested in
- // {@link View#requestFocusNoSearch(int, Rect)} when touch mode is false. To note,
- // here we launch overview from home.
- rv.getViewRootImpl().touchModeChanged(false);
+ RecentsView rv = activityInterface.getVisibleRecentsView();
+ if (rv != null && (cmd.type == TYPE_KEYBOARD_INPUT || cmd.type == TYPE_HIDE)) {
+ updateRecentsViewFocus(rv);
}
scheduleNextTask(cmd);
};
@@ -280,40 +284,55 @@
cmd.removeListener(handler);
Trace.endAsyncSection(TRANSITION_NAME, 0);
- if (cmd.type == TYPE_SHOW_NEXT_FOCUS) {
- RecentsView rv =
- mOverviewComponentObserver.getActivityInterface().getVisibleRecentsView();
- if (rv != null) {
- // When the overview is launched via alt tab (cmd type is TYPE_SHOW_NEXT_FOCUS),
- // the touch mode somehow is not change to false by the Android framework.
- // The subsequent tab to go through tasks in overview can only be dispatched to
- // focuses views, while focus can only be requested in
- // {@link View#requestFocusNoSearch(int, Rect)} when touch mode is false. To note,
- // here we launch overview with live tile.
- rv.getViewRootImpl().touchModeChanged(false);
- // Ensure that recents view has focus so that it receives the followup key inputs
- TaskView taskView = rv.getNextTaskView();
- if (taskView == null) {
- taskView = rv.getTaskViewAt(0);
- if (taskView != null) {
- taskView.requestFocus();
- } else {
- rv.requestFocus();
- }
- } else {
- taskView.requestFocus();
- }
- }
+ RecentsView rv =
+ mOverviewComponentObserver.getActivityInterface().getVisibleRecentsView();
+ if (rv != null && (cmd.type == TYPE_KEYBOARD_INPUT || cmd.type == TYPE_HIDE)) {
+ updateRecentsViewFocus(rv);
}
scheduleNextTask(cmd);
}
+ private void updateRecentsViewFocus(@NonNull RecentsView rv) {
+ // When the overview is launched via alt tab (cmd type is TYPE_KEYBOARD_INPUT),
+ // the touch mode somehow is not change to false by the Android framework.
+ // The subsequent tab to go through tasks in overview can only be dispatched to
+ // focuses views, while focus can only be requested in
+ // {@link View#requestFocusNoSearch(int, Rect)} when touch mode is false. To note,
+ // here we launch overview with live tile.
+ rv.getViewRootImpl().touchModeChanged(false);
+ // Ensure that recents view has focus so that it receives the followup key inputs
+ TaskView taskView = rv.getTaskViewAt(mTaskFocusIndexOverride);
+ if (taskView != null) {
+ requestFocus(taskView);
+ return;
+ }
+ taskView = rv.getNextTaskView();
+ if (taskView != null) {
+ requestFocus(taskView);
+ return;
+ }
+ taskView = rv.getTaskViewAt(0);
+ if (taskView != null) {
+ requestFocus(taskView);
+ return;
+ }
+ requestFocus(rv);
+ }
+
+ private void requestFocus(@NonNull View view) {
+ view.post(() -> {
+ view.requestFocus();
+ view.requestAccessibilityFocus();
+ });
+ }
+
public void dump(PrintWriter pw) {
pw.println("OverviewCommandHelper:");
pw.println(" mPendingCommands=" + mPendingCommands.size());
if (!mPendingCommands.isEmpty()) {
pw.println(" pendingCommandType=" + mPendingCommands.get(0).type);
}
+ pw.println(" mTaskFocusIndexOverride=" + mTaskFocusIndexOverride);
}
private static class CommandInfo {
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 287b468..1b8a93c 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -205,7 +205,7 @@
public void onOverviewShown(boolean triggeredFromAltTab) {
if (triggeredFromAltTab) {
TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
- mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_SHOW_NEXT_FOCUS);
+ mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_KEYBOARD_INPUT);
} else {
mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_SHOW);
}
diff --git a/quickstep/src/com/android/quickstep/util/BorderAnimator.java b/quickstep/src/com/android/quickstep/util/BorderAnimator.java
new file mode 100644
index 0000000..532edb2
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/BorderAnimator.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2023 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.quickstep.util;
+
+import android.animation.Animator;
+import android.annotation.ColorInt;
+import android.annotation.Nullable;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.view.animation.Interpolator;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Px;
+
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimatedFloat;
+import com.android.launcher3.anim.AnimatorListeners;
+import com.android.launcher3.anim.Interpolators;
+
+/**
+ * Utility class for drawing a rounded-rect border around a view.
+ * <p>
+ * To use this class:
+ * 1. Create an instance in the target view.
+ * 2. Override the target view's {@link android.view.View#draw(Canvas)} method and call
+ * {@link BorderAnimator#drawBorder(Canvas)} after {@code super.draw(canvas)}.
+ * 3. Call {@link BorderAnimator#buildAnimator(boolean)} and start the animation where appropriate.
+ */
+public final class BorderAnimator {
+
+ public static final int DEFAULT_BORDER_COLOR = 0xffffffff;
+
+ private static final long DEFAULT_APPEARANCE_ANIMATION_DURATION_MS = 300;
+ private static final long DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS = 133;
+ private static final Interpolator DEFAULT_INTERPOLATOR = Interpolators.EMPHASIZED_DECELERATE;
+
+ @NonNull private final AnimatedFloat mBorderAnimationProgress = new AnimatedFloat(
+ this::updateOutline);
+ @NonNull private final Rect mBorderBounds = new Rect();
+ @NonNull private final BorderBoundsBuilder mBorderBoundsBuilder;
+ @Px private final int mBorderWidthPx;
+ @Px private final int mBorderRadiusPx;
+ @NonNull private final Runnable mInvalidateViewCallback;
+ private final long mAppearanceDurationMs;
+ private final long mDisappearanceDurationMs;
+ @NonNull private final Interpolator mInterpolator;
+ @NonNull private final Paint mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ private int mAlignmentAdjustment;
+
+ @Nullable private Animator mRunningBorderAnimation;
+
+ public BorderAnimator(
+ @NonNull BorderBoundsBuilder borderBoundsBuilder,
+ int borderWidthPx,
+ int borderRadiusPx,
+ @ColorInt int borderColor,
+ @NonNull Runnable invalidateViewCallback) {
+ this(borderBoundsBuilder,
+ borderWidthPx,
+ borderRadiusPx,
+ borderColor,
+ invalidateViewCallback,
+ DEFAULT_APPEARANCE_ANIMATION_DURATION_MS,
+ DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS,
+ DEFAULT_INTERPOLATOR);
+ }
+
+ public BorderAnimator(
+ @NonNull BorderBoundsBuilder borderBoundsBuilder,
+ int borderWidthPx,
+ int borderRadiusPx,
+ @ColorInt int borderColor,
+ @NonNull Runnable invalidateViewCallback,
+ long appearanceDurationMs,
+ long disappearanceDurationMs,
+ @NonNull Interpolator interpolator) {
+ mBorderBoundsBuilder = borderBoundsBuilder;
+ mBorderWidthPx = borderWidthPx;
+ mBorderRadiusPx = borderRadiusPx;
+ mInvalidateViewCallback = invalidateViewCallback;
+ mAppearanceDurationMs = appearanceDurationMs;
+ mDisappearanceDurationMs = disappearanceDurationMs;
+ mInterpolator = interpolator;
+
+ mBorderPaint.setColor(borderColor);
+ mBorderPaint.setStyle(Paint.Style.STROKE);
+ mBorderPaint.setAlpha(0);
+ }
+
+ private void updateOutline() {
+ float interpolatedProgress = mInterpolator.getInterpolation(
+ mBorderAnimationProgress.value);
+ mAlignmentAdjustment = (int) Utilities.mapBoundToRange(
+ mBorderAnimationProgress.value,
+ /* lowerBound= */ 0f,
+ /* upperBound= */ 1f,
+ /* toMin= */ 0f,
+ /* toMax= */ (float) (mBorderWidthPx / 2f),
+ mInterpolator);
+
+ mBorderPaint.setAlpha(Math.round(255 * interpolatedProgress));
+ mBorderPaint.setStrokeWidth(Math.round(mBorderWidthPx * interpolatedProgress));
+ mInvalidateViewCallback.run();
+ }
+
+ /**
+ * Draws the border on the given canvas.
+ * <p>
+ * Call this method in the target view's {@link android.view.View#draw(Canvas)} method after
+ * calling super.
+ */
+ public void drawBorder(Canvas canvas) {
+ canvas.drawRoundRect(
+ /* left= */ mBorderBounds.left + mAlignmentAdjustment,
+ /* top= */ mBorderBounds.top + mAlignmentAdjustment,
+ /* right= */ mBorderBounds.right - mAlignmentAdjustment,
+ /* bottom= */ mBorderBounds.bottom - mAlignmentAdjustment,
+ /* rx= */ mBorderRadiusPx - mAlignmentAdjustment,
+ /* ry= */ mBorderRadiusPx - mAlignmentAdjustment,
+ /* paint= */ mBorderPaint);
+ }
+
+ /**
+ * Builds the border appearance/disappearance animation.
+ */
+ public Animator buildAnimator(boolean isAppearing) {
+ mBorderBoundsBuilder.updateBorderBounds(mBorderBounds);
+ mRunningBorderAnimation = mBorderAnimationProgress.animateToValue(isAppearing ? 1f : 0f);
+ mRunningBorderAnimation.setDuration(
+ isAppearing ? mAppearanceDurationMs : mDisappearanceDurationMs);
+
+ mRunningBorderAnimation.addListener(
+ AnimatorListeners.forEndCallback(() -> mRunningBorderAnimation = null));
+
+ return mRunningBorderAnimation;
+ }
+
+ /**
+ * Callback to update the border bounds when building this animation.
+ */
+ public interface BorderBoundsBuilder {
+
+ /**
+ * Sets the given rect to the most up-to-date bounds.
+ */
+ void updateBorderBounds(Rect rect);
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
index 5cf79ea..e9498fd 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
@@ -5,6 +5,7 @@
import android.content.Context;
import android.graphics.PointF;
+import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
@@ -72,6 +73,23 @@
}
@Override
+ protected void updateBorderBounds(Rect bounds) {
+ if (mSplitBoundsConfig == null) {
+ super.updateBorderBounds(bounds);
+ return;
+ }
+ bounds.set(
+ Math.min(mSnapshotView.getLeft() + Math.round(mSnapshotView.getTranslationX()),
+ mSnapshotView2.getLeft() + Math.round(mSnapshotView2.getTranslationX())),
+ Math.min(mSnapshotView.getTop() + Math.round(mSnapshotView.getTranslationY()),
+ mSnapshotView2.getTop() + Math.round(mSnapshotView2.getTranslationY())),
+ Math.max(mSnapshotView.getRight() + Math.round(mSnapshotView.getTranslationX()),
+ mSnapshotView2.getRight() + Math.round(mSnapshotView2.getTranslationX())),
+ Math.max(mSnapshotView.getBottom() + Math.round(mSnapshotView.getTranslationY()),
+ mSnapshotView2.getBottom() + Math.round(mSnapshotView2.getTranslationY())));
+ }
+
+ @Override
protected void onFinishInflate() {
super.onFinishInflate();
mSnapshotView2 = findViewById(R.id.bottomright_snapshot);
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index d3c7778..0e2f020 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -31,6 +31,7 @@
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED;
import static com.android.launcher3.util.SplitConfigurationOptions.getLogEventForPosition;
+import static com.android.quickstep.util.BorderAnimator.DEFAULT_BORDER_COLOR;
import static java.lang.annotation.RetentionPolicy.SOURCE;
@@ -42,6 +43,7 @@
import android.app.ActivityOptions;
import android.content.Context;
import android.content.Intent;
+import android.graphics.Canvas;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
@@ -94,6 +96,7 @@
import com.android.quickstep.TaskThumbnailCache;
import com.android.quickstep.TaskUtils;
import com.android.quickstep.TaskViewUtils;
+import com.android.quickstep.util.BorderAnimator;
import com.android.quickstep.util.CancellableTask;
import com.android.quickstep.util.RecentsOrientedState;
import com.android.quickstep.util.SplitSelectStateController;
@@ -405,6 +408,8 @@
private boolean mIsClickableAsLiveTile = true;
+ @Nullable private final BorderAnimator mBorderAnimator;
+
public TaskView(Context context) {
this(context, null);
}
@@ -414,12 +419,46 @@
}
public TaskView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public TaskView(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
mActivity = StatefulActivity.fromContext(context);
setOnClickListener(this::onClick);
mCurrentFullscreenParams = new FullscreenDrawParams(context);
mDigitalWellBeingToast = new DigitalWellBeingToast(mActivity, this);
+
+ setWillNotDraw(!FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH.get());
+
+ mBorderAnimator = !FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH.get()
+ ? null
+ : new BorderAnimator(
+ /* borderBoundsBuilder= */ this::updateBorderBounds,
+ /* borderWidthPx= */ context.getResources().getDimensionPixelSize(
+ R.dimen.keyboard_quick_switch_border_width),
+ /* borderRadiusPx= */ (int) mCurrentFullscreenParams.mCornerRadius,
+ /* borderColor= */ attrs == null
+ ? DEFAULT_BORDER_COLOR
+ : context.getTheme()
+ .obtainStyledAttributes(
+ attrs,
+ R.styleable.TaskView,
+ defStyleAttr,
+ defStyleRes)
+ .getColor(
+ R.styleable.TaskView_borderColor,
+ DEFAULT_BORDER_COLOR),
+ /* invalidateViewCallback= */ TaskView.this::invalidate);
+ }
+
+ protected void updateBorderBounds(Rect bounds) {
+ bounds.set(mSnapshotView.getLeft() + Math.round(mSnapshotView.getTranslationX()),
+ mSnapshotView.getTop() + Math.round(mSnapshotView.getTranslationY()),
+ mSnapshotView.getRight() + Math.round(mSnapshotView.getTranslationX()),
+ mSnapshotView.getBottom() + Math.round(mSnapshotView.getTranslationY()));
}
public void setTaskViewId(int id) {
@@ -463,6 +502,22 @@
mIconTouchDelegate = new TransformingTouchDelegate(mIconView);
}
+ @Override
+ protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
+ super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
+ if (mBorderAnimator != null) {
+ mBorderAnimator.buildAnimator(gainFocus).start();
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+ if (mBorderAnimator != null) {
+ mBorderAnimator.drawBorder(canvas);
+ }
+ }
+
/**
* Whether the taskview should take the touch event from parent. Events passed to children
* that might require special handling.
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 4b0b1ff..7b0033d 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -411,6 +411,10 @@
"Enables receiving unfold animation events from sysui instead of calculating "
+ "them in launcher process using hinge sensor values.");
+ public static final BooleanFlag ENABLE_KEYBOARD_QUICK_SWITCH = getDebugFlag(
+ "ENABLE_KEYBOARD_QUICK_SWITCH", false,
+ "Enables keyboard quick switching");
+
public static void initialize(Context context) {
synchronized (sDebugFlags) {
for (DebugFlag flag : sDebugFlags) {