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