From f908729fa83d452a9d09564eeb317b8afd95fa7d Mon Sep 17 00:00:00 2001 From: Schneider Victor-tulias Date: Tue, 24 Jan 2023 15:05:08 -0800 Subject: [PATCH] Add the KeyboardQuickSwitchView (1/2) Preparatory change for adding the KeyboardQuickSwitchView and associated flows. Test: Manually tested alt-tab and alt-shift-tab in and out of overview on a tablet and phone Bug: 258854035 Change-Id: I468481a023e82d3ef7c7d4d44c5b9435173b49ae --- quickstep/res/layout/task.xml | 5 +- quickstep/res/layout/task_grouped.xml | 5 +- quickstep/res/values/attrs.xml | 9 + quickstep/res/values/dimens.xml | 3 + .../taskbar/LauncherTaskbarUIController.java | 18 +- .../taskbar/TaskbarUIController.java | 8 + .../overlay/TaskbarOverlayDragLayer.java | 78 ++++++++- .../uioverrides/QuickstepLauncher.java | 29 ++++ .../quickstep/OverviewCommandHelper.java | 87 ++++++---- .../quickstep/TouchInteractionService.java | 2 +- .../quickstep/util/BorderAnimator.java | 163 ++++++++++++++++++ .../quickstep/views/GroupedTaskView.java | 18 ++ .../com/android/quickstep/views/TaskView.java | 57 +++++- .../launcher3/config/FeatureFlags.java | 4 + 14 files changed, 442 insertions(+), 44 deletions(-) create mode 100644 quickstep/src/com/android/quickstep/util/BorderAnimator.java diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml index bd11c1ef05..1642fd40ec 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. --> + android:focusable="true" + launcher:borderColor="?androidprv:attr/colorAccentSecondaryVariant"> + android:focusable="true" + launcher:borderColor="?androidprv:attr/colorAccentSecondaryVariant"> + + + + + + diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml index 3846a9c30e..fb04cc0fae 100644 --- a/quickstep/res/values/dimens.xml +++ b/quickstep/res/values/dimens.xml @@ -328,4 +328,7 @@ 20dp + + + 4dp diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java index 335482c854..0b6e5d312a 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.annotation.ColorInt; 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.DisplayController; 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; @@ -379,6 +381,17 @@ public class LauncherTaskbarUIController extends TaskbarUIController { .getValue() == 0; } + @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 @@ public class LauncherTaskbarUIController extends TaskbarUIController { 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 bfdf156dbd..a38838834b 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.ItemInfo; 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 @@ public class TaskbarUIController { } ); } + + /** + * 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 d91b650311..ec64128c91 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.View; 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 implements ViewTreeObserver.OnComputeInternalInsetsListener { + private final List 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 @@ public class TaskbarOverlayDragLayer extends @Override public void recreateControllers() { - mControllers = new TouchController[]{mActivity.getDragController()}; + mControllers = mOnClickListeners.isEmpty() + ? new TouchController[]{mActivity.getDragController()} + : new TouchController[] { + mActivity.getDragController(), mClickListenerTouchController}; } @Override @@ -98,6 +129,51 @@ public class TaskbarOverlayDragLayer extends mActivity.getOverlayController().maybeCloseWindow(); } + /** + * Adds the given callback to clicks to this drag layer. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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 diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java index fd986e6188..80ce3693bb 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.testing.shared.TestProtocol.QUICK_SWITCH_STA 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.ObjectWrapper; 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.RecentsModel; 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 @@ public class QuickstepLauncher extends Launcher { 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 { diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java index 5a09e021b2..b5240fd214 100644 --- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java +++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java @@ -24,8 +24,10 @@ import android.graphics.PointF; 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 @@ import java.util.HashMap; 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 @@ public class OverviewCommandHelper { private final TaskAnimationManager mTaskAnimationManager; private final ArrayList 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 @@ public class OverviewCommandHelper { // 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 @@ public class OverviewCommandHelper { } 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 @@ public class OverviewCommandHelper { 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 287b468a4a..1b8a93c7ed 100644 --- a/quickstep/src/com/android/quickstep/TouchInteractionService.java +++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java @@ -205,7 +205,7 @@ public class TouchInteractionService extends Service 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 0000000000..532edb2cf7 --- /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. + *

+ * 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. + *

+ * 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 5cf79ea8e2..e9498fd0f9 100644 --- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java +++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java @@ -5,6 +5,7 @@ import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITIO import android.content.Context; import android.graphics.PointF; +import android.graphics.Rect; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; @@ -71,6 +72,23 @@ public class GroupedTaskView extends TaskView { mDigitalWellBeingToast2 = new DigitalWellBeingToast(mActivity, this); } + @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(); diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java index d3c7778993..0e2f020cb1 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.Executors.UI_HELPER_EXECUTOR; 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.annotation.IdRes; 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.TaskOverlayFactory; 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 @@ public class TaskView extends FrameLayout implements Reusable { private boolean mIsClickableAsLiveTile = true; + @Nullable private final BorderAnimator mBorderAnimator; + public TaskView(Context context) { this(context, null); } @@ -414,12 +419,46 @@ public class TaskView extends FrameLayout implements Reusable { } 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 @@ public class TaskView extends FrameLayout implements Reusable { 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 af9ed95f59..e4815c6e28 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -408,6 +408,10 @@ public final class FeatureFlags { "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) {