Merge "Add the KeyboardQuickSwitchView (2/2)" into tm-qpr-dev am: 25656568e3

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/Launcher3/+/21087436

Change-Id: I2f769bd01dc85fbb36e0424e8aa6ecf2d18c8d91
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Schneider Victor-tulias
2023-02-15 05:24:01 +00:00
committed by Automerger Merge Worker
22 changed files with 1464 additions and 8 deletions

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:shape="rectangle">
<solid android:color="?androidprv:attr/colorSurfaceVariant" />
<corners android:radius="@dimen/keyboard_quick_switch_task_view_radius" />
</shape>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/transparent" />
<corners android:radius="@dimen/keyboard_quick_switch_task_view_radius" />
</shape>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="?attr/overviewScrimColor" />
<corners android:radius="@dimen/keyboard_quick_switch_view_radius" />
</shape>

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<com.android.launcher3.taskbar.KeyboardQuickSwitchTaskView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:launcher="http://schemas.android.com/apk/res-auto"
android:layout_width="@dimen/keyboard_quick_switch_taskview_width"
android:layout_height="@dimen/keyboard_quick_switch_taskview_height"
android:importantForAccessibility="yes"
android:background="@drawable/keyboard_quick_switch_task_view_background"
android:clipToOutline="true"
launcher:borderColor="?androidprv:attr/colorAccentSecondaryVariant">
<include
layout="@layout/keyboard_quick_switch_thumbnail"
android:id="@+id/thumbnail1"
android:layout_width="0dp"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/thumbnail2"/>
<include
layout="@layout/keyboard_quick_switch_thumbnail"
android:id="@+id/thumbnail2"
android:layout_width="0dp"
android:layout_height="match_parent"
android:visibility="gone"
android:layout_marginStart="@dimen/keyboard_quick_switch_split_view_spacing"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/thumbnail1"
app:layout_constraintEnd_toEndOf="parent"/>
</com.android.launcher3.taskbar.KeyboardQuickSwitchTaskView>

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<com.android.launcher3.taskbar.KeyboardQuickSwitchTaskView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:launcher="http://schemas.android.com/apk/res-auto"
android:layout_width="@dimen/keyboard_quick_switch_taskview_width"
android:layout_height="@dimen/keyboard_quick_switch_taskview_height"
android:background="@drawable/keyboard_quick_switch_overview_button_background"
android:clipToOutline="true"
android:importantForAccessibility="yes"
launcher:borderColor="?androidprv:attr/colorAccentSecondaryVariant">
<ImageView
android:id="@+id/icon"
android:layout_width="@dimen/keyboard_quick_switch_recents_icon_size"
android:layout_height="@dimen/keyboard_quick_switch_recents_icon_size"
android:layout_marginBottom="8dp"
android:src="@drawable/ic_empty_recents"
app:tint="?android:attr/textColorPrimary"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/text"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
style="@style/KeyboardQuickSwitchOverview"
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAlignment="center"
app:layout_constraintTop_toBottomOf="@id/icon"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</com.android.launcher3.taskbar.KeyboardQuickSwitchTaskView>

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<com.android.launcher3.taskbar.KeyboardQuickSwitchTaskView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:launcher="http://schemas.android.com/apk/res-auto"
android:layout_width="@dimen/keyboard_quick_switch_taskview_width"
android:layout_height="@dimen/keyboard_quick_switch_taskview_height"
android:importantForAccessibility="yes"
android:background="@drawable/keyboard_quick_switch_task_view_background"
android:clipToOutline="true"
launcher:borderColor="?androidprv:attr/colorAccentSecondaryVariant">
<include
layout="@layout/keyboard_quick_switch_thumbnail"
android:id="@+id/thumbnail1"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/thumbnail2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<include
layout="@layout/keyboard_quick_switch_thumbnail"
android:id="@+id/thumbnail2"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="gone"
android:layout_marginTop="@dimen/keyboard_quick_switch_split_view_spacing"
app:layout_constraintTop_toBottomOf="@id/thumbnail1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</com.android.launcher3.taskbar.KeyboardQuickSwitchTaskView>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<ImageView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:background="@drawable/keyboard_quick_switch_task_view_background"
android:clipToOutline="true"/>

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<com.android.launcher3.taskbar.KeyboardQuickSwitchView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="@dimen/keyboard_quick_switch_view_spacing"
android:layout_marginTop="@dimen/keyboard_quick_switch_margin_top"
android:layout_marginHorizontal="@dimen/keyboard_quick_switch_margin_ends"
android:background="@drawable/keyboard_quick_switch_view_background"
android:clipToOutline="true"
android:alpha="0"
android:visibility="invisible"
android:focusableInTouchMode="true">
<HorizontalScrollView
android:id="@+id/scroll_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:fillViewport="true"
android:scrollbars="none"
android:alpha="0"
android:visibility="invisible"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</HorizontalScrollView>
</com.android.launcher3.taskbar.KeyboardQuickSwitchView>

View File

@@ -82,4 +82,6 @@
<dimen name="taskbar_suw_frame">96dp</dimen>
<dimen name="taskbar_suw_insets">24dp</dimen>
</resources>
<dimen name="keyboard_quick_switch_taskview_width">205dp</dimen>
<dimen name="keyboard_quick_switch_taskview_height">119dp</dimen>
</resources>

View File

@@ -331,4 +331,13 @@
<!-- Keyboard Quick Switch -->
<dimen name="keyboard_quick_switch_border_width">4dp</dimen>
<dimen name="keyboard_quick_switch_taskview_width">104dp</dimen>
<dimen name="keyboard_quick_switch_taskview_height">134dp</dimen>
<dimen name="keyboard_quick_switch_recents_icon_size">20dp</dimen>
<dimen name="keyboard_quick_switch_margin_top">56dp</dimen>
<dimen name="keyboard_quick_switch_margin_ends">16dp</dimen>
<dimen name="keyboard_quick_switch_view_spacing">16dp</dimen>
<dimen name="keyboard_quick_switch_split_view_spacing">2dp</dimen>
<dimen name="keyboard_quick_switch_view_radius">28dp</dimen>
<dimen name="keyboard_quick_switch_task_view_radius">16dp</dimen>
</resources>

View File

@@ -282,4 +282,12 @@
<string name="move_drop_target_top_or_left">Move to top&#47;left</string>
<!-- Label for moving drop target to the bottom or right side of the screen, depending on orientation (from the Taskbar only). -->
<string name="move_drop_target_bottom_or_right">Move to bottom&#47;right</string>
<!-- Label for quick switch tile showing how many more apps are available [CHAR LIMIT=NONE] -->
<string name="quick_switch_overflow">{count, plural,
=1{Show # more app.}
other{Show # more apps.}
}</string>
<!-- Accessibility label for quick switch tiles showing split tasks [CHAR LIMIT=NONE] -->
<string name="quick_switch_split_task"><xliff:g id="app_name_1" example="Chrome">%1$s</xliff:g> and <xliff:g id="app_name_2" example="Gmail">%2$s</xliff:g></string>
</resources>

View File

@@ -223,4 +223,11 @@
<item name="android:fontFamily">google-sans-text</item>
<item name="android:textSize">14sp</item>
</style>
</resources>
<style name="KeyboardQuickSwitchOverview">
<item name="fontFamily">google-sans-text</item>
<item name="android:textSize">14sp</item>
<item name="android:textColor">?android:attr/textColorPrimary</item>
<item name="lineHeight">20sp</item>
</style>
</resources>

View File

@@ -0,0 +1,192 @@
/*
* 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.launcher3.taskbar;
import android.content.ComponentName;
import android.content.pm.ActivityInfo;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.R;
import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
import com.android.quickstep.RecentsModel;
import com.android.quickstep.util.GroupTask;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.ThumbnailData;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
/**
* Handles initialization of the {@link KeyboardQuickSwitchViewController}.
*/
public final class KeyboardQuickSwitchController implements
TaskbarControllers.LoggableTaskbarController {
static final int MAX_TASKS = 6;
@NonNull private final ControllerCallbacks mControllerCallbacks = new ControllerCallbacks();
// Initialized on init
@Nullable private RecentsModel mModel;
// Used to keep track of the last requested task list id, so that we do not request to load the
// tasks again if we have already requested it and the task list has not changed
private int mTaskListChangeId = -1;
// Only empty before the recent tasks list has been loaded the first time
@NonNull private List<GroupTask> mTasks = new ArrayList<>();
private int mNumHiddenTasks = 0;
// Initialized in init
private TaskbarControllers mControllers;
@Nullable private KeyboardQuickSwitchViewController mQuickSwitchViewController;
/** Initialize the controller. */
public void init(@NonNull TaskbarControllers controllers) {
mControllers = controllers;
mModel = RecentsModel.INSTANCE.get(controllers.taskbarActivityContext);
}
void onConfigurationChanged(@ActivityInfo.Config int configChanges) {
if (mQuickSwitchViewController == null) {
return;
}
if ((configChanges & (ActivityInfo.CONFIG_KEYBOARD
| ActivityInfo.CONFIG_KEYBOARD_HIDDEN)) != 0) {
mQuickSwitchViewController.closeQuickSwitchView(true);
return;
}
int currentFocusedIndex = mQuickSwitchViewController.getCurrentFocusedIndex();
onDestroy();
if (currentFocusedIndex != -1) {
mControllers.taskbarActivityContext.getMainThreadHandler().post(
() -> openQuickSwitchView(currentFocusedIndex));
}
}
void openQuickSwitchView() {
openQuickSwitchView(-1);
}
private void openQuickSwitchView(int currentFocusedIndex) {
if (mQuickSwitchViewController != null) {
return;
}
TaskbarOverlayContext overlayContext =
mControllers.taskbarOverlayController.requestWindow();
KeyboardQuickSwitchView keyboardQuickSwitchView =
(KeyboardQuickSwitchView) overlayContext.getLayoutInflater()
.inflate(
R.layout.keyboard_quick_switch_view,
overlayContext.getDragLayer(),
/* attachToRoot= */ false);
mQuickSwitchViewController = new KeyboardQuickSwitchViewController(
mControllers, overlayContext, keyboardQuickSwitchView, mControllerCallbacks);
if (mModel.isTaskListValid(mTaskListChangeId)) {
mQuickSwitchViewController.openQuickSwitchView(
mTasks, mNumHiddenTasks, /* updateTasks= */ false, currentFocusedIndex);
return;
}
mTaskListChangeId = mModel.getTasks((tasks) -> {
// Only store MAX_TASK tasks, from most to least recent
Collections.reverse(tasks);
mTasks = tasks.stream().limit(MAX_TASKS).collect(Collectors.toList());
mNumHiddenTasks = Math.max(0, tasks.size() - MAX_TASKS);
mQuickSwitchViewController.openQuickSwitchView(
mTasks, mNumHiddenTasks, /* updateTasks= */ true, currentFocusedIndex);
});
}
void closeQuickSwitchView() {
if (mQuickSwitchViewController == null) {
return;
}
mQuickSwitchViewController.closeQuickSwitchView(true);
}
/**
* See {@link TaskbarUIController#launchFocusedTask()}
*/
int launchFocusedTask() {
// Return -1 so that the RecentsView is not incorrectly opened when the user closes the
// quick switch view by tapping the screen.
return mQuickSwitchViewController == null
? -1 : mQuickSwitchViewController.launchFocusedTask();
}
void onDestroy() {
if (mQuickSwitchViewController != null) {
mQuickSwitchViewController.onDestroy();
}
}
@Override
public void dumpLogs(String prefix, PrintWriter pw) {
pw.println(prefix + "KeyboardQuickSwitchController:");
pw.println(prefix + "\tisOpen=" + (mQuickSwitchViewController != null));
pw.println(prefix + "\tmNumHiddenTasks=" + mNumHiddenTasks);
pw.println(prefix + "\tmTaskListChangeId=" + mTaskListChangeId);
pw.println(prefix + "\tmTasks=[");
for (GroupTask task : mTasks) {
Task task1 = task.task1;
Task task2 = task.task2;
ComponentName cn1 = task1.getTopComponent();
ComponentName cn2 = task2 != null ? task2.getTopComponent() : null;
pw.println(prefix + "\t\tt1: (id=" + task1.key.id
+ "; package=" + (cn1 != null ? cn1.getPackageName() + ")" : "no package)")
+ " t2: (id=" + (task2 != null ? task2.key.id : "-1")
+ "; package=" + (cn2 != null ? cn2.getPackageName() + ")"
: "no package)"));
}
pw.println(prefix + "\t]");
if (mQuickSwitchViewController != null) {
mQuickSwitchViewController.dumpLogs(prefix + '\t', pw);
}
}
class ControllerCallbacks {
int getTaskCount() {
return mNumHiddenTasks == 0 ? mTasks.size() : MAX_TASKS + 1;
}
@Nullable
GroupTask getTaskAt(int index) {
return index < 0 || index >= mTasks.size() ? null : mTasks.get(index);
}
void updateThumbnailInBackground(Task task, Consumer<ThumbnailData> callback) {
mModel.getThumbnailCache().updateThumbnailInBackground(task, callback);
}
void updateTitleInBackground(Task task, Consumer<Task> callback) {
mModel.getIconCache().updateIconInBackground(task, callback);
}
void onCloseComplete() {
mQuickSwitchViewController = null;
}
}
}

View File

@@ -0,0 +1,173 @@
/*
* 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.launcher3.taskbar;
import static com.android.quickstep.util.BorderAnimator.DEFAULT_BORDER_COLOR;
import android.animation.Animator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.android.launcher3.R;
import com.android.quickstep.util.BorderAnimator;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.ThumbnailData;
import java.util.function.Consumer;
/**
* A view that displays a recent task during a keyboard quick switch.
*/
public class KeyboardQuickSwitchTaskView extends ConstraintLayout {
@NonNull private final BorderAnimator mBorderAnimator;
@Nullable private ImageView mThumbnailView1;
@Nullable private ImageView mThumbnailView2;
public KeyboardQuickSwitchTaskView(@NonNull Context context) {
this(context, null);
}
public KeyboardQuickSwitchTaskView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public KeyboardQuickSwitchTaskView(
@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public KeyboardQuickSwitchTaskView(
@NonNull Context context,
@Nullable AttributeSet attrs,
int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
setWillNotDraw(false);
Resources resources = context.getResources();
mBorderAnimator = new BorderAnimator(
/* borderBoundsBuilder= */ bounds -> bounds.set(0, 0, getWidth(), getHeight()),
/* borderWidthPx= */ resources.getDimensionPixelSize(
R.dimen.keyboard_quick_switch_border_width),
/* borderRadiusPx= */ resources.getDimensionPixelSize(
R.dimen.keyboard_quick_switch_task_view_radius),
/* borderColor= */ attrs == null
? DEFAULT_BORDER_COLOR
: context.getTheme()
.obtainStyledAttributes(
attrs,
R.styleable.TaskView,
defStyleAttr,
defStyleRes)
.getColor(
R.styleable.TaskView_borderColor,
DEFAULT_BORDER_COLOR),
/* invalidateViewCallback= */ KeyboardQuickSwitchTaskView.this::invalidate);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mThumbnailView1 = findViewById(R.id.thumbnail1);
mThumbnailView2 = findViewById(R.id.thumbnail2);
}
@NonNull
protected Animator getFocusAnimator(boolean focused) {
return mBorderAnimator.buildAnimator(focused);
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
mBorderAnimator.drawBorder(canvas);
}
protected void setThumbnails(
@NonNull Task task1,
@Nullable Task task2,
@Nullable ThumbnailUpdateFunction thumbnailUpdateFunction,
@Nullable TitleUpdateFunction titleUpdateFunction) {
applyThumbnail(mThumbnailView1, task1, thumbnailUpdateFunction);
applyThumbnail(mThumbnailView2, task2, thumbnailUpdateFunction);
if (titleUpdateFunction == null) {
setContentDescription(task2 == null
? task1.titleDescription
: getContext().getString(
R.string.quick_switch_split_task,
task1.titleDescription,
task2.titleDescription));
return;
}
titleUpdateFunction.updateTitleInBackground(task1, t ->
setContentDescription(task1.titleDescription));
if (task2 == null) {
return;
}
titleUpdateFunction.updateTitleInBackground(task2, t ->
setContentDescription(getContext().getString(
R.string.quick_switch_split_task,
task1.titleDescription,
task2.titleDescription)));
}
private void applyThumbnail(
@Nullable ImageView thumbnailView,
@Nullable Task task,
@Nullable ThumbnailUpdateFunction updateFunction) {
if (thumbnailView == null) {
return;
}
if (task == null) {
return;
}
if (updateFunction == null) {
applyThumbnail(thumbnailView, task.thumbnail);
return;
}
updateFunction.updateThumbnailInBackground(
task, thumbnailData -> applyThumbnail(thumbnailView, thumbnailData));
}
private void applyThumbnail(
@NonNull ImageView thumbnailView, ThumbnailData thumbnailData) {
Bitmap bm = thumbnailData == null ? null : thumbnailData.thumbnail;
thumbnailView.setVisibility(VISIBLE);
thumbnailView.setImageBitmap(bm);
}
protected interface ThumbnailUpdateFunction {
void updateThumbnailInBackground(Task task, Consumer<ThumbnailData> callback);
}
protected interface TitleUpdateFunction {
void updateTitleInBackground(Task task, Consumer<Task> callback);
}
}

View File

@@ -0,0 +1,497 @@
/*
* 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.launcher3.taskbar;
import static androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID;
import static com.android.launcher3.taskbar.KeyboardQuickSwitchController.MAX_TASKS;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Outline;
import android.graphics.Rect;
import android.icu.text.MessageFormat;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver;
import android.view.animation.Interpolator;
import android.widget.HorizontalScrollView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.anim.Interpolators;
import com.android.quickstep.util.GroupTask;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
/**
* View that allows quick switching between recent tasks through keyboard alt-tab and alt-shift-tab
* commands.
*/
public class KeyboardQuickSwitchView extends ConstraintLayout {
private static final long OUTLINE_ANIMATION_DURATION_MS = 333;
private static final float OUTLINE_START_HEIGHT_FACTOR = 0.45f;
private static final float OUTLINE_START_RADIUS_FACTOR = 0.25f;
private static final Interpolator OPEN_OUTLINE_INTERPOLATOR =
Interpolators.EMPHASIZED_DECELERATE;
private static final Interpolator CLOSE_OUTLINE_INTERPOLATOR =
Interpolators.EMPHASIZED_ACCELERATE;
private static final long ALPHA_ANIMATION_DURATION_MS = 83;
private static final long ALPHA_ANIMATION_START_DELAY_MS = 67;
private static final long CONTENT_TRANSLATION_X_ANIMATION_DURATION_MS = 500;
private static final long CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS = 333;
private static final float CONTENT_START_TRANSLATION_X_DP = 32;
private static final float CONTENT_START_TRANSLATION_Y_DP = 40;
private static final Interpolator OPEN_TRANSLATION_X_INTERPOLATOR = Interpolators.EMPHASIZED;
private static final Interpolator OPEN_TRANSLATION_Y_INTERPOLATOR =
Interpolators.EMPHASIZED_DECELERATE;
private static final Interpolator CLOSE_TRANSLATION_Y_INTERPOLATOR =
Interpolators.EMPHASIZED_ACCELERATE;
private static final long CONTENT_ALPHA_ANIMATION_DURATION_MS = 83;
private static final long CONTENT_ALPHA_ANIMATION_START_DELAY_MS = 83;
private final AnimatedFloat mOutlineAnimationProgress = new AnimatedFloat(
this::invalidateOutline);
private HorizontalScrollView mScrollView;
private ConstraintLayout mContent;
private int mTaskViewHeight;
private int mSpacing;
private int mOutlineRadius;
private boolean mIsRtl;
@Nullable private AnimatorSet mOpenAnimation;
@Nullable private KeyboardQuickSwitchViewController.ViewCallbacks mViewCallbacks;
public KeyboardQuickSwitchView(@NonNull Context context) {
this(context, null);
}
public KeyboardQuickSwitchView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public KeyboardQuickSwitchView(@NonNull Context context, @Nullable AttributeSet attrs,
int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public KeyboardQuickSwitchView(@NonNull Context context, @Nullable AttributeSet attrs,
int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mScrollView = findViewById(R.id.scroll_view);
mContent = findViewById(R.id.content);
Resources resources = getResources();
mTaskViewHeight = resources.getDimensionPixelSize(
R.dimen.keyboard_quick_switch_taskview_height);
mSpacing = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_view_spacing);
mOutlineRadius = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_view_radius);
mIsRtl = Utilities.isRtl(resources);
}
@NonNull
private KeyboardQuickSwitchTaskView createAndAddTaskView(
int index,
int width,
boolean isFinalView,
boolean updateTasks,
@NonNull LayoutInflater layoutInflater,
@Nullable View previousView,
@NonNull List<GroupTask> groupTasks) {
KeyboardQuickSwitchTaskView taskView = (KeyboardQuickSwitchTaskView) layoutInflater.inflate(
R.layout.keyboard_quick_switch_taskview, mContent, false);
taskView.setId(View.generateViewId());
taskView.setOnClickListener(v -> mViewCallbacks.launchTappedTask(index));
LayoutParams lp = new LayoutParams(width, mTaskViewHeight);
// Create a right-to-left ordering of views (or left-to-right in RTL locales)
if (previousView != null) {
lp.endToStart = previousView.getId();
} else {
lp.endToEnd = PARENT_ID;
}
lp.topToTop = PARENT_ID;
lp.bottomToBottom = PARENT_ID;
// Add spacing between views
lp.setMarginEnd(mSpacing);
if (isFinalView) {
// Add spacing to the start of the final view so that scrolling ends with some padding.
lp.startToStart = PARENT_ID;
lp.setMarginStart(mSpacing);
lp.horizontalBias = 1f;
}
GroupTask groupTask = groupTasks.get(index);
taskView.setThumbnails(
groupTask.task1,
groupTask.task2,
updateTasks ? mViewCallbacks::updateThumbnailInBackground : null,
updateTasks ? mViewCallbacks::updateTitleInBackground : null);
mContent.addView(taskView, lp);
return taskView;
}
private void createAndAddOverviewButton(
int width,
@NonNull LayoutInflater layoutInflater,
@Nullable View previousView,
@NonNull String overflowString) {
KeyboardQuickSwitchTaskView overviewButton =
(KeyboardQuickSwitchTaskView) layoutInflater.inflate(
R.layout.keyboard_quick_switch_overview, this, false);
overviewButton.setOnClickListener(v -> mViewCallbacks.launchTappedTask(MAX_TASKS));
overviewButton.<TextView>findViewById(R.id.text).setText(overflowString);
ConstraintLayout.LayoutParams lp = new ConstraintLayout.LayoutParams(
width, mTaskViewHeight);
lp.startToStart = PARENT_ID;
lp.endToStart = previousView.getId();
lp.topToTop = PARENT_ID;
lp.bottomToBottom = PARENT_ID;
lp.setMarginEnd(mSpacing);
lp.setMarginStart(mSpacing);
mContent.addView(overviewButton, lp);
}
protected void applyLoadPlan(
@NonNull Context context,
@NonNull List<GroupTask> groupTasks,
int numHiddenTasks,
boolean updateTasks,
int currentFocusIndexOverride,
@NonNull KeyboardQuickSwitchViewController.ViewCallbacks viewCallbacks) {
if (groupTasks.isEmpty()) {
// Do not show the quick switch view.
return;
}
mViewCallbacks = viewCallbacks;
Resources resources = context.getResources();
int width = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_taskview_width);
View previousView = null;
LayoutInflater layoutInflater = LayoutInflater.from(context);
int tasksToDisplay = Math.min(MAX_TASKS, groupTasks.size());
for (int i = 0; i < tasksToDisplay; i++) {
previousView = createAndAddTaskView(
i,
width,
/* isFinalView= */ i == tasksToDisplay - 1 && numHiddenTasks == 0,
updateTasks,
layoutInflater,
previousView,
groupTasks);
}
if (numHiddenTasks > 0) {
HashMap<String, Integer> args = new HashMap<>();
args.put("count", numHiddenTasks);
createAndAddOverviewButton(
width,
layoutInflater,
previousView,
new MessageFormat(
resources.getString(R.string.quick_switch_overflow),
Locale.getDefault()).format(args));
}
getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
animateOpen(currentFocusIndexOverride);
getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
}
protected Animator getCloseAnimation() {
AnimatorSet closeAnimation = new AnimatorSet();
Animator outlineAnimation = mOutlineAnimationProgress.animateToValue(0f);
outlineAnimation.setDuration(OUTLINE_ANIMATION_DURATION_MS);
outlineAnimation.setInterpolator(CLOSE_OUTLINE_INTERPOLATOR);
closeAnimation.play(outlineAnimation);
Animator alphaAnimation = ObjectAnimator.ofFloat(this, ALPHA, 1f, 0f);
alphaAnimation.setStartDelay(ALPHA_ANIMATION_START_DELAY_MS);
alphaAnimation.setDuration(ALPHA_ANIMATION_DURATION_MS);
closeAnimation.play(alphaAnimation);
Animator translationYAnimation = ObjectAnimator.ofFloat(
mScrollView, TRANSLATION_Y, 0, -Utilities.dpToPx(CONTENT_START_TRANSLATION_Y_DP));
translationYAnimation.setDuration(CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS);
translationYAnimation.setInterpolator(CLOSE_TRANSLATION_Y_INTERPOLATOR);
closeAnimation.play(translationYAnimation);
Animator contentAlphaAnimation = ObjectAnimator.ofFloat(mScrollView, ALPHA, 1f, 0f);
contentAlphaAnimation.setDuration(CONTENT_ALPHA_ANIMATION_DURATION_MS);
closeAnimation.play(contentAlphaAnimation);
closeAnimation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
if (mOpenAnimation != null) {
mOpenAnimation.cancel();
}
}
});
return closeAnimation;
}
private void animateOpen(int currentFocusIndexOverride) {
if (mOpenAnimation != null) {
// Restart animation since currentFocusIndexOverride can change the initial scroll.
mOpenAnimation.cancel();
}
mOpenAnimation = new AnimatorSet();
Animator outlineAnimation = mOutlineAnimationProgress.animateToValue(1f);
outlineAnimation.setDuration(OUTLINE_ANIMATION_DURATION_MS);
mOpenAnimation.play(outlineAnimation);
Animator alphaAnimation = ObjectAnimator.ofFloat(this, ALPHA, 0f, 1f);
alphaAnimation.setDuration(ALPHA_ANIMATION_DURATION_MS);
mOpenAnimation.play(alphaAnimation);
Animator translationXAnimation = ObjectAnimator.ofFloat(
mScrollView, TRANSLATION_X, -Utilities.dpToPx(CONTENT_START_TRANSLATION_X_DP), 0);
translationXAnimation.setDuration(CONTENT_TRANSLATION_X_ANIMATION_DURATION_MS);
translationXAnimation.setInterpolator(OPEN_TRANSLATION_X_INTERPOLATOR);
mOpenAnimation.play(translationXAnimation);
Animator translationYAnimation = ObjectAnimator.ofFloat(
mScrollView, TRANSLATION_Y, -Utilities.dpToPx(CONTENT_START_TRANSLATION_Y_DP), 0);
translationYAnimation.setDuration(CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS);
translationYAnimation.setInterpolator(OPEN_TRANSLATION_Y_INTERPOLATOR);
mOpenAnimation.play(translationYAnimation);
Animator contentAlphaAnimation = ObjectAnimator.ofFloat(mScrollView, ALPHA, 0f, 1f);
contentAlphaAnimation.setStartDelay(CONTENT_ALPHA_ANIMATION_START_DELAY_MS);
contentAlphaAnimation.setDuration(CONTENT_ALPHA_ANIMATION_DURATION_MS);
mOpenAnimation.play(contentAlphaAnimation);
ViewOutlineProvider outlineProvider = getOutlineProvider();
mOpenAnimation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
setClipToPadding(false);
setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setRoundRect(
/* rect= */ new Rect(
/* left= */ 0,
/* top= */ 0,
/* right= */ getWidth(),
/* bottom= */
(int) (getHeight() * Utilities.mapBoundToRange(
mOutlineAnimationProgress.value,
/* lowerBound= */ 0f,
/* upperBound= */ 1f,
/* toMin= */ OUTLINE_START_HEIGHT_FACTOR,
/* toMax= */ 1f,
OPEN_OUTLINE_INTERPOLATOR))),
/* radius= */ mOutlineRadius * Utilities.mapBoundToRange(
mOutlineAnimationProgress.value,
/* lowerBound= */ 0f,
/* upperBound= */ 1f,
/* toMin= */ OUTLINE_START_RADIUS_FACTOR,
/* toMax= */ 1f,
OPEN_OUTLINE_INTERPOLATOR));
}
});
if (currentFocusIndexOverride == -1) {
initializeScroll(/* index= */ 0, /* shouldTruncateTarget= */ false);
} else {
animateFocusMove(-1, currentFocusIndexOverride);
}
mScrollView.setVisibility(VISIBLE);
setVisibility(VISIBLE);
requestFocus();
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
setClipToPadding(true);
setOutlineProvider(outlineProvider);
invalidateOutline();
mOpenAnimation = null;
}
});
mOpenAnimation.start();
}
protected void animateFocusMove(int fromIndex, int toIndex) {
KeyboardQuickSwitchTaskView focusedTask = getTaskAt(toIndex);
if (focusedTask == null) {
return;
}
AnimatorSet focusAnimation = new AnimatorSet();
focusAnimation.play(focusedTask.getFocusAnimator(true));
KeyboardQuickSwitchTaskView previouslyFocusedTask = getTaskAt(fromIndex);
if (previouslyFocusedTask != null) {
focusAnimation.play(previouslyFocusedTask.getFocusAnimator(false));
}
focusAnimation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
focusedTask.requestAccessibilityFocus();
if (fromIndex == -1) {
int firstVisibleTaskIndex = toIndex == 0
? toIndex
: getTaskAt(toIndex - 1) == null
? toIndex : toIndex - 1;
// Scroll so that the previous task view is truncated as a visual hint that
// there are more tasks
initializeScroll(
firstVisibleTaskIndex,
/* shouldTruncateTarget= */ firstVisibleTaskIndex != toIndex);
} else if (toIndex > fromIndex || toIndex == 0) {
// Scrolling to next task view
if (mIsRtl) {
scrollRightTo(focusedTask);
} else {
scrollLeftTo(focusedTask);
}
} else {
// Scrolling to previous task view
if (mIsRtl) {
scrollLeftTo(focusedTask);
} else {
scrollRightTo(focusedTask);
}
}
if (mViewCallbacks != null) {
mViewCallbacks.updateCurrentFocusIndex(toIndex);
}
}
});
focusAnimation.start();
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
return (mViewCallbacks != null && mViewCallbacks.onKeyUp(keyCode, event))
|| super.onKeyUp(keyCode, event);
}
private void initializeScroll(int index, boolean shouldTruncateTarget) {
View task = getTaskAt(index);
if (task == null) {
return;
}
if (mIsRtl) {
scrollRightTo(
task, shouldTruncateTarget, /* smoothScroll= */ false);
} else {
scrollLeftTo(
task, shouldTruncateTarget, /* smoothScroll= */ false);
}
}
private void scrollRightTo(@NonNull View targetTask) {
scrollRightTo(targetTask, /* shouldTruncateTarget= */ false, /* smoothScroll= */ true);
}
private void scrollRightTo(
@NonNull View targetTask, boolean shouldTruncateTarget, boolean smoothScroll) {
if (smoothScroll && !shouldScroll(targetTask, shouldTruncateTarget)) {
return;
}
int scrollTo = targetTask.getLeft() - mSpacing
+ (shouldTruncateTarget ? targetTask.getWidth() / 2 : 0);
// Scroll so that the focused task is to the left of the list
if (smoothScroll) {
mScrollView.smoothScrollTo(scrollTo, 0);
} else {
mScrollView.scrollTo(scrollTo, 0);
}
}
private void scrollLeftTo(@NonNull View targetTask) {
scrollLeftTo(targetTask, /* shouldTruncateTarget= */ false, /* smoothScroll= */ true);
}
private void scrollLeftTo(
@NonNull View targetTask, boolean shouldTruncateTarget, boolean smoothScroll) {
if (smoothScroll && !shouldScroll(targetTask, shouldTruncateTarget)) {
return;
}
int scrollTo = targetTask.getRight() + mSpacing - mScrollView.getWidth()
- (shouldTruncateTarget ? targetTask.getWidth() / 2 : 0);
// Scroll so that the focused task is to the right of the list
if (smoothScroll) {
mScrollView.smoothScrollTo(scrollTo, 0);
} else {
mScrollView.scrollTo(scrollTo, 0);
}
}
private boolean shouldScroll(@NonNull View targetTask, boolean shouldTruncateTarget) {
boolean isTargetTruncated =
targetTask.getRight() + mSpacing > mScrollView.getScrollX() + mScrollView.getWidth()
|| Math.max(0, targetTask.getLeft() - mSpacing) < mScrollView.getScrollX();
return isTargetTruncated && !shouldTruncateTarget;
}
@Nullable
protected KeyboardQuickSwitchTaskView getTaskAt(int index) {
return index < 0 || index >= mContent.getChildCount()
? null : (KeyboardQuickSwitchTaskView) mContent.getChildAt(index);
}
}

View File

@@ -0,0 +1,200 @@
/*
* 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.launcher3.taskbar;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import android.animation.Animator;
import android.view.KeyEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.anim.AnimationSuccessListener;
import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
import com.android.launcher3.taskbar.overlay.TaskbarOverlayDragLayer;
import com.android.quickstep.util.GroupTask;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import java.io.PrintWriter;
import java.util.List;
import java.util.function.Consumer;
/**
* Handles initialization of the {@link KeyboardQuickSwitchView} and supplies it with the list of
* tasks.
*/
public class KeyboardQuickSwitchViewController {
@NonNull private final ViewCallbacks mViewCallbacks = new ViewCallbacks();
@NonNull private final TaskbarControllers mControllers;
@NonNull private final TaskbarOverlayContext mOverlayContext;
@NonNull private final KeyboardQuickSwitchView mKeyboardQuickSwitchView;
@NonNull private final KeyboardQuickSwitchController.ControllerCallbacks mControllerCallbacks;
@Nullable private Animator mCloseAnimation;
private int mCurrentFocusIndex = -1;
protected KeyboardQuickSwitchViewController(
@NonNull TaskbarControllers controllers,
@NonNull TaskbarOverlayContext overlayContext,
@NonNull KeyboardQuickSwitchView keyboardQuickSwitchView,
@NonNull KeyboardQuickSwitchController.ControllerCallbacks controllerCallbacks) {
mControllers = controllers;
mOverlayContext = overlayContext;
mKeyboardQuickSwitchView = keyboardQuickSwitchView;
mControllerCallbacks = controllerCallbacks;
}
protected int getCurrentFocusedIndex() {
return mCurrentFocusIndex;
}
protected void openQuickSwitchView(
@NonNull List<GroupTask> tasks,
int numHiddenTasks,
boolean updateTasks,
int currentFocusIndexOverride) {
TaskbarOverlayDragLayer dragLayer = mOverlayContext.getDragLayer();
dragLayer.addView(mKeyboardQuickSwitchView);
dragLayer.runOnClickOnce(v -> closeQuickSwitchView(true));
mKeyboardQuickSwitchView.applyLoadPlan(
mOverlayContext,
tasks,
numHiddenTasks,
updateTasks,
currentFocusIndexOverride,
mViewCallbacks);
}
protected void closeQuickSwitchView(boolean animate) {
if (mCloseAnimation != null) {
if (animate) {
// Let currently-running animation finish.
return;
} else {
mCloseAnimation.cancel();
}
}
if (!animate) {
mCloseAnimation = null;
onCloseComplete();
return;
}
mCloseAnimation = mKeyboardQuickSwitchView.getCloseAnimation();
mCloseAnimation.addListener(new AnimationSuccessListener() {
@Override
public void onAnimationSuccess(Animator animator) {
mCloseAnimation = null;
onCloseComplete();
}
});
mCloseAnimation.start();
}
/**
* Launched the currently-focused task.
*
* Returns index -1 iff the RecentsView shouldn't be opened.
*
* If the index is not -1, then the {@link com.android.quickstep.views.TaskView} at the returned
* index will be focused.
*/
protected int launchFocusedTask() {
// Launch the second-most recent task if the user quick switches too quickly, if possible.
return launchTaskAt(mCurrentFocusIndex == -1
? (mControllerCallbacks.getTaskCount() > 1 ? 1 : 0) : mCurrentFocusIndex);
}
private int launchTaskAt(int index) {
KeyboardQuickSwitchTaskView taskView = mKeyboardQuickSwitchView.getTaskAt(index);
GroupTask task = mControllerCallbacks.getTaskAt(index);
if (taskView == null || task == null) {
return Math.max(0, index);
} else if (task.task2 == null) {
UI_HELPER_EXECUTOR.execute(() ->
ActivityManagerWrapper.getInstance().startActivityFromRecents(
task.task1.key,
mControllers.taskbarActivityContext.getActivityLaunchOptions(
taskView, null).options));
} else {
mControllers.uiController.launchSplitTasks(taskView, task);
}
return -1;
}
private void onCloseComplete() {
mOverlayContext.getDragLayer().removeView(mKeyboardQuickSwitchView);
mControllerCallbacks.onCloseComplete();
}
protected void onDestroy() {
closeQuickSwitchView(false);
}
public void dumpLogs(String prefix, PrintWriter pw) {
pw.println(prefix + "KeyboardQuickSwitchViewController:");
pw.println(prefix + "\thasFocus=" + mKeyboardQuickSwitchView.hasFocus());
pw.println(prefix + "\tcloseAnimationRunning=" + (mCloseAnimation != null));
pw.println(prefix + "\tmCurrentFocusIndex=" + mCurrentFocusIndex);
}
class ViewCallbacks {
boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode != KeyEvent.KEYCODE_TAB) {
return false;
}
int taskCount = mControllerCallbacks.getTaskCount();
int toIndex = mCurrentFocusIndex == -1
// Focus the second-most recent app if possible
? (taskCount > 1 ? 1 : 0)
: (event.isShiftPressed()
// focus a more recent task or loop back to the opposite end
? Math.max(0, mCurrentFocusIndex == 0
? taskCount - 1 : mCurrentFocusIndex - 1)
// focus a less recent app or loop back to the opposite end
: ((mCurrentFocusIndex + 1) % taskCount));
mKeyboardQuickSwitchView.animateFocusMove(mCurrentFocusIndex, toIndex);
return true;
}
void updateCurrentFocusIndex(int index) {
mCurrentFocusIndex = index;
}
void launchTappedTask(int index) {
KeyboardQuickSwitchViewController.this.launchTaskAt(index);
closeQuickSwitchView(true);
}
void updateThumbnailInBackground(Task task, Consumer<ThumbnailData> callback) {
mControllerCallbacks.updateThumbnailInBackground(task, callback);
}
void updateTitleInBackground(Task task, Consumer<Task> callback) {
mControllerCallbacks.updateTitleInBackground(task, callback);
}
}
}

View File

@@ -15,7 +15,6 @@
*/
package com.android.launcher3.taskbar;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.content.pm.PackageManager.FEATURE_PC;
import static android.os.Trace.TRACE_TAG_APP;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
@@ -236,7 +235,8 @@ public class TaskbarActivityContext extends BaseTaskbarContext {
isDesktopMode
? new DesktopTaskbarRecentAppsController(this)
: TaskbarRecentAppsController.DEFAULT,
new TaskbarEduTooltipController(this));
new TaskbarEduTooltipController(this),
new KeyboardQuickSwitchController());
}
public void init(@NonNull TaskbarSharedState sharedState) {

View File

@@ -59,6 +59,7 @@ public class TaskbarControllers {
public final TaskbarTranslationController taskbarTranslationController;
public final TaskbarOverlayController taskbarOverlayController;
public final TaskbarEduTooltipController taskbarEduTooltipController;
public final KeyboardQuickSwitchController keyboardQuickSwitchController;
@Nullable private LoggableTaskbarController[] mControllersToLog = null;
@Nullable private BackgroundRendererController[] mBackgroundRendererControllers = null;
@@ -103,7 +104,8 @@ public class TaskbarControllers {
VoiceInteractionWindowController voiceInteractionWindowController,
TaskbarTranslationController taskbarTranslationController,
TaskbarRecentAppsController taskbarRecentAppsController,
TaskbarEduTooltipController taskbarEduTooltipController) {
TaskbarEduTooltipController taskbarEduTooltipController,
KeyboardQuickSwitchController keyboardQuickSwitchController) {
this.taskbarActivityContext = taskbarActivityContext;
this.taskbarDragController = taskbarDragController;
this.navButtonController = navButtonController;
@@ -127,6 +129,7 @@ public class TaskbarControllers {
this.taskbarTranslationController = taskbarTranslationController;
this.taskbarRecentAppsController = taskbarRecentAppsController;
this.taskbarEduTooltipController = taskbarEduTooltipController;
this.keyboardQuickSwitchController = keyboardQuickSwitchController;
}
/**
@@ -159,6 +162,7 @@ public class TaskbarControllers {
taskbarRecentAppsController.init(this);
taskbarTranslationController.init(this);
taskbarEduTooltipController.init(this);
keyboardQuickSwitchController.init(this);
mControllersToLog = new LoggableTaskbarController[] {
taskbarDragController, navButtonController, navbarButtonsViewController,
@@ -167,7 +171,7 @@ public class TaskbarControllers {
stashedHandleViewController, taskbarStashController, taskbarEduController,
taskbarAutohideSuspendController, taskbarPopupController, taskbarInsetsController,
voiceInteractionWindowController, taskbarTranslationController,
taskbarEduTooltipController
taskbarEduTooltipController, keyboardQuickSwitchController
};
mBackgroundRendererControllers = new BackgroundRendererController[] {
taskbarDragLayerController, taskbarScrimViewController,
@@ -191,6 +195,7 @@ public class TaskbarControllers {
public void onConfigurationChanged(@Config int configChanges) {
navbarButtonsViewController.onConfigurationChanged(configChanges);
taskbarDragLayerController.onConfigurationChanged();
keyboardQuickSwitchController.onConfigurationChanged(configChanges);
}
/**
@@ -216,6 +221,7 @@ public class TaskbarControllers {
taskbarInsetsController.onDestroy();
voiceInteractionWindowController.onDestroy();
taskbarRecentAppsController.onDestroy();
keyboardQuickSwitchController.onDestroy();
mControllersToLog = null;
mBackgroundRendererControllers = null;

View File

@@ -230,6 +230,38 @@ public class TaskbarUIController {
);
}
/**
* Opens the Keyboard Quick Switch View.
*
* This will set the focus to the first task from the right (from the left in RTL)
*/
public void openQuickSwitchView() {
mControllers.keyboardQuickSwitchController.openQuickSwitchView();
}
/**
* Closes the Keyboard Quick Switch View.
*
* No-op if the view is already closed
*/
public void closeQuickSwitchView() {
mControllers.keyboardQuickSwitchController.closeQuickSwitchView();
}
/**
* Launches the focused task and closes the Keyboard Quick Switch View.
*
* If the overlay or view are closed, or the overview task is focused, then Overview is
* launched. If the overview task is launched, then the first hidden task is focused.
*
* @return the index of what task should be focused in ; -1 iff Overview shouldn't be launched
*/
public int launchFocusedTask() {
int focusedTaskIndex = mControllers.keyboardQuickSwitchController.launchFocusedTask();
mControllers.keyboardQuickSwitchController.closeQuickSwitchView();
return focusedTaskIndex;
}
/**
* Launches the focused task in splitscreen.
*

View File

@@ -31,7 +31,10 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.statemanager.StatefulActivity;
import com.android.launcher3.taskbar.TaskbarUIController;
import com.android.launcher3.util.RunnableList;
import com.android.quickstep.RecentsAnimationCallbacks.RecentsAnimationListener;
import com.android.quickstep.views.RecentsView;
@@ -174,8 +177,25 @@ public class OverviewCommandHelper {
mOverviewComponentObserver.getActivityInterface();
RecentsView recents = activityInterface.getVisibleRecentsView();
if (recents == null) {
T activity = activityInterface.getCreatedActivity();
DeviceProfile dp = activity == null ? null : activity.getDeviceProfile();
TaskbarUIController uiController = activityInterface.getTaskbarController();
boolean allowQuickSwitch = FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH.get()
&& uiController != null
&& dp != null
&& (dp.isTablet || dp.isTwoPanels);
if (cmd.type == TYPE_HIDE) {
// already hidden
if (!allowQuickSwitch) {
return true;
}
mTaskFocusIndexOverride = uiController.launchFocusedTask();
if (mTaskFocusIndexOverride == -1) {
return true;
}
}
if (cmd.type == TYPE_KEYBOARD_INPUT && allowQuickSwitch) {
uiController.openQuickSwitchView();
return true;
}
if (cmd.type == TYPE_HOME) {

View File

@@ -38,7 +38,8 @@ import com.android.launcher3.anim.Interpolators;
* 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.
* 3. Call {@link BorderAnimator#buildAnimator(boolean)} and start the animation or call
* {@link BorderAnimator#setBorderVisible(boolean)} where appropriate.
*/
public final class BorderAnimator {
@@ -138,6 +139,7 @@ public final class BorderAnimator {
/**
* Builds the border appearance/disappearance animation.
*/
@NonNull
public Animator buildAnimator(boolean isAppearing) {
mBorderBoundsBuilder.updateBorderBounds(mBorderBounds);
mRunningBorderAnimation = mBorderAnimationProgress.animateToValue(isAppearing ? 1f : 0f);
@@ -150,6 +152,18 @@ public final class BorderAnimator {
return mRunningBorderAnimation;
}
/**
* Immediately shows/hides the border without an animation.
*
* To animate the appearance/disappearance, see {@link BorderAnimator#buildAnimator(boolean)}
*/
public void setBorderVisible(boolean visible) {
if (mRunningBorderAnimation != null) {
mRunningBorderAnimation.end();
}
mBorderAnimationProgress.updateValue(visible ? 1f : 0f);
}
/**
* Callback to update the border bounds when building this animation.
*/

View File

@@ -52,6 +52,7 @@ abstract class TaskbarBaseTestCase {
@Mock lateinit var taskbarTranslationController: TaskbarTranslationController
@Mock lateinit var taskbarOverlayController: TaskbarOverlayController
@Mock lateinit var taskbarEduTooltipController: TaskbarEduTooltipController
@Mock lateinit var keyboardQuickSwitchController: KeyboardQuickSwitchController
lateinit var mTaskbarControllers: TaskbarControllers
@@ -90,6 +91,7 @@ abstract class TaskbarBaseTestCase {
taskbarTranslationController,
taskbarRecentAppsController,
taskbarEduTooltipController,
keyboardQuickSwitchController
)
}
}