1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.car.carlauncher.recyclerview;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ValueAnimator;
22 import android.view.View;
23 import android.view.ViewPropertyAnimator;
24 
25 import androidx.recyclerview.widget.DefaultItemAnimator;
26 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
27 
28 import com.android.car.carlauncher.R;
29 
30 import java.util.ArrayList;
31 import java.util.List;
32 
33 /**
34  * RecyclerView.ItemAnimator that animates the dropping of drag shadow onto the new view holder.
35  */
36 public class AppGridItemAnimator extends DefaultItemAnimator {
37     private long mDropDuration = 50L;
38     private final ArrayList<DropInfo> mPendingDrops = new ArrayList<>();
39     private final ArrayList<ViewHolder> mDropAnimations = new ArrayList<>();
40     private final ArrayList<ViewHolder> mQueuedMoveAnimations = new ArrayList<>();
41 
42     private static class DropInfo {
43         public AppItemViewHolder holder;
DropInfo(AppItemViewHolder holder)44         DropInfo(AppItemViewHolder holder) {
45             this.holder = holder;
46         }
47     }
48 
49     @Override
animateMove(ViewHolder holder, int fromX, int fromY, int toX, int toY)50     public boolean animateMove(ViewHolder holder, int fromX, int fromY,
51             int toX, int toY) {
52         AppItemViewHolder viewHolder = (AppItemViewHolder) holder;
53         if (viewHolder.isMostRecentlySelected()) {
54             return animateDrop(viewHolder);
55         }
56         resetAnimation(viewHolder);
57         viewHolder.resetTranslationZ();
58         mQueuedMoveAnimations.add(viewHolder);
59         return super.animateMove(holder, fromX, fromY, toX, toY);
60     }
61 
62     @Override
onMoveStarting(ViewHolder holder)63     public void onMoveStarting(ViewHolder holder) {
64         AppItemViewHolder viewHolder = (AppItemViewHolder) holder;
65         if (mQueuedMoveAnimations.contains(viewHolder)) {
66             mQueuedMoveAnimations.remove(viewHolder);
67             if (mQueuedMoveAnimations.isEmpty()) {
68                 Runnable dropper = new Runnable() {
69                     @Override
70                     public void run() {
71                         for (DropInfo dropInfo : mPendingDrops) {
72                             animateDropImpl(dropInfo.holder);
73                         }
74                         mPendingDrops.clear();
75                     }
76                 };
77                 dropper.run();
78             }
79         }
80     }
81 
82     @Override
animateAdd(ViewHolder holder)83     public boolean animateAdd(ViewHolder holder) {
84         AppItemViewHolder viewHolder = (AppItemViewHolder) holder;
85         if (viewHolder.isMostRecentlySelected()) {
86             return animateDrop(viewHolder);
87         }
88         return super.animateAdd(holder);
89     }
90 
91     /**
92      * Called when an item is dropped in the RecyclerView. Implementors can choose
93      * whether and how to animate that change, but must always call
94      * {@link #dispatchDropFinished(ViewHolder)} when done, either
95      * immediately (if no animation will occur) or after the animation actually finishes.
96      * The return value indicates whether an animation has been set up and whether the
97      * ItemAnimator's {@link #runPendingAnimations()} method should be called at the
98      * next opportunity.
99      */
100 
animateDrop(ViewHolder holder)101     public boolean animateDrop(ViewHolder holder) {
102         AppItemViewHolder viewHolder = (AppItemViewHolder) holder;
103         resetAnimation(viewHolder);
104         viewHolder.prepareForDropAnimation();
105         mPendingDrops.add(new DropInfo(viewHolder));
106         return true;
107     }
108 
animateDropImpl(AppItemViewHolder viewHolder)109     private void animateDropImpl(AppItemViewHolder viewHolder) {
110         mDropAnimations.add(viewHolder);
111         final ViewPropertyAnimator dropAnimation = viewHolder.getDropAnimation();
112         dropAnimation.setDuration(getDropDuration())
113                 .setStartDelay(viewHolder.itemView.getResources().getInteger(
114                         R.integer.ms_drop_animation_delay))
115                 .setListener(new AnimatorListenerAdapter() {
116                     @Override
117                     public void onAnimationStart(Animator animator) {
118                         dispatchDropStarting(viewHolder);
119                     }
120 
121                     @Override
122                     public void onAnimationEnd(Animator animator) {
123                         dropAnimation.setListener(null);
124                         mDropAnimations.remove(viewHolder);
125                         dispatchDropFinished(viewHolder);
126                         if (!isRunning()) {
127                             dispatchAnimationsFinished();
128                         }
129                     }
130                 }).start();
131     }
132 
133     @Override
isRunning()134     public boolean isRunning() {
135         return (!mPendingDrops.isEmpty()
136                 || !mQueuedMoveAnimations.isEmpty()
137                 || !mDropAnimations.isEmpty()
138                 || super.isRunning());
139     }
140 
141     /**
142      * Method to be called by subclasses when a drop animation is being started.
143      */
dispatchDropStarting(ViewHolder item)144     public void dispatchDropStarting(ViewHolder item) {
145         onDropStarting(item);
146     }
147 
148     /**
149      * Called when a drop animation is being started on a given ViewHolder.
150      * The default implementation does nothing. Subclasses may wish to override
151      * this method to handle any ViewHolder-specific operations linked to animation
152      * lifecycles.
153      */
onDropStarting(ViewHolder item)154     public void onDropStarting(ViewHolder item) {
155     }
156 
157     /**
158      * Method to be called by subclasses when a drop animation is done.
159      */
dispatchDropFinished(ViewHolder item)160     public void dispatchDropFinished(ViewHolder item) {
161         onDropFinished(item);
162         dispatchAnimationFinished(item);
163     }
164 
165     /**
166      * Called when a drop animation has ended on a given ViewHolder.
167      * The default implementation does nothing. Subclasses may wish to override
168      * this method to handle any ViewHolder-specific operations linked to animation
169      * lifecycles.
170      */
onDropFinished(ViewHolder item)171     public void onDropFinished(ViewHolder item) {
172     }
173 
174     @Override
endAnimations()175     public void endAnimations() {
176         int count = mPendingDrops.size();
177         for (int i = count - 1; i >= 0; i--) {
178             ViewHolder holder = mPendingDrops.get(i).holder;
179             View view = holder.itemView;
180             view.setTranslationX(0);
181             view.setTranslationY(0);
182             dispatchDropFinished(holder);
183             mPendingDrops.remove(i);
184         }
185         cancelAll(mDropAnimations);
186         mDropAnimations.clear();
187         mQueuedMoveAnimations.clear();
188         super.endAnimations();
189     }
190 
resetAnimation(ViewHolder holder)191     private void resetAnimation(ViewHolder holder) {
192         holder.itemView.animate().setInterpolator(new ValueAnimator().getInterpolator());
193         endAnimation(holder);
194     }
195 
cancelAll(List<ViewHolder> viewHolders)196     private void cancelAll(List<ViewHolder> viewHolders) {
197         for (int i = viewHolders.size() - 1; i >= 0; i--) {
198             viewHolders.get(i).itemView.animate().cancel();
199         }
200     }
201 
setDropDuration(long duration)202     public void setDropDuration(long duration) {
203         mDropDuration = duration;
204     }
205 
getDropDuration()206     public long getDropDuration() {
207         return mDropDuration;
208     }
209 }
210