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 package com.android.launcher3.allapps;
17 
18 import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
19 
20 import static com.android.app.animation.Interpolators.DECELERATE_1_7;
21 import static com.android.app.animation.Interpolators.INSTANT;
22 import static com.android.app.animation.Interpolators.clampToProgress;
23 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
24 import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
25 import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
26 
27 import android.animation.ObjectAnimator;
28 import android.animation.TimeInterpolator;
29 import android.graphics.drawable.Drawable;
30 import android.util.FloatProperty;
31 import android.util.Log;
32 import android.view.View;
33 import android.view.ViewGroup;
34 import android.view.animation.Interpolator;
35 
36 import com.android.launcher3.BubbleTextView;
37 import com.android.launcher3.Utilities;
38 import com.android.launcher3.model.data.ItemInfo;
39 
40 import java.util.List;
41 
42 public class RecyclerViewAnimationController {
43 
44     private static final String LOG_TAG = "AnimationCtrl";
45 
46     /**
47      * These values represent points on the [0, 1] animation progress spectrum. They are used to
48      * animate items in the {@link SearchRecyclerView} and private space container in
49      * {@link AllAppsRecyclerView}.
50      */
51     protected static final float TOP_CONTENT_FADE_PROGRESS_START = 0.133f;
52     protected static final float CONTENT_FADE_PROGRESS_DURATION = 0.083f;
53     protected static final float TOP_BACKGROUND_FADE_PROGRESS_START = 0.633f;
54     protected static final float BACKGROUND_FADE_PROGRESS_DURATION = 0.15f;
55     // Progress before next item starts fading.
56     protected static final float CONTENT_STAGGER = 0.01f;
57 
58     protected static final FloatProperty<RecyclerViewAnimationController> PROGRESS =
59             new FloatProperty<RecyclerViewAnimationController>("expansionProgress") {
60                 @Override
61                 public Float get(RecyclerViewAnimationController controller) {
62                     return controller.getAnimationProgress();
63                 }
64 
65                 @Override
66                 public void setValue(RecyclerViewAnimationController controller, float progress) {
67                     controller.setAnimationProgress(progress);
68                 }
69             };
70 
71     protected final ActivityAllAppsContainerView<?> mAllAppsContainerView;
72     protected ObjectAnimator mAnimator = null;
73     private float mAnimatorProgress = 1f;
74 
RecyclerViewAnimationController(ActivityAllAppsContainerView<?> allAppsContainerView)75     public RecyclerViewAnimationController(ActivityAllAppsContainerView<?> allAppsContainerView) {
76         mAllAppsContainerView = allAppsContainerView;
77     }
78 
79     /**
80      * Updates the children views of the current recyclerView based on the current animation
81      * progress.
82      *
83      * @return the total height of animating views (may exclude at most one row of app icons
84      * depending on which recyclerView is being acted upon).
85      */
onProgressUpdated(float expansionProgress)86     protected int onProgressUpdated(float expansionProgress) {
87         int numItemsAnimated = 0;
88         int totalHeight = 0;
89         int appRowHeight = 0;
90         boolean appRowComplete = false;
91         Integer top = null;
92         AllAppsRecyclerView allAppsRecyclerView = getRecyclerView();
93 
94         for (int i = 0; i < allAppsRecyclerView.getChildCount(); i++) {
95             View currentView = allAppsRecyclerView.getChildAt(i);
96             if (currentView == null) {
97                 continue;
98             }
99             if (top == null) {
100                 top = currentView.getTop();
101             }
102             int adapterPosition = allAppsRecyclerView.getChildAdapterPosition(currentView);
103             List<BaseAllAppsAdapter.AdapterItem> allAppsAdapters = allAppsRecyclerView.getApps()
104                     .getAdapterItems();
105             if (adapterPosition < 0 || adapterPosition >= allAppsAdapters.size()) {
106                 continue;
107             }
108             BaseAllAppsAdapter.AdapterItem adapterItemAtPosition =
109                     allAppsAdapters.get(adapterPosition);
110             int spanIndex = getSpanIndex(allAppsRecyclerView, adapterPosition);
111             appRowComplete |= appRowHeight > 0 && spanIndex == 0;
112 
113             float backgroundAlpha = 1f;
114             boolean hasDecorationInfo = adapterItemAtPosition.getDecorationInfo() != null;
115             boolean shouldAnimate = shouldAnimate(currentView, hasDecorationInfo, appRowComplete);
116 
117             if (shouldAnimate) {
118                 if (spanIndex > 0) {
119                     // Animate this item with the previous item on the same row.
120                     numItemsAnimated--;
121                 }
122                 // Adjust background (or decorator) alpha based on start progress and stagger.
123                 backgroundAlpha = getAdjustedBackgroundAlpha(numItemsAnimated);
124             }
125 
126             Drawable background = currentView.getBackground();
127             if (background != null && currentView instanceof ViewGroup currentViewGroup) {
128                 currentView.setAlpha(1f);
129                 // Apply content alpha to each child, since the view needs to be fully opaque for
130                 // the background to show properly.
131                 for (int j = 0; j < currentViewGroup.getChildCount(); j++) {
132                     setViewAdjustedContentAlpha(currentViewGroup.getChildAt(j), numItemsAnimated,
133                             shouldAnimate);
134                 }
135 
136                 // Apply background alpha to the background drawable directly.
137                 background.setAlpha((int) (255 * backgroundAlpha));
138             } else {
139                 // Adjust content alpha based on start progress and stagger.
140                 setViewAdjustedContentAlpha(currentView, numItemsAnimated, shouldAnimate);
141 
142                 // Apply background alpha to decorator if possible.
143                 setAdjustedAdapterItemDecorationBackgroundAlpha(
144                         allAppsRecyclerView.getApps().getAdapterItems().get(adapterPosition),
145                         numItemsAnimated);
146 
147                 // Apply background alpha to view's background (e.g. for Search Edu card).
148                 if (background != null) {
149                     background.setAlpha((int) (255 * backgroundAlpha));
150                 }
151             }
152 
153             float scaleY = 1;
154             if (shouldAnimate) {
155                 scaleY = 1 - getAnimationProgress();
156                 // Update number of search results that has been animated.
157                 numItemsAnimated++;
158             }
159             int scaledHeight = (int) (currentView.getHeight() * scaleY);
160             currentView.setScaleY(scaleY);
161 
162             // For rows with multiple elements, only count the height once and translate elements to
163             // the same y position.
164             int y = top + totalHeight;
165             if (spanIndex > 0) {
166                 // Continuation of an existing row; move this item into the row.
167                 y -= scaledHeight;
168             } else {
169                 // Start of a new row contributes to total height.
170                 totalHeight += scaledHeight;
171                 if (!shouldAnimate) {
172                     appRowHeight = scaledHeight;
173                 }
174             }
175             currentView.setY(y);
176         }
177         return totalHeight - appRowHeight;
178     }
179 
animateToState(boolean expand, long duration, Runnable onEndRunnable)180     protected void animateToState(boolean expand, long duration, Runnable onEndRunnable) {
181         float targetProgress = expand ? 0 : 1;
182         if (mAnimator != null) {
183             mAnimator.cancel();
184         }
185         mAnimator = ObjectAnimator.ofFloat(this, PROGRESS, targetProgress);
186 
187         TimeInterpolator timeInterpolator = getInterpolator();
188         if (timeInterpolator == INSTANT) {
189             duration = 0;
190         }
191 
192         mAnimator.addListener(forEndCallback(() -> mAnimator = null));
193         mAnimator.setDuration(duration).setInterpolator(timeInterpolator);
194         mAnimator.addListener(forSuccessCallback(onEndRunnable));
195         mAnimator.start();
196         getRecyclerView().setChildAttachedConsumer(this::onChildAttached);
197     }
198 
199     /** Called just before a child is attached to the RecyclerView. */
onChildAttached(View child)200     private void onChildAttached(View child) {
201         // Avoid allocating hardware layers for alpha changes.
202         child.forceHasOverlappingRendering(false);
203         child.setPivotY(0);
204         if (getAnimationProgress() > 0 && getAnimationProgress() < 1) {
205             // Before the child is rendered, apply the animation including it to avoid flicker.
206             onProgressUpdated(getAnimationProgress());
207         } else {
208             // Apply default states without processing the full layout.
209             child.setAlpha(1);
210             child.setScaleY(1);
211             child.setTranslationY(0);
212             int adapterPosition = getRecyclerView().getChildAdapterPosition(child);
213             List<BaseAllAppsAdapter.AdapterItem> allAppsAdapters =
214                     getRecyclerView().getApps().getAdapterItems();
215             if (adapterPosition >= 0 && adapterPosition < allAppsAdapters.size()) {
216                 allAppsAdapters.get(adapterPosition).setDecorationFillAlpha(255);
217             }
218             if (child instanceof ViewGroup childGroup) {
219                 for (int i = 0; i < childGroup.getChildCount(); i++) {
220                     childGroup.getChildAt(i).setAlpha(1f);
221                 }
222             }
223             if (child.getBackground() != null) {
224                 child.getBackground().setAlpha(255);
225             }
226         }
227     }
228 
229     /** @return the column that the view at this position is found (0 assumed if indeterminate). */
getSpanIndex(AllAppsRecyclerView appsRecyclerView, int adapterPosition)230     protected int getSpanIndex(AllAppsRecyclerView appsRecyclerView, int adapterPosition) {
231         if (adapterPosition == NO_POSITION) {
232             Log.w(LOG_TAG, "Can't determine span index - child not found in adapter");
233             return 0;
234         }
235         if (!(appsRecyclerView.getAdapter() instanceof AllAppsGridAdapter<?>)) {
236             Log.e(LOG_TAG, "Search RV doesn't have an AllAppsGridAdapter?");
237             // This case shouldn't happen, but for debug devices we will continue to create a more
238             // visible crash.
239             if (!Utilities.IS_DEBUG_DEVICE) {
240                 return 0;
241             }
242         }
243         AllAppsGridAdapter<?> adapter = (AllAppsGridAdapter<?>) appsRecyclerView.getAdapter();
244         return adapter.getSpanIndex(adapterPosition);
245     }
246 
getInterpolator()247     protected TimeInterpolator getInterpolator() {
248         return DECELERATE_1_7;
249     }
250 
getRecyclerView()251     protected AllAppsRecyclerView getRecyclerView() {
252         return mAllAppsContainerView.mAH.get(ActivityAllAppsContainerView.AdapterHolder.MAIN)
253                 .mRecyclerView;
254     }
255 
256     /** Returns true if a transition animation is currently in progress. */
isRunning()257     protected boolean isRunning() {
258         return mAnimator != null;
259     }
260 
261     /** Should only animate if the view is an app icon and if it has a decoration info. */
shouldAnimate(View view, boolean hasDecorationInfo, boolean firstAppRowComplete)262     protected boolean shouldAnimate(View view, boolean hasDecorationInfo,
263             boolean firstAppRowComplete) {
264         return isAppIcon(view) && hasDecorationInfo;
265     }
266 
getAdjustedContentAlpha(int itemsAnimated)267     private float getAdjustedContentAlpha(int itemsAnimated) {
268         float startContentFadeProgress = Math.max(0,
269                 TOP_CONTENT_FADE_PROGRESS_START - CONTENT_STAGGER * itemsAnimated);
270         float endContentFadeProgress = Math.min(1,
271                 startContentFadeProgress + CONTENT_FADE_PROGRESS_DURATION);
272         return 1 - clampToProgress(mAnimatorProgress,
273                 startContentFadeProgress, endContentFadeProgress);
274     }
275 
getAdjustedBackgroundAlpha(int itemsAnimated)276     private float getAdjustedBackgroundAlpha(int itemsAnimated) {
277         float startBackgroundFadeProgress = Math.max(0,
278                 TOP_BACKGROUND_FADE_PROGRESS_START - CONTENT_STAGGER * itemsAnimated);
279         float endBackgroundFadeProgress = Math.min(1,
280                 startBackgroundFadeProgress + BACKGROUND_FADE_PROGRESS_DURATION);
281         return 1 - clampToProgress(mAnimatorProgress,
282                 startBackgroundFadeProgress, endBackgroundFadeProgress);
283     }
284 
setViewAdjustedContentAlpha(View view, int numberOfItemsAnimated, boolean shouldAnimate)285     private void setViewAdjustedContentAlpha(View view, int numberOfItemsAnimated,
286             boolean shouldAnimate) {
287         view.setAlpha(shouldAnimate ? getAdjustedContentAlpha(numberOfItemsAnimated) : 1f);
288     }
289 
setAdjustedAdapterItemDecorationBackgroundAlpha( BaseAllAppsAdapter.AdapterItem adapterItem, int numberOfItemsAnimated)290     private void setAdjustedAdapterItemDecorationBackgroundAlpha(
291             BaseAllAppsAdapter.AdapterItem adapterItem, int numberOfItemsAnimated) {
292         adapterItem.setDecorationFillAlpha((int)
293                 (255 * getAdjustedBackgroundAlpha(numberOfItemsAnimated)));
294     }
295 
getAnimationProgress()296     private float getAnimationProgress() {
297         return mAnimatorProgress;
298     }
299 
setAnimationProgress(float expansionProgress)300     private void setAnimationProgress(float expansionProgress) {
301         mAnimatorProgress = expansionProgress;
302         onProgressUpdated(expansionProgress);
303     }
304 
isAppIcon(View item)305     protected boolean isAppIcon(View item) {
306         return item instanceof BubbleTextView && item.getTag() instanceof ItemInfo
307                 && ((ItemInfo) item.getTag()).itemType == ITEM_TYPE_APPLICATION;
308     }
309 }
310