Animate content fill animation to Recents Go

This CL adds the animation that occurs when transitioning from a set of
empty views to the actual task list after it is loaded. This is done by
setting a one-shot item animator that animates changes, for item views
that fade from empty to filled, and removes, for when we have too many
empty views.

Bug: 114136250
Test: Artificially increase task list load time and see animation fill
Change-Id: Ibbc09db702e591063ceea61df2359f18a3fcf8f9
(cherry picked from commit 987799dfa1)
This commit is contained in:
Kevin 2019-04-11 16:40:29 -07:00 committed by Kevin Han
parent 1060a0d7fc
commit 29cdac41f1
3 changed files with 323 additions and 10 deletions

View File

@ -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();
}
}
}
}

View File

@ -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())) {
@ -96,13 +120,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) {

View File

@ -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);
});
}