summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--go/quickstep/src/com/android/quickstep/ContentFillItemAnimator.java276
-rw-r--r--go/quickstep/src/com/android/quickstep/TaskAdapter.java33
-rw-r--r--go/quickstep/src/com/android/quickstep/views/IconRecentsView.java24
3 files changed, 323 insertions, 10 deletions
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 000000000..1b6f2e34d
--- /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/TaskAdapter.java b/go/quickstep/src/com/android/quickstep/TaskAdapter.java
index 02cbf4e01..5e0e8ff8b 100644
--- a/go/quickstep/src/com/android/quickstep/TaskAdapter.java
+++ b/go/quickstep/src/com/android/quickstep/TaskAdapter.java
@@ -34,6 +34,8 @@ import java.util.Objects;
*/
public final class TaskAdapter extends Adapter<TaskHolder> {
+ public static final int CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT = 0;
+
private static final int MAX_TASKS_TO_DISPLAY = 6;
private static final String TAG = "TaskAdapter";
private final TaskListLoader mLoader;
@@ -71,6 +73,28 @@ public final class TaskAdapter extends Adapter<TaskHolder> {
@Override
public void onBindViewHolder(TaskHolder holder, int position) {
+ onBindViewHolderInternal(holder, position, false /* willAnimate */);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull TaskHolder holder, int position,
+ @NonNull List<Object> payloads) {
+ if (payloads.isEmpty()) {
+ super.onBindViewHolder(holder, position, payloads);
+ 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);
+ }
+ }
+
+ private void onBindViewHolderInternal(@NonNull TaskHolder holder, int position,
+ boolean willAnimate) {
if (mIsShowingLoadingUi) {
holder.bindEmptyUi();
return;
@@ -81,7 +105,7 @@ public final class TaskAdapter extends Adapter<TaskHolder> {
return;
}
Task task = tasks.get(position);
- holder.bindTask(task, false /* willAnimate */);
+ holder.bindTask(task, willAnimate /* willAnimate */);
mLoader.loadTaskIconAndLabel(task, () -> {
// Ensure holder still has the same task.
if (Objects.equals(task, holder.getTask())) {
@@ -97,13 +121,6 @@ public final class TaskAdapter extends Adapter<TaskHolder> {
}
@Override
- public void onBindViewHolder(@NonNull TaskHolder holder, int position,
- @NonNull List<Object> payloads) {
- // TODO: Bind task in preparation for animation. For now, we apply UI changes immediately.
- super.onBindViewHolder(holder, position, payloads);
- }
-
- @Override
public int getItemCount() {
if (mIsShowingLoadingUi) {
// Show loading version of all items.
diff --git a/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java b/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
index 59755bcb3..41f25105c 100644
--- a/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
+++ b/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
@@ -17,6 +17,8 @@ package com.android.quickstep.views;
import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL;
+import static com.android.quickstep.TaskAdapter.CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT;
+
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
@@ -34,6 +36,7 @@ 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;
@@ -41,6 +44,7 @@ import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
import androidx.recyclerview.widget.RecyclerView.OnChildAttachStateChangeListener;
import com.android.launcher3.R;
+import com.android.quickstep.ContentFillItemAnimator;
import com.android.quickstep.RecentsToActivityHelper;
import com.android.quickstep.TaskActionController;
import com.android.quickstep.TaskAdapter;
@@ -89,6 +93,9 @@ public final class IconRecentsView extends FrameLayout {
private final TaskListLoader mTaskLoader;
private final TaskAdapter mTaskAdapter;
private final TaskActionController mTaskActionController;
+ private final DefaultItemAnimator mDefaultItemAnimator = new DefaultItemAnimator();
+ private final ContentFillItemAnimator mLoadingContentItemAnimator =
+ new ContentFillItemAnimator();
private RecentsToActivityHelper mActivityHelper;
private RecyclerView mTaskRecyclerView;
@@ -134,6 +141,9 @@ public final class IconRecentsView extends FrameLayout {
@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);
@@ -186,9 +196,19 @@ public final class IconRecentsView extends FrameLayout {
mTaskAdapter.setIsShowingLoadingUi(true);
mTaskAdapter.notifyDataSetChanged();
mTaskLoader.loadTaskList(tasks -> {
+ int numEmptyItems = mTaskAdapter.getItemCount();
mTaskAdapter.setIsShowingLoadingUi(false);
- // TODO: Animate the loading UI out and the loaded data in.
- mTaskAdapter.notifyDataSetChanged();
+ int numActualItems = mTaskAdapter.getItemCount();
+ if (numEmptyItems < numActualItems) {
+ throw new IllegalStateException("There are less empty item views than the number "
+ + "of items to animate to.");
+ }
+ // Set item animator for content filling animation. The item animator will switch back
+ // to the default on completion.
+ mTaskRecyclerView.setItemAnimator(mLoadingContentItemAnimator);
+ mTaskAdapter.notifyItemRangeRemoved(numActualItems, numEmptyItems - numActualItems);
+ mTaskAdapter.notifyItemRangeChanged(
+ 0, numActualItems, CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT);
});
}