Merge "Add tracing for non-loading model" into ub-launcher3-qt-dev
diff --git a/Android.mk b/Android.mk
index a817415..6568a26 100644
--- a/Android.mk
+++ b/Android.mk
@@ -251,7 +251,7 @@
LOCAL_PACKAGE_NAME := Launcher3QuickStepGo
LOCAL_PRIVILEGED_MODULE := true
LOCAL_PRODUCT_MODULE := true
-LOCAL_OVERRIDES_PACKAGES := Home Launcher2 Launcher3 Launcher3QuickStep
+LOCAL_OVERRIDES_PACKAGES := Home Launcher2 Launcher3 Launcher3QuickStep Launcher3GoIconRecents
LOCAL_REQUIRED_MODULES := privapp_whitelist_com.android.launcher3
LOCAL_FULL_LIBS_MANIFEST_FILES := \
@@ -297,7 +297,8 @@
LOCAL_PACKAGE_NAME := Launcher3GoIconRecents
LOCAL_PRIVILEGED_MODULE := true
-LOCAL_OVERRIDES_PACKAGES := Home Launcher2 Launcher3 Launcher3QuickStep
+LOCAL_PRODUCT_MODULE := true
+LOCAL_OVERRIDES_PACKAGES := Home Launcher2 Launcher3 Launcher3QuickStep Launcher3QuickStepGo
LOCAL_REQUIRED_MODULES := privapp_whitelist_com.android.launcher3
LOCAL_FULL_LIBS_MANIFEST_FILES := \
diff --git a/go/quickstep/res/values/dimens.xml b/go/quickstep/res/drawable/default_thumbnail.xml
similarity index 61%
copy from go/quickstep/res/values/dimens.xml
copy to go/quickstep/res/drawable/default_thumbnail.xml
index e2fa387..0a2dbf0 100644
--- a/go/quickstep/res/values/dimens.xml
+++ b/go/quickstep/res/drawable/default_thumbnail.xml
@@ -14,13 +14,9 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<resources>
- <dimen name="task_item_half_vert_margin">8dp</dimen>
- <dimen name="task_thumbnail_and_icon_view_size">60dp</dimen>
- <dimen name="task_thumbnail_height">60dp</dimen>
- <dimen name="task_thumbnail_width">36dp</dimen>
- <dimen name="task_icon_size">36dp</dimen>
-
- <dimen name="clear_all_button_width">106dp</dimen>
- <dimen name="clear_all_button_height">36dp</dimen>
-</resources>
\ No newline at end of file
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@android:color/darker_gray"/>
+ <corners android:radius="2dp"/>
+</shape>
diff --git a/go/quickstep/res/values/dimens.xml b/go/quickstep/res/drawable/empty_content_box.xml
similarity index 61%
rename from go/quickstep/res/values/dimens.xml
rename to go/quickstep/res/drawable/empty_content_box.xml
index e2fa387..a488388 100644
--- a/go/quickstep/res/values/dimens.xml
+++ b/go/quickstep/res/drawable/empty_content_box.xml
@@ -14,13 +14,10 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<resources>
- <dimen name="task_item_half_vert_margin">8dp</dimen>
- <dimen name="task_thumbnail_and_icon_view_size">60dp</dimen>
- <dimen name="task_thumbnail_height">60dp</dimen>
- <dimen name="task_thumbnail_width">36dp</dimen>
- <dimen name="task_icon_size">36dp</dimen>
-
- <dimen name="clear_all_button_width">106dp</dimen>
- <dimen name="clear_all_button_height">36dp</dimen>
-</resources>
\ No newline at end of file
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@android:color/transparent"/>
+ <stroke android:color="@android:color/white" android:width="4px"/>
+ <corners android:radius="2dp"/>
+</shape>
\ No newline at end of file
diff --git a/go/quickstep/res/layout/clear_all_button.xml b/go/quickstep/res/layout/clear_all_button.xml
new file mode 100644
index 0000000..003ee86
--- /dev/null
+++ b/go/quickstep/res/layout/clear_all_button.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 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.quickstep.views.ClearAllItemView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/clear_all_item_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <Button
+ android:id="@+id/clear_all_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:background="@drawable/clear_all_button"
+ android:gravity="center"
+ android:text="@string/recents_clear_all"
+ android:textAllCaps="false"
+ android:textColor="@color/clear_all_button_text"
+ android:textSize="14sp"/>
+</com.android.quickstep.views.ClearAllItemView>
diff --git a/go/quickstep/res/layout/icon_recents_root_view.xml b/go/quickstep/res/layout/icon_recents_root_view.xml
index fddb1d3..6fb7e19 100644
--- a/go/quickstep/res/layout/icon_recents_root_view.xml
+++ b/go/quickstep/res/layout/icon_recents_root_view.xml
@@ -19,31 +19,11 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
- <LinearLayout
- android:id="@+id/recent_task_content_view"
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/recent_task_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:orientation="vertical"
- android:visibility="gone">
- <androidx.recyclerview.widget.RecyclerView
- android:id="@+id/recent_task_recycler_view"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_weight="1"
- android:scrollbars="none"/>
- <Button
- android:id="@+id/clear_all_button"
- android:layout_width="@dimen/clear_all_button_width"
- android:layout_height="@dimen/clear_all_button_height"
- android:layout_gravity="center_horizontal"
- android:layout_marginVertical="@dimen/task_item_half_vert_margin"
- android:background="@drawable/clear_all_button"
- android:gravity="center"
- android:text="@string/recents_clear_all"
- android:textAllCaps="false"
- android:textColor="@color/clear_all_button_text"
- android:textSize="14sp"/>
- </LinearLayout>
+ android:scrollbars="none"/>
<TextView
android:id="@+id/recent_task_empty_view"
android:layout_width="match_parent"
diff --git a/go/quickstep/res/layout/task_item_view.xml b/go/quickstep/res/layout/task_item_view.xml
index 048e9c5..1483d4c 100644
--- a/go/quickstep/res/layout/task_item_view.xml
+++ b/go/quickstep/res/layout/task_item_view.xml
@@ -17,26 +17,23 @@
<com.android.quickstep.views.TaskItemView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
+ android:layout_height="match_parent"
android:orientation="horizontal">
- <FrameLayout
+ <com.android.quickstep.views.TaskThumbnailIconView
android:id="@+id/task_icon_and_thumbnail"
- android:layout_width="@dimen/task_thumbnail_and_icon_view_size"
- android:layout_height="@dimen/task_thumbnail_and_icon_view_size"
- android:layout_gravity="center_vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
android:layout_marginHorizontal="8dp"
- android:layout_marginVertical="@dimen/task_item_half_vert_margin">
+ android:layout_marginTop="16dp">
<ImageView
android:id="@+id/task_thumbnail"
- android:layout_width="@dimen/task_thumbnail_width"
- android:layout_height="@dimen/task_thumbnail_height"
- android:layout_gravity="top|start"/>
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
<ImageView
android:id="@+id/task_icon"
- android:layout_width="@dimen/task_icon_size"
- android:layout_height="@dimen/task_icon_size"
- android:layout_gravity="bottom|end"/>
- </FrameLayout>
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+ </com.android.quickstep.views.TaskThumbnailIconView>
<TextView
android:id="@+id/task_label"
android:layout_width="wrap_content"
diff --git a/go/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/go/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
index 6730e97..d20910f 100644
--- a/go/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/go/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -18,6 +18,7 @@
import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS;
import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
+import static com.android.launcher3.states.RotationHelper.REQUEST_ROTATE;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Launcher;
@@ -51,6 +52,9 @@
public void onStateEnabled(Launcher launcher) {
IconRecentsView recentsView = launcher.getOverviewPanel();
recentsView.onBeginTransitionToOverview();
+ // Request orientation be set to unspecified, letting the system decide the best
+ // orientation.
+ launcher.getRotationHelper().setCurrentStateRequest(REQUEST_ROTATE);
}
@Override
diff --git a/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java b/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java
index d1d697c..c228bb9 100644
--- a/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java
+++ b/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java
@@ -131,10 +131,11 @@
return anim;
}
- View thumbnailView = mRecentsView.getThumbnailViewForTask(mTargetTaskId);
+ View thumbnailView = mRecentsView.getBottomThumbnailView();
if (thumbnailView == null) {
- // TODO: We should either 1) guarantee the view is loaded before attempting this
- // or 2) have a backup animation.
+ // This can be null if there were previously 0 tasks and the recycler view has not had
+ // enough time to take in the data change, bind a new view, and lay out the new view.
+ // TODO: Have a fallback to animate to
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "No thumbnail view for running task. Using stub animation.");
}
diff --git a/go/quickstep/src/com/android/quickstep/ClearAllHolder.java b/go/quickstep/src/com/android/quickstep/ClearAllHolder.java
new file mode 100644
index 0000000..ce87171
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/ClearAllHolder.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2019 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;
+
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+
+/**
+ * Holder for clear all button view in task recycler view.
+ */
+final class ClearAllHolder extends ViewHolder {
+ public ClearAllHolder(@NonNull View itemView) {
+ super(itemView);
+ }
+}
diff --git a/go/quickstep/src/com/android/quickstep/ContentFillItemAnimator.java b/go/quickstep/src/com/android/quickstep/ContentFillItemAnimator.java
new file mode 100644
index 0000000..1b6f2e3
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/ContentFillItemAnimator.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2019 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;
+
+import static android.view.View.ALPHA;
+
+import static com.android.quickstep.TaskAdapter.CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT;
+import static com.android.quickstep.views.TaskItemView.CONTENT_TRANSITION_PROGRESS;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+import androidx.recyclerview.widget.SimpleItemAnimator;
+
+import com.android.quickstep.views.TaskItemView;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * An item animator that is only set and used for the transition from the empty loading UI to
+ * the filled task content UI. The animation starts from the bottom to top, changing all valid
+ * empty item views to be filled and removing all extra empty views.
+ */
+public final class ContentFillItemAnimator extends SimpleItemAnimator {
+
+ private static final class PendingAnimation {
+ ViewHolder viewHolder;
+ int animType;
+
+ PendingAnimation(ViewHolder vh, int type) {
+ viewHolder = vh;
+ animType = type;
+ }
+ }
+
+ private static final int ANIM_TYPE_REMOVE = 0;
+ private static final int ANIM_TYPE_CHANGE = 1;
+
+ private static final int ITEM_BETWEEN_DELAY = 40;
+ private static final int ITEM_CHANGE_DURATION = 150;
+ private static final int ITEM_REMOVE_DURATION = 150;
+
+ /**
+ * Animations that have been registered to occur together at the next call of
+ * {@link #runPendingAnimations()} but have not started.
+ */
+ private final ArrayList<PendingAnimation> mPendingAnims = new ArrayList<>();
+
+ /**
+ * Animations that have started and are running.
+ */
+ private final ArrayList<ObjectAnimator> mRunningAnims = new ArrayList<>();
+
+ private Runnable mOnFinishRunnable;
+
+ /**
+ * Set runnable to run after the content fill animation is fully completed.
+ *
+ * @param runnable runnable to run on end
+ */
+ public void setOnAnimationFinishedRunnable(Runnable runnable) {
+ mOnFinishRunnable = runnable;
+ }
+
+ @Override
+ public void setChangeDuration(long changeDuration) {
+ throw new UnsupportedOperationException("Cascading item animator cannot have animation "
+ + "duration changed.");
+ }
+
+ @Override
+ public void setRemoveDuration(long removeDuration) {
+ throw new UnsupportedOperationException("Cascading item animator cannot have animation "
+ + "duration changed.");
+ }
+
+ @Override
+ public boolean animateRemove(ViewHolder holder) {
+ PendingAnimation pendAnim = new PendingAnimation(holder, ANIM_TYPE_REMOVE);
+ mPendingAnims.add(pendAnim);
+ return true;
+ }
+
+ private void animateRemoveImpl(ViewHolder holder, long startDelay) {
+ final View view = holder.itemView;
+ if (holder.itemView.getAlpha() == 0) {
+ // View is already visually removed. We can just get rid of it now.
+ view.setAlpha(1.0f);
+ dispatchRemoveFinished(holder);
+ dispatchFinishedWhenDone();
+ return;
+ }
+ final ObjectAnimator anim = ObjectAnimator.ofFloat(
+ holder.itemView, ALPHA, holder.itemView.getAlpha(), 0.0f);
+ anim.setDuration(ITEM_REMOVE_DURATION).setStartDelay(startDelay);
+ anim.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ dispatchRemoveStarting(holder);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ view.setAlpha(1);
+ dispatchRemoveFinished(holder);
+ mRunningAnims.remove(anim);
+ dispatchFinishedWhenDone();
+ }
+ }
+ );
+ anim.start();
+ mRunningAnims.add(anim);
+ }
+
+ @Override
+ public boolean animateAdd(ViewHolder holder) {
+ dispatchAddFinished(holder);
+ return false;
+ }
+
+ @Override
+ public boolean animateMove(ViewHolder holder, int fromX, int fromY, int toX,
+ int toY) {
+ dispatchMoveFinished(holder);
+ return false;
+ }
+
+ @Override
+ public boolean animateChange(ViewHolder oldHolder,
+ ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop) {
+ // Only support changes where the holders are the same
+ if (oldHolder == newHolder) {
+ PendingAnimation pendAnim = new PendingAnimation(oldHolder, ANIM_TYPE_CHANGE);
+ mPendingAnims.add(pendAnim);
+ return true;
+ }
+ dispatchChangeFinished(oldHolder, true /* oldItem */);
+ dispatchChangeFinished(newHolder, false /* oldItem */);
+ return false;
+ }
+
+ private void animateChangeImpl(ViewHolder viewHolder, long startDelay) {
+ TaskItemView itemView = (TaskItemView) viewHolder.itemView;
+ final ObjectAnimator anim =
+ ObjectAnimator.ofFloat(itemView, CONTENT_TRANSITION_PROGRESS, 0.0f, 1.0f);
+ anim.setDuration(ITEM_CHANGE_DURATION).setStartDelay(startDelay);
+ anim.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ dispatchChangeStarting(viewHolder, true /* oldItem */);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ dispatchChangeFinished(viewHolder, true /* oldItem */);
+ mRunningAnims.remove(anim);
+ dispatchFinishedWhenDone();
+ }
+ }
+ );
+ anim.start();
+ mRunningAnims.add(anim);
+ }
+
+ @Override
+ public void runPendingAnimations() {
+ // Run animations bottom to top.
+ mPendingAnims.sort(Comparator.comparingInt(o -> -o.viewHolder.itemView.getBottom()));
+ int delay = 0;
+ while (!mPendingAnims.isEmpty()) {
+ PendingAnimation curAnim = mPendingAnims.remove(0);
+ ViewHolder vh = curAnim.viewHolder;
+ switch (curAnim.animType) {
+ case ANIM_TYPE_REMOVE:
+ animateRemoveImpl(vh, delay);
+ break;
+ case ANIM_TYPE_CHANGE:
+ animateChangeImpl(vh, delay);
+ break;
+ default:
+ break;
+ }
+ delay += ITEM_BETWEEN_DELAY;
+ }
+ }
+
+ @Override
+ public void endAnimation(@NonNull ViewHolder item) {
+ for (int i = mPendingAnims.size() - 1; i >= 0; i--) {
+ PendingAnimation pendAnim = mPendingAnims.get(i);
+ if (pendAnim.viewHolder == item) {
+ mPendingAnims.remove(i);
+ switch (pendAnim.animType) {
+ case ANIM_TYPE_REMOVE:
+ dispatchRemoveFinished(item);
+ break;
+ case ANIM_TYPE_CHANGE:
+ dispatchChangeFinished(item, true /* oldItem */);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ dispatchFinishedWhenDone();
+ }
+
+ @Override
+ public void endAnimations() {
+ for (int i = mPendingAnims.size() - 1; i >= 0; i--) {
+ PendingAnimation pendAnim = mPendingAnims.get(i);
+ ViewHolder item = pendAnim.viewHolder;
+ switch (pendAnim.animType) {
+ case ANIM_TYPE_REMOVE:
+ dispatchRemoveFinished(item);
+ break;
+ case ANIM_TYPE_CHANGE:
+ dispatchChangeFinished(item, true /* oldItem */);
+ break;
+ default:
+ break;
+ }
+ mPendingAnims.remove(i);
+ }
+ for (int i = 0; i < mRunningAnims.size(); i++) {
+ ObjectAnimator anim = mRunningAnims.get(i);
+ anim.end();
+ }
+ dispatchAnimationsFinished();
+ }
+
+ @Override
+ public boolean isRunning() {
+ return !mPendingAnims.isEmpty() || !mRunningAnims.isEmpty();
+ }
+
+ @Override
+ public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,
+ @NonNull List<Object> payloads) {
+ if (!payloads.isEmpty()
+ && (int) payloads.get(0) == CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT) {
+ return true;
+ }
+ return super.canReuseUpdatedViewHolder(viewHolder, payloads);
+ }
+
+ private void dispatchFinishedWhenDone() {
+ if (!isRunning()) {
+ dispatchAnimationsFinished();
+ if (mOnFinishRunnable != null) {
+ mOnFinishRunnable.run();
+ }
+ }
+ }
+}
diff --git a/go/quickstep/src/com/android/quickstep/TaskActionController.java b/go/quickstep/src/com/android/quickstep/TaskActionController.java
index 71bee91..09e2367 100644
--- a/go/quickstep/src/com/android/quickstep/TaskActionController.java
+++ b/go/quickstep/src/com/android/quickstep/TaskActionController.java
@@ -15,6 +15,8 @@
*/
package com.android.quickstep;
+import static com.android.quickstep.TaskAdapter.TASKS_START_POSITION;
+
import android.app.ActivityOptions;
import android.view.View;
@@ -42,7 +44,7 @@
* @param viewHolder the task view holder to launch
*/
public void launchTask(TaskHolder viewHolder) {
- if (viewHolder.getTask() == null) {
+ if (!viewHolder.getTask().isPresent()) {
return;
}
TaskItemView itemView = (TaskItemView) (viewHolder.itemView);
@@ -53,8 +55,9 @@
int height = v.getMeasuredHeight();
ActivityOptions opts = ActivityOptions.makeClipRevealAnimation(v, left, top, width, height);
- ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(viewHolder.getTask().key,
- opts, null /* resultCallback */, null /* resultCallbackHandler */);
+ ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(
+ viewHolder.getTask().get().key, opts, null /* resultCallback */,
+ null /* resultCallbackHandler */);
}
/**
@@ -63,11 +66,11 @@
* @param viewHolder the task view holder to remove
*/
public void removeTask(TaskHolder viewHolder) {
- if (viewHolder.getTask() == null) {
+ if (!viewHolder.getTask().isPresent()) {
return;
}
int position = viewHolder.getAdapterPosition();
- Task task = viewHolder.getTask();
+ Task task = viewHolder.getTask().get();
ActivityManagerWrapper.getInstance().removeTask(task.key.id);
mLoader.removeTask(task);
mAdapter.notifyItemRemoved(position);
@@ -80,6 +83,6 @@
int count = mAdapter.getItemCount();
ActivityManagerWrapper.getInstance().removeAllRecentTasks();
mLoader.clearAllTasks();
- mAdapter.notifyItemRangeRemoved(0 /* positionStart */, count);
+ mAdapter.notifyItemRangeRemoved(TASKS_START_POSITION /* positionStart */, count);
}
}
diff --git a/go/quickstep/src/com/android/quickstep/TaskAdapter.java b/go/quickstep/src/com/android/quickstep/TaskAdapter.java
index 674fcae..6f75629 100644
--- a/go/quickstep/src/com/android/quickstep/TaskAdapter.java
+++ b/go/quickstep/src/com/android/quickstep/TaskAdapter.java
@@ -15,13 +15,15 @@
*/
package com.android.quickstep;
-import android.util.ArrayMap;
import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
import android.view.ViewGroup;
+import android.widget.Button;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView.Adapter;
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import com.android.launcher3.R;
import com.android.quickstep.views.TaskItemView;
@@ -29,18 +31,25 @@
import java.util.List;
import java.util.Objects;
+import java.util.Optional;
/**
* Recycler view adapter that dynamically inflates and binds {@link TaskHolder} instances with the
* appropriate {@link Task} from the recents task list.
*/
-public final class TaskAdapter extends Adapter<TaskHolder> {
+public final class TaskAdapter extends Adapter<ViewHolder> {
- private static final int MAX_TASKS_TO_DISPLAY = 6;
+ public static final int CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT = 0;
+ public static final int MAX_TASKS_TO_DISPLAY = 6;
+ public static final int TASKS_START_POSITION = 1;
+
+ public static final int ITEM_TYPE_TASK = 0;
+ public static final int ITEM_TYPE_CLEAR_ALL = 1;
+
private static final String TAG = "TaskAdapter";
private final TaskListLoader mLoader;
- private final ArrayMap<Integer, TaskItemView> mTaskIdToViewMap = new ArrayMap<>();
private TaskActionController mTaskActionController;
+ private OnClickListener mClearAllListener;
private boolean mIsShowingLoadingUi;
public TaskAdapter(@NonNull TaskListLoader loader) {
@@ -51,6 +60,10 @@
mTaskActionController = taskActionController;
}
+ public void setOnClearAllClickListener(OnClickListener listener) {
+ mClearAllListener = listener;
+ }
+
/**
* Sets all positions in the task adapter to loading views, binding new views if necessary.
* This changes the task adapter's view of the data, so the appropriate notify events should be
@@ -63,75 +76,103 @@
mIsShowingLoadingUi = isShowingLoadingUi;
}
- /**
- * Get task item view for a given task id if it's attached to the view.
- *
- * @param taskId task id to search for
- * @return corresponding task item view if it's attached, null otherwise
- */
- public @Nullable TaskItemView getTaskItemView(int taskId) {
- return mTaskIdToViewMap.get(taskId);
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ switch (viewType) {
+ case ITEM_TYPE_TASK:
+ TaskItemView itemView = (TaskItemView) LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.task_item_view, parent, false);
+ TaskHolder taskHolder = new TaskHolder(itemView);
+ itemView.setOnClickListener(view -> mTaskActionController.launchTask(taskHolder));
+ return taskHolder;
+ case ITEM_TYPE_CLEAR_ALL:
+ View clearView = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.clear_all_button, parent, false);
+ ClearAllHolder clearAllHolder = new ClearAllHolder(clearView);
+ Button clearViewButton = clearView.findViewById(R.id.clear_all_button);
+ clearViewButton.setOnClickListener(mClearAllListener);
+ return clearAllHolder;
+ default:
+ throw new IllegalArgumentException("No known holder for item type: " + viewType);
+ }
}
@Override
- public TaskHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- TaskItemView itemView = (TaskItemView) LayoutInflater.from(parent.getContext())
- .inflate(R.layout.task_item_view, parent, false);
- TaskHolder holder = new TaskHolder(itemView);
- itemView.setOnClickListener(view -> mTaskActionController.launchTask(holder));
- return holder;
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ onBindViewHolderInternal(holder, position, false /* willAnimate */);
}
@Override
- public void onBindViewHolder(TaskHolder holder, int position) {
- if (mIsShowingLoadingUi) {
- holder.bindEmptyUi();
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position,
+ @NonNull List<Object> payloads) {
+ if (payloads.isEmpty()) {
+ super.onBindViewHolder(holder, position, payloads);
return;
}
- List<Task> tasks = mLoader.getCurrentTaskList();
- if (position >= tasks.size()) {
- // Task list has updated.
- return;
+ int changeType = (int) payloads.get(0);
+ if (changeType == CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT) {
+ // Bind in preparation for animation
+ onBindViewHolderInternal(holder, position, true /* willAnimate */);
+ } else {
+ throw new IllegalArgumentException("Payload content is not a valid change event type: "
+ + changeType);
}
- Task task = tasks.get(position);
- holder.bindTask(task);
- mLoader.loadTaskIconAndLabel(task, () -> {
- // Ensure holder still has the same task.
- if (Objects.equals(task, holder.getTask())) {
- holder.getTaskItemView().setIcon(task.icon);
- holder.getTaskItemView().setLabel(task.titleDescription);
- }
- });
- mLoader.loadTaskThumbnail(task, () -> {
- if (Objects.equals(task, holder.getTask())) {
- holder.getTaskItemView().setThumbnail(task.thumbnail.thumbnail);
- }
- });
+ }
+
+ private void onBindViewHolderInternal(@NonNull ViewHolder holder, int position,
+ boolean willAnimate) {
+ int itemType = getItemViewType(position);
+ switch (itemType) {
+ case ITEM_TYPE_TASK:
+ TaskHolder taskHolder = (TaskHolder) holder;
+ if (mIsShowingLoadingUi) {
+ taskHolder.bindEmptyUi();
+ return;
+ }
+ List<Task> tasks = mLoader.getCurrentTaskList();
+ int taskPos = position - TASKS_START_POSITION;
+ if (taskPos >= tasks.size()) {
+ // Task list has updated.
+ return;
+ }
+ Task task = tasks.get(taskPos);
+ taskHolder.bindTask(task, willAnimate /* willAnimate */);
+ mLoader.loadTaskIconAndLabel(task, () -> {
+ // Ensure holder still has the same task.
+ if (Objects.equals(Optional.of(task), taskHolder.getTask())) {
+ taskHolder.getTaskItemView().setIcon(task.icon);
+ taskHolder.getTaskItemView().setLabel(task.titleDescription);
+ }
+ });
+ mLoader.loadTaskThumbnail(task, () -> {
+ if (Objects.equals(Optional.of(task), taskHolder.getTask())) {
+ taskHolder.getTaskItemView().setThumbnail(task.thumbnail.thumbnail);
+ }
+ });
+ break;
+ case ITEM_TYPE_CLEAR_ALL:
+ // Nothing to bind.
+ break;
+ default:
+ throw new IllegalArgumentException("No known holder for item type: " + itemType);
+ }
}
@Override
- public void onViewAttachedToWindow(@NonNull TaskHolder holder) {
- if (holder.getTask() == null) {
- return;
- }
- mTaskIdToViewMap.put(holder.getTask().key.id, (TaskItemView) holder.itemView);
- }
-
- @Override
- public void onViewDetachedFromWindow(@NonNull TaskHolder holder) {
- if (holder.getTask() == null) {
- return;
- }
- mTaskIdToViewMap.remove(holder.getTask().key.id);
+ public int getItemViewType(int position) {
+ // Bottom is always clear all button.
+ return (position == 0) ? ITEM_TYPE_CLEAR_ALL : ITEM_TYPE_TASK;
}
@Override
public int getItemCount() {
+ int itemCount = TASKS_START_POSITION;
if (mIsShowingLoadingUi) {
// Show loading version of all items.
- return MAX_TASKS_TO_DISPLAY;
+ itemCount += MAX_TASKS_TO_DISPLAY;
} else {
- return Math.min(mLoader.getCurrentTaskList().size(), MAX_TASKS_TO_DISPLAY);
+ itemCount += Math.min(mLoader.getCurrentTaskList().size(), MAX_TASKS_TO_DISPLAY);
}
+ return itemCount;
}
}
diff --git a/go/quickstep/src/com/android/quickstep/TaskHolder.java b/go/quickstep/src/com/android/quickstep/TaskHolder.java
index 98dc989..5755df4 100644
--- a/go/quickstep/src/com/android/quickstep/TaskHolder.java
+++ b/go/quickstep/src/com/android/quickstep/TaskHolder.java
@@ -15,12 +15,16 @@
*/
package com.android.quickstep;
-import androidx.annotation.Nullable;
+import android.graphics.Bitmap;
+
+import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import com.android.quickstep.views.TaskItemView;
import com.android.systemui.shared.recents.model.Task;
+import java.util.Optional;
+
/**
* A recycler view holder that holds the task view and binds {@link Task} content (app title, icon,
* etc.) to the view.
@@ -40,13 +44,28 @@
}
/**
- * Bind a task to the holder, resetting the view and preparing it for content to load in.
+ * Bind the task model to the holder. This will take the current task content in the task
+ * object (i.e. icon, thumbnail, label) and either apply the content immediately or simply bind
+ * the content to animate to at a later time. If the task does not have all its content loaded,
+ * the view will prepare appropriate default placeholders and it is the callers responsibility
+ * to change them at a later time.
+ *
+ * Regardless of whether it is animating, input handlers will be bound immediately (see
+ * {@link TaskActionController}).
*
* @param task the task to bind to the view
+ * @param willAnimate true if UI should animate in later, false if it should apply immediately
*/
- public void bindTask(Task task) {
+ public void bindTask(@NonNull Task task, boolean willAnimate) {
mTask = task;
- mTaskItemView.resetTaskItemView();
+ Bitmap thumbnail = (task.thumbnail != null) ? task.thumbnail.thumbnail : null;
+ if (willAnimate) {
+ mTaskItemView.startContentAnimation(task.icon, thumbnail, task.titleDescription);
+ } else {
+ mTaskItemView.setIcon(task.icon);
+ mTaskItemView.setThumbnail(thumbnail);
+ mTaskItemView.setLabel(task.titleDescription);
+ }
}
/**
@@ -55,10 +74,7 @@
*/
public void bindEmptyUi() {
mTask = null;
- // TODO: Set the task view to a loading, empty UI.
- // Temporarily using the one below for visual confirmation but should be swapped out to new
- // UI later.
- mTaskItemView.resetTaskItemView();
+ mTaskItemView.resetToEmptyUi();
}
/**
@@ -66,7 +82,7 @@
*
* @return the current task
*/
- public @Nullable Task getTask() {
- return mTask;
+ public Optional<Task> getTask() {
+ return Optional.ofNullable(mTask);
}
}
diff --git a/go/quickstep/src/com/android/quickstep/TaskListLoader.java b/go/quickstep/src/com/android/quickstep/TaskListLoader.java
index 51b73f1..850c7e6 100644
--- a/go/quickstep/src/com/android/quickstep/TaskListLoader.java
+++ b/go/quickstep/src/com/android/quickstep/TaskListLoader.java
@@ -38,23 +38,9 @@
private ArrayList<Task> mTaskList = new ArrayList<>();
private int mTaskListChangeId;
- private RecentsModel.TaskThumbnailChangeListener listener = (taskId, thumbnailData) -> {
- Task foundTask = null;
- for (Task task : mTaskList) {
- if (task.key.id == taskId) {
- foundTask = task;
- break;
- }
- }
- if (foundTask != null) {
- foundTask.thumbnail = thumbnailData;
- }
- return foundTask;
- };
public TaskListLoader(Context context) {
mRecentsModel = RecentsModel.INSTANCE.get(context);
- mRecentsModel.addThumbnailChangeListener(listener);
}
/**
diff --git a/go/quickstep/src/com/android/quickstep/TaskSwipeCallback.java b/go/quickstep/src/com/android/quickstep/TaskSwipeCallback.java
index 98407d8..19951bb 100644
--- a/go/quickstep/src/com/android/quickstep/TaskSwipeCallback.java
+++ b/go/quickstep/src/com/android/quickstep/TaskSwipeCallback.java
@@ -17,6 +17,9 @@
import static androidx.recyclerview.widget.ItemTouchHelper.RIGHT;
+import static com.android.quickstep.TaskAdapter.ITEM_TYPE_CLEAR_ALL;
+
+import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
@@ -45,4 +48,14 @@
mTaskActionController.removeTask((TaskHolder) viewHolder);
}
}
+
+ @Override
+ public int getSwipeDirs(@NonNull RecyclerView recyclerView,
+ @NonNull ViewHolder viewHolder) {
+ if (viewHolder.getItemViewType() == ITEM_TYPE_CLEAR_ALL) {
+ // Clear all button should not be swipable.
+ return 0;
+ }
+ return super.getSwipeDirs(recyclerView, viewHolder);
+ }
}
diff --git a/go/quickstep/src/com/android/quickstep/ThumbnailDrawable.java b/go/quickstep/src/com/android/quickstep/ThumbnailDrawable.java
new file mode 100644
index 0000000..6ef9039
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/ThumbnailDrawable.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2019 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;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+import androidx.annotation.NonNull;
+
+import com.android.systemui.shared.recents.model.ThumbnailData;
+
+/**
+ * Bitmap backed drawable that supports rotating the thumbnail bitmap depending on if the
+ * orientation the thumbnail was taken in matches the desired orientation. In addition, the
+ * thumbnail always fills into the containing bounds.
+ */
+public final class ThumbnailDrawable extends Drawable {
+
+ private final Paint mPaint = new Paint();
+ private final Matrix mMatrix = new Matrix();
+ private final ThumbnailData mThumbnailData;
+ private int mRequestedOrientation;
+
+ public ThumbnailDrawable(@NonNull ThumbnailData thumbnailData, int requestedOrientation) {
+ mThumbnailData = thumbnailData;
+ mRequestedOrientation = requestedOrientation;
+ updateMatrix();
+ }
+
+ /**
+ * Set the requested orientation.
+ *
+ * @param orientation the orientation we want the thumbnail to be in
+ */
+ public void setRequestedOrientation(int orientation) {
+ if (mRequestedOrientation != orientation) {
+ mRequestedOrientation = orientation;
+ updateMatrix();
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ if (mThumbnailData.thumbnail == null) {
+ return;
+ }
+ canvas.drawBitmap(mThumbnailData.thumbnail, mMatrix, mPaint);
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ super.onBoundsChange(bounds);
+ updateMatrix();
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ final int oldAlpha = mPaint.getAlpha();
+ if (alpha != oldAlpha) {
+ mPaint.setAlpha(alpha);
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public int getAlpha() {
+ return mPaint.getAlpha();
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {
+ mPaint.setColorFilter(colorFilter);
+ invalidateSelf();
+ }
+
+ @Override
+ public ColorFilter getColorFilter() {
+ return mPaint.getColorFilter();
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ private void updateMatrix() {
+ if (mThumbnailData.thumbnail == null) {
+ return;
+ }
+ mMatrix.reset();
+ float scaleX;
+ float scaleY;
+ Rect bounds = getBounds();
+ Bitmap thumbnail = mThumbnailData.thumbnail;
+ if (mRequestedOrientation != mThumbnailData.orientation) {
+ // Rotate and translate so that top left is the same.
+ mMatrix.postRotate(90, 0, 0);
+ mMatrix.postTranslate(thumbnail.getHeight(), 0);
+
+ scaleX = (float) bounds.width() / thumbnail.getHeight();
+ scaleY = (float) bounds.height() / thumbnail.getWidth();
+ } else {
+ scaleX = (float) bounds.width() / thumbnail.getWidth();
+ scaleY = (float) bounds.height() / thumbnail.getHeight();
+ }
+ // Scale to fill.
+ mMatrix.postScale(scaleX, scaleY);
+ }
+}
diff --git a/go/quickstep/src/com/android/quickstep/views/ClearAllItemView.java b/go/quickstep/src/com/android/quickstep/views/ClearAllItemView.java
new file mode 100644
index 0000000..378dbf4
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/views/ClearAllItemView.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2019 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.views;
+
+import static com.android.quickstep.views.TaskLayoutUtils.getClearAllItemHeight;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+/**
+ * Recycler view item that lays out the clear all button and measures the space it takes based on
+ * the device height.
+ */
+public final class ClearAllItemView extends FrameLayout {
+
+ public ClearAllItemView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int buttonHeight = getClearAllItemHeight(getContext());
+ int newHeightSpec = MeasureSpec.makeMeasureSpec(buttonHeight, MeasureSpec.EXACTLY);
+ super.onMeasure(widthMeasureSpec, newHeightSpec);
+ }
+}
diff --git a/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java b/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
index c06b6ec..b7ed5b5 100644
--- a/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
+++ b/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
@@ -15,8 +15,13 @@
*/
package com.android.quickstep.views;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+
import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL;
+import static com.android.quickstep.TaskAdapter.CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT;
+import static com.android.quickstep.TaskAdapter.TASKS_START_POSITION;
+
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
@@ -24,30 +29,37 @@
import android.animation.PropertyValuesHolder;
import android.animation.ValueAnimator;
import android.content.Context;
+import android.util.ArraySet;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.view.View;
import android.view.ViewDebug;
-import android.view.animation.AlphaAnimation;
-import android.view.animation.Animation;
-import android.view.animation.AnimationSet;
-import android.view.animation.LayoutAnimationController;
+import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
+import androidx.recyclerview.widget.RecyclerView.OnChildAttachStateChangeListener;
+import com.android.launcher3.BaseActivity;
import com.android.launcher3.R;
+import com.android.quickstep.ContentFillItemAnimator;
+import com.android.quickstep.RecentsModel;
import com.android.quickstep.RecentsToActivityHelper;
import com.android.quickstep.TaskActionController;
import com.android.quickstep.TaskAdapter;
import com.android.quickstep.TaskHolder;
import com.android.quickstep.TaskListLoader;
import com.android.quickstep.TaskSwipeCallback;
+import com.android.systemui.shared.recents.model.Task;
+
+import java.util.ArrayList;
+import java.util.Optional;
/**
* Root view for the icon recents view. Acts as the main interface to the rest of the Launcher code
@@ -89,24 +101,48 @@
private final Context mContext;
private final TaskListLoader mTaskLoader;
private final TaskAdapter mTaskAdapter;
+ private final LinearLayoutManager mTaskLayoutManager;
private final TaskActionController mTaskActionController;
- private final LayoutAnimationController mLayoutAnimation;
+ private final DefaultItemAnimator mDefaultItemAnimator = new DefaultItemAnimator();
+ private final ContentFillItemAnimator mLoadingContentItemAnimator =
+ new ContentFillItemAnimator();
private RecentsToActivityHelper mActivityHelper;
private RecyclerView mTaskRecyclerView;
+ private View mShowingContentView;
private View mEmptyView;
private View mContentView;
- private View mClearAllView;
private boolean mTransitionedFromApp;
+ private AnimatorSet mLayoutAnimation;
+ private final ArraySet<View> mLayingOutViews = new ArraySet<>();
+ private final RecentsModel.TaskThumbnailChangeListener listener = (taskId, thumbnailData) -> {
+ ArrayList<TaskItemView> itemViews = getTaskViews();
+ for (int i = 0, size = itemViews.size(); i < size; i++) {
+ TaskItemView taskView = itemViews.get(i);
+ TaskHolder taskHolder = (TaskHolder) mTaskRecyclerView.getChildViewHolder(taskView);
+ Optional<Task> optTask = taskHolder.getTask();
+ if (optTask.filter(task -> task.key.id == taskId).isPresent()) {
+ Task task = optTask.get();
+ // Update thumbnail on the task.
+ task.thumbnail = thumbnailData;
+ taskView.setThumbnail(thumbnailData.thumbnail);
+ return task;
+ }
+ }
+ return null;
+ };
public IconRecentsView(Context context, AttributeSet attrs) {
super(context, attrs);
+ BaseActivity activity = BaseActivity.fromContext(context);
mContext = context;
mTaskLoader = new TaskListLoader(mContext);
mTaskAdapter = new TaskAdapter(mTaskLoader);
+ mTaskAdapter.setOnClearAllClickListener(view -> animateClearAllTasks());
mTaskActionController = new TaskActionController(mTaskLoader, mTaskAdapter);
mTaskAdapter.setActionController(mTaskActionController);
- mLayoutAnimation = createLayoutAnimation();
+ mTaskLayoutManager = new LinearLayoutManager(mContext, VERTICAL, true /* reverseLayout */);
+ RecentsModel.INSTANCE.get(context).addThumbnailChangeListener(listener);
}
@Override
@@ -115,15 +151,30 @@
if (mTaskRecyclerView == null) {
mTaskRecyclerView = findViewById(R.id.recent_task_recycler_view);
mTaskRecyclerView.setAdapter(mTaskAdapter);
- mTaskRecyclerView.setLayoutManager(
- new LinearLayoutManager(mContext, VERTICAL, true /* reverseLayout */));
+ mTaskRecyclerView.setLayoutManager(mTaskLayoutManager);
ItemTouchHelper helper = new ItemTouchHelper(
new TaskSwipeCallback(mTaskActionController));
helper.attachToRecyclerView(mTaskRecyclerView);
- mTaskRecyclerView.setLayoutAnimation(mLayoutAnimation);
+ mTaskRecyclerView.addOnChildAttachStateChangeListener(
+ new OnChildAttachStateChangeListener() {
+ @Override
+ public void onChildViewAttachedToWindow(@NonNull View view) {
+ if (mLayoutAnimation != null && !mLayingOutViews.contains(view)) {
+ // Child view was added that is not part of current layout animation
+ // so restart the animation.
+ animateFadeInLayoutAnimation();
+ }
+ }
+
+ @Override
+ public void onChildViewDetachedFromWindow(@NonNull View view) { }
+ });
+ mTaskRecyclerView.setItemAnimator(mDefaultItemAnimator);
+ mLoadingContentItemAnimator.setOnAnimationFinishedRunnable(
+ () -> mTaskRecyclerView.setItemAnimator(new DefaultItemAnimator()));
mEmptyView = findViewById(R.id.recent_task_empty_view);
- mContentView = findViewById(R.id.recent_task_content_view);
+ mContentView = mTaskRecyclerView;
mTaskAdapter.registerAdapterDataObserver(new AdapterDataObserver() {
@Override
public void onChanged() {
@@ -135,19 +186,17 @@
updateContentViewVisibility();
}
});
- mClearAllView = findViewById(R.id.clear_all_button);
- mClearAllView.setOnClickListener(v -> animateClearAllTasks());
+ // TODO: Move layout param logic into onMeasure
}
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
- TaskItemView[] itemViews = getTaskViews();
- for (TaskItemView itemView : itemViews) {
- itemView.setEnabled(enabled);
+ int childCount = mTaskRecyclerView.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ mTaskRecyclerView.getChildAt(i).setEnabled(enabled);
}
- mClearAllView.setEnabled(enabled);
}
/**
@@ -165,8 +214,13 @@
* becomes visible.
*/
public void onBeginTransitionToOverview() {
- mTaskRecyclerView.scheduleLayoutAnimation();
-
+ if (mContext.getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) {
+ // Scroll to bottom of task in landscape mode. This is a non-issue in portrait mode as
+ // all tasks should be visible to fill up the screen in portrait mode and the view will
+ // not be scrollable.
+ mTaskLayoutManager.scrollToPositionWithOffset(TASKS_START_POSITION, 0 /* offset */);
+ }
+ scheduleFadeInLayoutAnimation();
// Load any task changes
if (!mTaskLoader.needsToLoad()) {
return;
@@ -174,9 +228,24 @@
mTaskAdapter.setIsShowingLoadingUi(true);
mTaskAdapter.notifyDataSetChanged();
mTaskLoader.loadTaskList(tasks -> {
+ int numEmptyItems = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
mTaskAdapter.setIsShowingLoadingUi(false);
- // TODO: Animate the loading UI out and the loaded data in.
- mTaskAdapter.notifyDataSetChanged();
+ int numActualItems = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
+ if (numEmptyItems < numActualItems) {
+ throw new IllegalStateException("There are less empty item views than the number "
+ + "of items to animate to.");
+ }
+ // Possible that task list loads faster than adapter changes propagate to layout so
+ // only start content fill animation if there aren't any pending adapter changes.
+ if (!mTaskRecyclerView.hasPendingAdapterUpdates()) {
+ // Set item animator for content filling animation. The item animator will switch
+ // back to the default on completion
+ mTaskRecyclerView.setItemAnimator(mLoadingContentItemAnimator);
+ }
+ mTaskAdapter.notifyItemRangeRemoved(TASKS_START_POSITION + numActualItems,
+ numEmptyItems - numActualItems);
+ mTaskAdapter.notifyItemRangeChanged(TASKS_START_POSITION, numActualItems,
+ CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT);
});
}
@@ -194,35 +263,37 @@
* the app. In that case, we launch the next most recent.
*/
public void handleOverviewCommand() {
- int childCount = mTaskRecyclerView.getChildCount();
- if (childCount == 0) {
+ // TODO(130735711): Need to address case where most recent task is off screen/unattached.
+ ArrayList<TaskItemView> taskViews = getTaskViews();
+ int taskViewsSize = taskViews.size();
+ if (taskViewsSize <= 1) {
// Do nothing
return;
}
TaskHolder taskToLaunch;
- if (mTransitionedFromApp && childCount > 1) {
+ if (mTransitionedFromApp && taskViewsSize > 1) {
// Launch the next most recent app
- TaskItemView itemView = (TaskItemView) mTaskRecyclerView.getChildAt(1);
+ TaskItemView itemView = taskViews.get(1);
taskToLaunch = (TaskHolder) mTaskRecyclerView.getChildViewHolder(itemView);
} else {
// Launch the most recent app
- TaskItemView itemView = (TaskItemView) mTaskRecyclerView.getChildAt(0);
+ TaskItemView itemView = taskViews.get(0);
taskToLaunch = (TaskHolder) mTaskRecyclerView.getChildViewHolder(itemView);
}
mTaskActionController.launchTask(taskToLaunch);
}
/**
- * Get the thumbnail view associated with a task for the purposes of animation.
+ * Get the bottom most thumbnail view to animate to.
*
- * @param taskId task id of thumbnail view to get
- * @return the thumbnail view for the task if attached, null otherwise
+ * @return the thumbnail view if laid out
*/
- public @Nullable View getThumbnailViewForTask(int taskId) {
- TaskItemView view = mTaskAdapter.getTaskItemView(taskId);
- if (view == null) {
+ public @Nullable View getBottomThumbnailView() {
+ ArrayList<TaskItemView> taskViews = getTaskViews();
+ if (taskViews.isEmpty()) {
return null;
}
+ TaskItemView view = taskViews.get(0);
return view.getThumbnailView();
}
@@ -231,13 +302,14 @@
*/
private void animateClearAllTasks() {
setEnabled(false);
- TaskItemView[] itemViews = getTaskViews();
+ ArrayList<TaskItemView> itemViews = getTaskViews();
AnimatorSet clearAnim = new AnimatorSet();
long currentDelay = 0;
// Animate each item view to the right and fade out.
- for (TaskItemView itemView : itemViews) {
+ for (int i = 0, size = itemViews.size(); i < size; i++) {
+ TaskItemView itemView = itemViews.get(i);
PropertyValuesHolder transXproperty = PropertyValuesHolder.ofFloat(TRANSLATION_X,
0, itemView.getWidth() * ITEM_ANIMATE_OUT_TRANSLATION_X_RATIO);
PropertyValuesHolder alphaProperty = PropertyValuesHolder.ofFloat(ALPHA, 1.0f, 0f);
@@ -272,7 +344,8 @@
clearAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
- for (TaskItemView itemView : itemViews) {
+ for (int i = 0, size = itemViews.size(); i < size; i++) {
+ TaskItemView itemView = itemViews.get(i);
itemView.setTranslationX(0);
itemView.setAlpha(1.0f);
}
@@ -287,13 +360,16 @@
/**
* Get attached task item views ordered by most recent.
*
- * @return array of attached task item views
+ * @return array list of attached task item views
*/
- private TaskItemView[] getTaskViews() {
+ private ArrayList<TaskItemView> getTaskViews() {
int taskCount = mTaskRecyclerView.getChildCount();
- TaskItemView[] itemViews = new TaskItemView[taskCount];
+ ArrayList<TaskItemView> itemViews = new ArrayList<>();
for (int i = 0; i < taskCount; i ++) {
- itemViews[i] = (TaskItemView) mTaskRecyclerView.getChildAt(i);
+ View child = mTaskRecyclerView.getChildAt(i);
+ if (child instanceof TaskItemView) {
+ itemViews.add((TaskItemView) child);
+ }
}
return itemViews;
}
@@ -303,12 +379,14 @@
* of tasks.
*/
private void updateContentViewVisibility() {
- int taskListSize = mTaskLoader.getCurrentTaskList().size();
- if (mEmptyView.getVisibility() != VISIBLE && taskListSize == 0) {
+ int taskListSize = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
+ if (mShowingContentView != mEmptyView && taskListSize == 0) {
+ mShowingContentView = mEmptyView;
crossfadeViews(mEmptyView, mContentView);
mActivityHelper.leaveRecents();
}
- if (mContentView.getVisibility() != VISIBLE && taskListSize > 0) {
+ if (mShowingContentView != mContentView && taskListSize > 0) {
+ mShowingContentView = mContentView;
crossfadeViews(mContentView, mEmptyView);
}
}
@@ -320,6 +398,7 @@
* @param fadeOutView view that should fade out
*/
private void crossfadeViews(View fadeInView, View fadeOutView) {
+ fadeInView.animate().cancel();
fadeInView.setVisibility(VISIBLE);
fadeInView.setAlpha(0f);
fadeInView.animate()
@@ -327,6 +406,7 @@
.setDuration(CROSSFADE_DURATION)
.setListener(null);
+ fadeOutView.animate().cancel();
fadeOutView.animate()
.alpha(0f)
.setDuration(CROSSFADE_DURATION)
@@ -338,17 +418,56 @@
});
}
- private static LayoutAnimationController createLayoutAnimation() {
- AnimationSet anim = new AnimationSet(false /* shareInterpolator */);
+ /**
+ * Schedule a one-shot layout animation on the next layout. Separate from
+ * {@link #scheduleLayoutAnimation()} as the animation is {@link Animator} based and acts on the
+ * view properties themselves, allowing more controllable behavior and making it easier to
+ * manage when the animation conflicts with another animation.
+ */
+ private void scheduleFadeInLayoutAnimation() {
+ mTaskRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ animateFadeInLayoutAnimation();
+ mTaskRecyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ }
+ });
+ }
- Animation alphaAnim = new AlphaAnimation(0, 1);
- alphaAnim.setDuration(LAYOUT_ITEM_ANIMATE_IN_DURATION);
- anim.addAnimation(alphaAnim);
-
- LayoutAnimationController layoutAnim = new LayoutAnimationController(anim);
- layoutAnim.setDelay(
- (float) LAYOUT_ITEM_ANIMATE_IN_DELAY_BETWEEN / LAYOUT_ITEM_ANIMATE_IN_DURATION);
-
- return layoutAnim;
+ /**
+ * Start animating the layout animation where items fade in.
+ */
+ private void animateFadeInLayoutAnimation() {
+ if (mLayoutAnimation != null) {
+ // If layout animation still in progress, cancel and restart.
+ mLayoutAnimation.cancel();
+ }
+ ArrayList<TaskItemView> views = getTaskViews();
+ int delay = 0;
+ mLayoutAnimation = new AnimatorSet();
+ for (int i = 0, size = views.size(); i < size; i++) {
+ TaskItemView view = views.get(i);
+ view.setAlpha(0.0f);
+ Animator alphaAnim = ObjectAnimator.ofFloat(view, ALPHA, 0.0f, 1.0f);
+ alphaAnim.setDuration(LAYOUT_ITEM_ANIMATE_IN_DURATION).setStartDelay(delay);
+ alphaAnim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ view.setAlpha(1.0f);
+ mLayingOutViews.remove(view);
+ }
+ });
+ delay += LAYOUT_ITEM_ANIMATE_IN_DELAY_BETWEEN;
+ mLayoutAnimation.play(alphaAnim);
+ mLayingOutViews.add(view);
+ }
+ mLayoutAnimation.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mLayoutAnimation = null;
+ }
+ });
+ mLayoutAnimation.start();
}
}
diff --git a/go/quickstep/src/com/android/quickstep/views/TaskItemView.java b/go/quickstep/src/com/android/quickstep/views/TaskItemView.java
index d831b20..7d9916e 100644
--- a/go/quickstep/src/com/android/quickstep/views/TaskItemView.java
+++ b/go/quickstep/src/com/android/quickstep/views/TaskItemView.java
@@ -15,16 +15,21 @@
*/
package com.android.quickstep.views;
+import static com.android.quickstep.views.TaskLayoutUtils.getTaskHeight;
+
import android.content.Context;
+import android.content.res.Resources;
import android.graphics.Bitmap;
-import android.graphics.Color;
+import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
+import android.util.FloatProperty;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.R;
@@ -34,16 +39,41 @@
*/
public final class TaskItemView extends LinearLayout {
+ private static final String EMPTY_LABEL = "";
private static final String DEFAULT_LABEL = "...";
private final Drawable mDefaultIcon;
+ private final Drawable mDefaultThumbnail;
+ private final TaskLayerDrawable mIconDrawable;
+ private final TaskLayerDrawable mThumbnailDrawable;
private TextView mLabelView;
private ImageView mIconView;
private ImageView mThumbnailView;
+ private float mContentTransitionProgress;
+
+ /**
+ * Property representing the content transition progress of the view. 1.0f represents that the
+ * currently bound icon, thumbnail, and label are fully animated in and visible.
+ */
+ public static FloatProperty CONTENT_TRANSITION_PROGRESS =
+ new FloatProperty<TaskItemView>("taskContentTransitionProgress") {
+ @Override
+ public void setValue(TaskItemView view, float progress) {
+ view.setContentTransitionProgress(progress);
+ }
+
+ @Override
+ public Float get(TaskItemView view) {
+ return view.mContentTransitionProgress;
+ }
+ };
public TaskItemView(Context context, AttributeSet attrs) {
super(context, attrs);
- mDefaultIcon = context.getResources().getDrawable(
- android.R.drawable.sym_def_app_icon, context.getTheme());
+ Resources res = context.getResources();
+ mDefaultIcon = res.getDrawable(android.R.drawable.sym_def_app_icon, context.getTheme());
+ mDefaultThumbnail = res.getDrawable(R.drawable.default_thumbnail, context.getTheme());
+ mIconDrawable = new TaskLayerDrawable(context);
+ mThumbnailDrawable = new TaskLayerDrawable(context);
}
@Override
@@ -52,15 +82,28 @@
mLabelView = findViewById(R.id.task_label);
mThumbnailView = findViewById(R.id.task_thumbnail);
mIconView = findViewById(R.id.task_icon);
+
+ mThumbnailView.setImageDrawable(mThumbnailDrawable);
+ mIconView.setImageDrawable(mIconDrawable);
+
+ resetToEmptyUi();
+ CONTENT_TRANSITION_PROGRESS.setValue(this, 1.0f);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int taskHeight = getTaskHeight(getContext());
+ int newHeightSpec = MeasureSpec.makeMeasureSpec(taskHeight,MeasureSpec.EXACTLY);
+ super.onMeasure(widthMeasureSpec, newHeightSpec);
}
/**
- * Resets task item view to default values.
+ * Resets task item view to empty, loading UI.
*/
- public void resetTaskItemView() {
- setLabel(DEFAULT_LABEL);
- setIcon(null);
- setThumbnail(null);
+ public void resetToEmptyUi() {
+ mIconDrawable.resetDrawable();
+ mThumbnailDrawable.resetDrawable();
+ setLabel(EMPTY_LABEL);
}
/**
@@ -69,11 +112,8 @@
* @param label task label
*/
public void setLabel(@Nullable String label) {
- if (label == null) {
- mLabelView.setText(DEFAULT_LABEL);
- return;
- }
- mLabelView.setText(label);
+ mLabelView.setText(getSafeLabel(label));
+ // TODO: Animation for label
}
/**
@@ -86,11 +126,7 @@
// The icon proper is actually smaller than the drawable and has "padding" on the side for
// the purpose of drawing the shadow, allowing the icon to pop up, so we need to scale the
// view if we want the icon to be flush with the bottom of the thumbnail.
- if (icon == null) {
- mIconView.setImageDrawable(mDefaultIcon);
- return;
- }
- mIconView.setImageDrawable(icon);
+ mIconDrawable.setCurrentDrawable(getSafeIcon(icon));
}
/**
@@ -99,16 +135,48 @@
* @param thumbnail task thumbnail for the task
*/
public void setThumbnail(@Nullable Bitmap thumbnail) {
- if (thumbnail == null) {
- mThumbnailView.setImageBitmap(null);
- mThumbnailView.setBackgroundColor(Color.GRAY);
- return;
- }
- mThumbnailView.setBackgroundColor(Color.TRANSPARENT);
- mThumbnailView.setImageBitmap(thumbnail);
+ mThumbnailDrawable.setCurrentDrawable(getSafeThumbnail(thumbnail));
}
public View getThumbnailView() {
return mThumbnailView;
}
+
+ /**
+ * Start a new animation from the current task content to the specified new content. The caller
+ * is responsible for the actual animation control via the property
+ * {@link #CONTENT_TRANSITION_PROGRESS}.
+ *
+ * @param endIcon the icon to animate to
+ * @param endThumbnail the thumbnail to animate to
+ * @param endLabel the label to animate to
+ */
+ public void startContentAnimation(@Nullable Drawable endIcon, @Nullable Bitmap endThumbnail,
+ @Nullable String endLabel) {
+ mIconDrawable.startNewTransition(getSafeIcon(endIcon));
+ mThumbnailDrawable.startNewTransition(getSafeThumbnail(endThumbnail));
+ // TODO: Animation for label
+
+ setContentTransitionProgress(0.0f);
+ }
+
+ private void setContentTransitionProgress(float progress) {
+ mContentTransitionProgress = progress;
+ mIconDrawable.setTransitionProgress(progress);
+ mThumbnailDrawable.setTransitionProgress(progress);
+ // TODO: Animation for label
+ }
+
+ private @NonNull Drawable getSafeIcon(@Nullable Drawable icon) {
+ return (icon != null) ? icon : mDefaultIcon;
+ }
+
+ private @NonNull Drawable getSafeThumbnail(@Nullable Bitmap thumbnail) {
+ return (thumbnail != null) ? new BitmapDrawable(getResources(), thumbnail)
+ : mDefaultThumbnail;
+ }
+
+ private @NonNull String getSafeLabel(@Nullable String label) {
+ return (label != null) ? label : DEFAULT_LABEL;
+ }
}
diff --git a/go/quickstep/src/com/android/quickstep/views/TaskLayerDrawable.java b/go/quickstep/src/com/android/quickstep/views/TaskLayerDrawable.java
new file mode 100644
index 0000000..98b66b9
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/views/TaskLayerDrawable.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2019 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.views;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.R;
+
+/**
+ * A layer drawable for task content that transitions between two drawables by crossfading. Similar
+ * to {@link android.graphics.drawable.TransitionDrawable} but allows callers to control transition
+ * progress and provides a default, empty drawable.
+ */
+public final class TaskLayerDrawable extends LayerDrawable {
+ private final Drawable mEmptyDrawable;
+ private float mProgress;
+
+ public TaskLayerDrawable(Context context) {
+ super(new Drawable[0]);
+
+ // Use empty drawable for both layers initially.
+ mEmptyDrawable = context.getResources().getDrawable(
+ R.drawable.empty_content_box, context.getTheme());
+ addLayer(mEmptyDrawable);
+ addLayer(mEmptyDrawable);
+ setTransitionProgress(1.0f);
+ }
+
+ /**
+ * Immediately set the front-most drawable layer.
+ *
+ * @param drawable drawable to set
+ */
+ public void setCurrentDrawable(@NonNull Drawable drawable) {
+ setDrawable(0, drawable);
+ applyTransitionProgress(mProgress);
+ }
+
+ /**
+ * Immediately reset the drawable to showing the empty drawable.
+ */
+ public void resetDrawable() {
+ setCurrentDrawable(mEmptyDrawable);
+ }
+
+ /**
+ * Prepare to start animating the transition by pushing the current drawable to the back and
+ * setting a new drawable to the front layer and making it invisible.
+ *
+ * @param endDrawable drawable to animate to
+ */
+ public void startNewTransition(@NonNull Drawable endDrawable) {
+ Drawable oldDrawable = getDrawable(0);
+ setDrawable(1, oldDrawable);
+ setDrawable(0, endDrawable);
+ setTransitionProgress(0.0f);
+ }
+
+ /**
+ * Set the progress of the transition animation to crossfade the two drawables.
+ *
+ * @param progress current transition progress between 0 (front view invisible) and 1
+ * (front view visible)
+ */
+ public void setTransitionProgress(float progress) {
+ if (progress > 1 || progress < 0) {
+ throw new IllegalArgumentException("Transition progress should be between 0 and 1");
+ }
+ mProgress = progress;
+ applyTransitionProgress(progress);
+ }
+
+ private void applyTransitionProgress(float progress) {
+ int drawableAlpha = (int) (progress * 255);
+ getDrawable(0).setAlpha(drawableAlpha);
+ if (getDrawable(0) != getDrawable(1)) {
+ // Only do this if it's a different drawable so that it fades out.
+ // Otherwise, we'd just be overwriting the front drawable's alpha.
+ getDrawable(1).setAlpha(255 - drawableAlpha);
+ }
+ invalidateSelf();
+ }
+}
diff --git a/go/quickstep/src/com/android/quickstep/views/TaskLayoutUtils.java b/go/quickstep/src/com/android/quickstep/views/TaskLayoutUtils.java
new file mode 100644
index 0000000..e28a9e0
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/views/TaskLayoutUtils.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2019 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.views;
+
+import static com.android.quickstep.TaskAdapter.MAX_TASKS_TO_DISPLAY;
+
+import android.content.Context;
+
+import com.android.launcher3.InvariantDeviceProfile;
+
+/**
+ * Utils to determine dynamically task and view sizes based off the device height and width.
+ */
+public final class TaskLayoutUtils {
+
+ private static final float CLEAR_ALL_ITEM_TO_HEIGHT_RATIO = 7.0f / 64;
+
+ private TaskLayoutUtils() {}
+
+ /**
+ * Calculate task height based off the available height in portrait mode such that when the
+ * recents list is full, the total height fills in the available device height perfectly. In
+ * landscape mode, we keep the same task height so that tasks scroll off the top.
+ *
+ * @param context current context
+ * @return task height
+ */
+ public static int getTaskHeight(Context context) {
+ final int availableHeight =
+ InvariantDeviceProfile.INSTANCE.get(context).portraitProfile.availableHeightPx;
+ final int availableTaskSpace = availableHeight - getClearAllItemHeight(context);
+ return (int) (availableTaskSpace * 1.0f / MAX_TASKS_TO_DISPLAY);
+ }
+
+ /**
+ * Calculate clear all item height scaled to available height in portrait mode.
+ *
+ * @param context current context
+ * @return clear all item height
+ */
+ public static int getClearAllItemHeight(Context context) {
+ final int availableHeight =
+ InvariantDeviceProfile.INSTANCE.get(context).portraitProfile.availableHeightPx;
+ return (int) (CLEAR_ALL_ITEM_TO_HEIGHT_RATIO * availableHeight);
+ }
+}
diff --git a/go/quickstep/src/com/android/quickstep/views/TaskThumbnailIconView.java b/go/quickstep/src/com/android/quickstep/views/TaskThumbnailIconView.java
new file mode 100644
index 0000000..b1c60dd
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/views/TaskThumbnailIconView.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2019 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.views;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.launcher3.R;
+
+/**
+ * Square view that holds thumbnail and icon and shrinks them appropriately so that both fit nicely
+ * within the view. Side length is determined by height.
+ */
+final class TaskThumbnailIconView extends ViewGroup {
+ private final Rect mTmpFrameRect = new Rect();
+ private final Rect mTmpChildRect = new Rect();
+ private View mThumbnailView;
+ private View mIconView;
+ private static final float SUBITEM_FRAME_RATIO = .6f;
+
+ public TaskThumbnailIconView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mThumbnailView = findViewById(R.id.task_thumbnail);
+ mIconView = findViewById(R.id.task_icon);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int height = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec);
+ int width = height;
+ setMeasuredDimension(width, height);
+
+ int subItemSize = (int) (SUBITEM_FRAME_RATIO * height);
+ if (mThumbnailView.getVisibility() != GONE) {
+ int thumbnailHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+ int thumbnailWidthSpec = MeasureSpec.makeMeasureSpec(subItemSize, MeasureSpec.EXACTLY);
+ measureChild(mThumbnailView, thumbnailWidthSpec, thumbnailHeightSpec);
+ }
+ if (mIconView.getVisibility() != GONE) {
+ int iconHeightSpec = MeasureSpec.makeMeasureSpec(subItemSize, MeasureSpec.EXACTLY);
+ int iconWidthSpec = MeasureSpec.makeMeasureSpec(subItemSize, MeasureSpec.EXACTLY);
+ measureChild(mIconView, iconWidthSpec, iconHeightSpec);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ mTmpFrameRect.left = getPaddingLeft();
+ mTmpFrameRect.right = right - left - getPaddingRight();
+ mTmpFrameRect.top = getPaddingTop();
+ mTmpFrameRect.bottom = bottom - top - getPaddingBottom();
+
+ // Layout the thumbnail to the top-start corner of the view
+ if (mThumbnailView.getVisibility() != GONE) {
+ final int width = mThumbnailView.getMeasuredWidth();
+ final int height = mThumbnailView.getMeasuredHeight();
+
+ final int thumbnailGravity = Gravity.TOP | Gravity.START;
+ Gravity.apply(thumbnailGravity, width, height, mTmpFrameRect, mTmpChildRect);
+
+ mThumbnailView.layout(mTmpChildRect.left, mTmpChildRect.top,
+ mTmpChildRect.right, mTmpChildRect.bottom);
+ }
+
+ // Layout the icon to the bottom-end corner of the view
+ if (mIconView.getVisibility() != GONE) {
+ final int width = mIconView.getMeasuredWidth();
+ final int height = mIconView.getMeasuredHeight();
+
+ int thumbnailGravity = Gravity.BOTTOM | Gravity.END;
+ Gravity.apply(thumbnailGravity, width, height, mTmpFrameRect, mTmpChildRect);
+
+ mIconView.layout(mTmpChildRect.left, mTmpChildRect.top,
+ mTmpChildRect.right, mTmpChildRect.bottom);
+ }
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/AssistantTouchConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/AssistantTouchConsumer.java
index e865137..be87a96 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/AssistantTouchConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/AssistantTouchConsumer.java
@@ -61,6 +61,9 @@
private static final int STATE_ASSISTANT_ACTIVE = 1;
private static final int STATE_DELEGATE_ACTIVE = 2;
+ private static final String INVOCATION_TYPE_KEY = "invocation_type";
+ private static final int INVOCATION_TYPE_GESTURE = 1;
+
private final PointF mDownPos = new PointF();
private final PointF mLastPos = new PointF();
private final PointF mStartDragPos = new PointF();
@@ -238,7 +241,9 @@
if (mDistance >= mDistThreshold && mTimeFraction >= 1) {
UserEventDispatcher.newInstance(mContext).logActionOnContainer(
SWIPE, mDirection, NAVBAR);
- mSysUiProxy.startAssistant(new Bundle());
+ Bundle args = new Bundle();
+ args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_GESTURE);
+ mSysUiProxy.startAssistant(args);
mLaunchedAssistant = true;
}
} catch (RemoteException e) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
index 7c71846..3b2d66d 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
@@ -97,7 +97,7 @@
public void onAssistantVisibilityChanged(float visibility) {
Launcher launcher = getCreatedActivity();
if (launcher != null) {
- launcher.setQuickSearchBarAlpha(1f - visibility);
+ launcher.onAssistantVisibilityChanged(visibility);
}
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
index b263a4c..4f50cdb 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
@@ -87,7 +87,7 @@
OverviewInteractionState.INSTANCE.get(launcher)
.setBackButtonAlpha(shouldBackButtonBeHidden ? 0 : 1, true /* animate */);
if (launcher != null && launcher.getDragLayer() != null) {
- launcher.getDragLayer().setDisallowBackGesture(shouldBackButtonBeHidden);
+ launcher.getRootView().setDisallowBackGesture(shouldBackButtonBeHidden);
}
}
diff --git a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
index dc83e87..a47c8e7 100644
--- a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
+++ b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
@@ -61,7 +61,7 @@
}
@Test
- @Ignore // Ignoring until gestural navigation event sequence settles
+ @Ignore // Ignoring until race condition repro framework is changes for multi-process case.
@NavigationModeSwitch(mode = Mode.TWO_BUTTON)
public void testPressHome() {
runTest(enterEvt(Launcher.ON_CREATE_EVT),
@@ -76,14 +76,6 @@
}
@Test
- @Ignore // Ignoring until gestural navigation event sequence settles
- @NavigationModeSwitch(mode = Mode.TWO_BUTTON)
- public void testSwipeToOverview() {
- closeLauncherActivity();
- mLauncher.getBackground().switchToOverview();
- }
-
- @Test
@NavigationModeSwitch
public void testStressPressHome() {
for (int i = 0; i < STRESS_REPEAT_COUNT; ++i) {
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 5e50a43..b0ce5f5 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -74,7 +74,7 @@
@Test
@PortraitLandscape
- @Ignore
+ @Ignore // Enable after b/131115533
public void testPressRecentAppsLauncherAndGetOverview() throws RemoteException {
mDevice.pressRecentApps();
waitForState("Launcher internal state didn't switch to Overview", LauncherState.OVERVIEW);
diff --git a/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
index e046cad..42848f4 100644
--- a/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
@@ -41,7 +41,7 @@
}
@Test
- @Ignore("This test fails with resource errors")
+ @Ignore("This test fails with resource errors") // b/131115553
public void testCacheUpdate_update_apps() throws Exception {
// Clear all icons from apps list so that its easy to check what was updated
for (AppInfo info : allAppsList.data) {
@@ -66,7 +66,7 @@
}
@Test
- @Ignore("This test fails with resource errors")
+ @Ignore("This test fails with resource errors") // b/131115553
public void testSessionUpdate_ignores_normal_apps() throws Exception {
executeTaskForTest(newTask(CacheDataUpdatedTask.OP_SESSION_UPDATE, "app1"));
@@ -75,7 +75,7 @@
}
@Test
- @Ignore("This test fails with resource errors")
+ @Ignore("This test fails with resource errors") // b/131115553
public void testSessionUpdate_updates_pending_apps() throws Exception {
executeTaskForTest(newTask(CacheDataUpdatedTask.OP_SESSION_UPDATE, "app3"));
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 0c35566..fff8f02 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -24,6 +24,8 @@
import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
import static com.android.launcher3.LauncherState.ALL_APPS;
import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.LauncherState.OVERVIEW_PEEK;
import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_LAUNCHER_LOAD;
import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
import static com.android.launcher3.logging.LoggerUtils.newTarget;
@@ -128,6 +130,7 @@
import com.android.launcher3.util.ViewOnDrawExecutor;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.OptionsPopupView;
+import com.android.launcher3.views.ScrimView;
import com.android.launcher3.widget.LauncherAppWidgetHostView;
import com.android.launcher3.widget.PendingAddShortcutInfo;
import com.android.launcher3.widget.PendingAddWidgetInfo;
@@ -147,6 +150,7 @@
import java.util.List;
import java.util.function.Predicate;
+import androidx.annotation.IdRes;
import androidx.annotation.Nullable;
/**
@@ -202,6 +206,9 @@
private static final int NEW_APPS_ANIMATION_INACTIVE_TIMEOUT_SECONDS = 5;
@Thunk static final int NEW_APPS_ANIMATION_DELAY = 500;
+ private static final int APPS_VIEW_ALPHA_CHANNEL_INDEX = 1;
+ private static final int SCRIM_VIEW_ALPHA_CHANNEL_INDEX = 0;
+
private LauncherAppTransitionManager mAppTransitionManager;
private Configuration mOldConfig;
@@ -223,6 +230,9 @@
@Thunk AllAppsContainerView mAppsView;
AllAppsTransitionController mAllAppsController;
+ // Scrim view for the all apps and overview state.
+ @Thunk ScrimView mScrimView;
+
// UI and state for the overview panel
private View mOverviewPanel;
@@ -261,6 +271,8 @@
final Handler mHandler = new Handler();
private final Runnable mHandleDeferredResume = this::handleDeferredResume;
+ private float mCurrentAssistantVisibility = 0f;
+
@Override
protected void onCreate(Bundle savedInstanceState) {
RaceConditionTracker.onEvent(ON_CREATE_EVT, ENTER);
@@ -361,6 +373,24 @@
TraceHelper.endSection("Launcher-onCreate");
RaceConditionTracker.onEvent(ON_CREATE_EVT, EXIT);
+ mStateManager.addStateListener(new LauncherStateManager.StateListener() {
+ @Override
+ public void onStateTransitionStart(LauncherState toState) {}
+
+ @Override
+ public void onStateTransitionComplete(LauncherState finalState) {
+ float alpha = 1f - mCurrentAssistantVisibility;
+ if (finalState == NORMAL) {
+ mAppsView.getAlphaProperty(APPS_VIEW_ALPHA_CHANNEL_INDEX).setValue(alpha);
+ } else if (finalState == OVERVIEW || finalState == OVERVIEW_PEEK) {
+ mAppsView.getAlphaProperty(APPS_VIEW_ALPHA_CHANNEL_INDEX).setValue(alpha);
+ mScrimView.getAlphaProperty(SCRIM_VIEW_ALPHA_CHANNEL_INDEX).setValue(alpha);
+ } else {
+ mAppsView.getAlphaProperty(APPS_VIEW_ALPHA_CHANNEL_INDEX).setValue(1f);
+ mScrimView.getAlphaProperty(SCRIM_VIEW_ALPHA_CHANNEL_INDEX).setValue(1f);
+ }
+ }
+ });
}
@Override
@@ -407,13 +437,6 @@
onIdpChanged(idp);
}
- public void setQuickSearchBarAlpha(float alpha) {
- View qsbAllApps = findViewById(R.id.search_container_all_apps);
- if (qsbAllApps != null) {
- qsbAllApps.setAlpha(alpha);
- }
- }
-
private void onIdpChanged(InvariantDeviceProfile idp) {
mUserEventDispatcher = null;
@@ -426,6 +449,18 @@
rebindModel();
}
+ public void onAssistantVisibilityChanged(float visibility) {
+ mCurrentAssistantVisibility = visibility;
+ float alpha = 1f - visibility;
+ LauncherState state = mStateManager.getState();
+ if (state == NORMAL) {
+ mAppsView.getAlphaProperty(APPS_VIEW_ALPHA_CHANNEL_INDEX).setValue(alpha);
+ } else if (state == OVERVIEW || state == OVERVIEW_PEEK) {
+ mAppsView.getAlphaProperty(APPS_VIEW_ALPHA_CHANNEL_INDEX).setValue(alpha);
+ mScrimView.getAlphaProperty(SCRIM_VIEW_ALPHA_CHANNEL_INDEX).setValue(alpha);
+ }
+ }
+
private void initDeviceProfile(InvariantDeviceProfile idp) {
// Load configuration-specific DeviceProfile
mDeviceProfile = idp.getDeviceProfile(this);
@@ -968,6 +1003,9 @@
// Setup Apps
mAppsView = findViewById(R.id.apps_view);
+ // Setup Scrim
+ mScrimView = findViewById(R.id.scrim_view);
+
// Setup the drag controller (drop targets have to be added in reverse order in priority)
mDragController.setMoveTarget(mWorkspace);
mDropTargetBar.setup(mDragController);
@@ -1639,7 +1677,7 @@
public int getCurrentState() {
if(mStateManager.getState() == LauncherState.ALL_APPS) {
return StatsLogUtils.LAUNCHER_STATE_ALLAPPS;
- } else if (mStateManager.getState() == LauncherState.OVERVIEW) {
+ } else if (mStateManager.getState() == OVERVIEW) {
return StatsLogUtils.LAUNCHER_STATE_OVERVIEW;
}
return StatsLogUtils.LAUNCHER_STATE_HOME;
@@ -1657,7 +1695,7 @@
LauncherState state = mStateManager.getState();
if (state == LauncherState.ALL_APPS) {
event.srcTarget[2].containerType = ContainerType.ALLAPPS;
- } else if (state == LauncherState.OVERVIEW) {
+ } else if (state == OVERVIEW) {
event.srcTarget[2].containerType = ContainerType.TASKSWITCHER;
}
}
diff --git a/src/com/android/launcher3/LauncherRootView.java b/src/com/android/launcher3/LauncherRootView.java
index e738eb7..199220a 100644
--- a/src/com/android/launcher3/LauncherRootView.java
+++ b/src/com/android/launcher3/LauncherRootView.java
@@ -12,12 +12,16 @@
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
+import android.os.Build;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewDebug;
import android.view.WindowInsets;
+import java.util.Collections;
+import java.util.List;
+
public class LauncherRootView extends InsettableFrameLayout {
private final Launcher mLauncher;
@@ -30,8 +34,14 @@
@ViewDebug.ExportedProperty(category = "launcher")
private final RectF mTouchExcludeRegion = new RectF();
+ @ViewDebug.ExportedProperty(category = "launcher")
+ private static final List<Rect> SYSTEM_GESTURE_EXCLUSION_RECT =
+ Collections.singletonList(new Rect());
+
private View mAlignedView;
private WindowStateListener mWindowStateListener;
+ @ViewDebug.ExportedProperty(category = "launcher")
+ private boolean mDisallowBackGesture;
public LauncherRootView(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -177,6 +187,24 @@
return super.dispatchTouchEvent(ev);
}
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b);
+ SYSTEM_GESTURE_EXCLUSION_RECT.get(0).set(l, t, r, b);
+ setDisallowBackGesture(mDisallowBackGesture);
+ }
+
+ @TargetApi(Build.VERSION_CODES.Q)
+ public void setDisallowBackGesture(boolean disallowBackGesture) {
+ if (!Utilities.ATLEAST_Q) {
+ return;
+ }
+ mDisallowBackGesture = disallowBackGesture;
+ setSystemGestureExclusionRects(mDisallowBackGesture
+ ? SYSTEM_GESTURE_EXCLUSION_RECT
+ : Collections.emptyList());
+ }
+
public interface WindowStateListener {
void onWindowFocusChanged(boolean hasFocus);
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index 86b96b4..e2a5160 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -49,6 +49,8 @@
import com.android.launcher3.keyboard.FocusedItemDecorator;
import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
import com.android.launcher3.util.ItemInfoMatcher;
+import com.android.launcher3.util.MultiValueAlpha;
+import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
import com.android.launcher3.util.Themes;
import com.android.launcher3.views.BottomUserEducationView;
import com.android.launcher3.views.RecyclerViewFastScroller;
@@ -70,6 +72,7 @@
private static final float FLING_VELOCITY_MULTIPLIER = 135f;
// Starts the springs after at least 55% of the animation has passed.
private static final float FLING_ANIMATION_THRESHOLD = 0.55f;
+ private static final int ALPHA_CHANNEL_COUNT = 2;
private final Launcher mLauncher;
private final AdapterHolder[] mAH;
@@ -93,6 +96,8 @@
private RecyclerViewFastScroller mTouchHandler;
private final Point mFastScrollerOffset = new Point();
+ private final MultiValueAlpha mMultiValueAlpha;
+
public AllAppsContainerView(Context context) {
this(context, null);
}
@@ -122,12 +127,18 @@
addSpringView(R.id.all_apps_header);
addSpringView(R.id.apps_list_view);
addSpringView(R.id.all_apps_tabs_view_pager);
+
+ mMultiValueAlpha = new MultiValueAlpha(this, ALPHA_CHANNEL_COUNT);
}
public AllAppsStore getAppsStore() {
return mAllAppsStore;
}
+ public AlphaProperty getAlphaProperty(int index) {
+ return mMultiValueAlpha.getProperty(index);
+ }
+
@Override
protected void setDampedScrollShift(float shift) {
// Bound the shift amount to avoid content from drawing on top (Y-val) of the QSB.
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 4a1d432..a7f89d9 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -29,6 +29,7 @@
import com.android.launcher3.anim.AnimatorSetBuilder;
import com.android.launcher3.anim.SpringObjectAnimator;
import com.android.launcher3.anim.PropertySetter;
+import com.android.launcher3.util.MultiValueAlpha;
import com.android.launcher3.util.Themes;
import com.android.launcher3.views.ScrimView;
@@ -64,6 +65,8 @@
}
};
+ private static final int APPS_VIEW_ALPHA_CHANNEL_INDEX = 0;
+
private AllAppsContainerView mAppsView;
private ScrimView mScrimView;
@@ -102,7 +105,7 @@
setScrollRangeDelta(mScrollRangeDelta);
if (mIsVerticalLayout) {
- mAppsView.setAlpha(1);
+ mAppsView.getAlphaProperty(APPS_VIEW_ALPHA_CHANNEL_INDEX).setValue(1);
mLauncher.getHotseat().setTranslationY(0);
mLauncher.getWorkspace().getPageIndicator().setTranslationY(0);
}
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index 6cc49de..9f902ed 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -24,12 +24,10 @@
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
-import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Rect;
-import android.os.Build;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.MotionEvent;
@@ -45,7 +43,6 @@
import com.android.launcher3.Launcher;
import com.android.launcher3.R;
import com.android.launcher3.ShortcutAndWidgetContainer;
-import com.android.launcher3.Utilities;
import com.android.launcher3.Workspace;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.folder.Folder;
@@ -57,8 +54,6 @@
import com.android.launcher3.views.BaseDragLayer;
import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
/**
* A ViewGroup that coordinates dragging across its descendants
@@ -73,9 +68,6 @@
public static final int ANIMATION_END_DISAPPEAR = 0;
public static final int ANIMATION_END_REMAIN_VISIBLE = 2;
- private static final List<Rect> SYSTEM_GESTURE_EXCLUSION_RECT =
- Collections.singletonList(new Rect());
-
@Thunk DragController mDragController;
// Variables relating to animation of views after drop
@@ -94,8 +86,6 @@
private final ViewGroupFocusHelper mFocusIndicatorHelper;
private final WorkspaceAndHotseatScrim mScrim;
- private boolean mDisallowBackGesture;
-
/**
* Used to create a new DragLayer from XML.
*
@@ -562,24 +552,6 @@
mScrim.onInsetsChanged(insets);
}
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- super.onLayout(changed, l, t, r, b);
- SYSTEM_GESTURE_EXCLUSION_RECT.get(0).set(l, t, r, b);
- setDisallowBackGesture(mDisallowBackGesture);
- }
-
- @TargetApi(Build.VERSION_CODES.Q)
- public void setDisallowBackGesture(boolean disallowBackGesture) {
- if (!Utilities.ATLEAST_Q) {
- return;
- }
- mDisallowBackGesture = disallowBackGesture;
- setSystemGestureExclusionRects(mDisallowBackGesture
- ? SYSTEM_GESTURE_EXCLUSION_RECT
- : Collections.emptyList());
- }
-
public WorkspaceAndHotseatScrim getScrim() {
return mScrim;
}
diff --git a/src/com/android/launcher3/views/ScrimView.java b/src/com/android/launcher3/views/ScrimView.java
index a264f9b..c360117 100644
--- a/src/com/android/launcher3/views/ScrimView.java
+++ b/src/com/android/launcher3/views/ScrimView.java
@@ -59,6 +59,8 @@
import com.android.launcher3.uioverrides.WallpaperColorInfo.OnChangeListener;
import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
+import com.android.launcher3.util.MultiValueAlpha;
+import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
import com.android.launcher3.util.Themes;
import java.util.List;
@@ -92,6 +94,7 @@
private static final int WALLPAPERS = R.string.wallpaper_button_text;
private static final int WIDGETS = R.string.widget_button_text;
private static final int SETTINGS = R.string.settings_button_text;
+ private static final int ALPHA_CHANNEL_COUNT = 1;
private final Rect mTempRect = new Rect();
private final int[] mTempPos = new int[2];
@@ -115,6 +118,8 @@
private final Rect mDragHandleBounds;
private final RectF mHitRect = new RectF();
+ private final MultiValueAlpha mMultiValueAlpha;
+
private final AccessibilityHelper mAccessibilityHelper;
@Nullable
protected Drawable mDragHandle;
@@ -138,6 +143,11 @@
mAM = (AccessibilityManager) context.getSystemService(ACCESSIBILITY_SERVICE);
setFocusable(false);
+ mMultiValueAlpha = new MultiValueAlpha(this, ALPHA_CHANNEL_COUNT);
+ }
+
+ public AlphaProperty getAlphaProperty(int index) {
+ return mMultiValueAlpha.getProperty(index);
}
@NonNull
diff --git a/tests/src/com/android/launcher3/ui/DefaultLayoutProviderTest.java b/tests/src/com/android/launcher3/ui/DefaultLayoutProviderTest.java
index 33b6f61..357e029 100644
--- a/tests/src/com/android/launcher3/ui/DefaultLayoutProviderTest.java
+++ b/tests/src/com/android/launcher3/ui/DefaultLayoutProviderTest.java
@@ -71,7 +71,7 @@
}
@Test
- @Ignore
+ @Ignore // Convert test to TAPL and enable them; b/131116002
public void testCustomProfileLoaded_with_icon_on_hotseat() throws Exception {
writeLayout(new LauncherLayoutBuilder().atHotseat(0).putApp(SETTINGS_APP, SETTINGS_APP));
@@ -86,7 +86,7 @@
}
@Test
- @Ignore
+ @Ignore // Convert test to TAPL and enable them; b/131116002
public void testCustomProfileLoaded_with_widget() throws Exception {
// A non-restored widget with no config screen gets restored automatically.
LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false);
@@ -106,7 +106,7 @@
}
@Test
- @Ignore
+ @Ignore // Convert test to TAPL and enable them; b/131116002
public void testCustomProfileLoaded_with_folder() throws Exception {
writeLayout(new LauncherLayoutBuilder().atHotseat(0).putFolder(android.R.string.copy)
.addApp(SETTINGS_APP, SETTINGS_APP)
diff --git a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
index 80561fc..84452b4 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
@@ -71,25 +71,25 @@
}
@Test
- @Ignore
+ @Ignore // Convert test to TAPL and enable them; b/131116002
public void testWidgetConfig() throws Throwable {
runTest(false, true);
}
@Test
- @Ignore
+ @Ignore // Convert test to TAPL and enable them; b/131116002
public void testWidgetConfig_rotate() throws Throwable {
runTest(true, true);
}
@Test
- @Ignore
+ @Ignore // Convert test to TAPL and enable them; b/131116002
public void testConfigCancelled() throws Throwable {
runTest(false, false);
}
@Test
- @Ignore
+ @Ignore // Convert test to TAPL and enable them; b/131116002
public void testConfigCancelled_rotate() throws Throwable {
runTest(true, false);
}
diff --git a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
index 7d3cf2b..90c339f 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
@@ -49,14 +49,14 @@
@Rule public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
@Test
- @Ignore
+ @Ignore // Convert test to TAPL and enable them; b/131116002
public void testDragIcon_portrait() throws Throwable {
lockRotation(true);
performTest();
}
@Test
- @Ignore
+ @Ignore // Convert test to TAPL and enable them; b/131116002
public void testDragIcon_landscape() throws Throwable {
lockRotation(false);
performTest();
diff --git a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
index c8e7786..918e6e2 100644
--- a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
@@ -125,7 +125,7 @@
verifyWidgetPresent(info);
}
- @Test @Ignore
+ @Test @Ignore // b/131116593
public void testUnboundWidget_removed() {
LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false);
LauncherAppWidgetInfo item = createWidgetInfo(info, false);
@@ -145,7 +145,7 @@
assertFalse(mDevice.findObject(new UiSelector().description(info.label)).exists());
}
- @Test @Ignore
+ @Test @Ignore // b/131116593
public void testPendingWidget_autoRestored() {
// A non-restored widget with no config screen gets restored automatically.
LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false);
@@ -183,7 +183,7 @@
LauncherSettings.Favorites.APPWIDGET_ID))));
}
- @Test @Ignore
+ @Test @Ignore // b/131116593
public void testPendingWidget_notRestored_removed() {
LauncherAppWidgetInfo item = getInvalidWidgetInfo();
item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID
diff --git a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
index 4899334..65d8a82 100644
--- a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
@@ -80,7 +80,7 @@
@Test
public void testEmpty() throws Throwable { /* needed while the broken tests are being fixed */ }
- @Test @Ignore
+ @Test @Ignore // b/131116593
public void testPinWidgetNoConfig() throws Throwable {
runTest("pinWidgetNoConfig", true, new ItemOperator() {
@Override
@@ -93,7 +93,7 @@
});
}
- @Test @Ignore
+ @Test @Ignore // b/131116593
public void testPinWidgetNoConfig_customPreview() throws Throwable {
// Command to set custom preview
Intent command = RequestPinItemActivity.getCommandIntent(
@@ -111,7 +111,7 @@
}, command);
}
- @Test @Ignore
+ @Test @Ignore // b/131116593
public void testPinWidgetWithConfig() throws Throwable {
runTest("pinWidgetWithConfig", true, new ItemOperator() {
@Override
@@ -124,7 +124,7 @@
});
}
- @Test @Ignore
+ @Test @Ignore // b/131116593
public void testPinShortcut() throws Throwable {
// Command to set the shortcut id
Intent command = RequestPinItemActivity.getCommandIntent(