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:
parent
1060a0d7fc
commit
29cdac41f1
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue