• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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.launcher3.allapps;
18 
19 import static android.view.View.VISIBLE;
20 
21 import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
22 
23 import static com.android.app.animation.Interpolators.DECELERATE_1_7;
24 import static com.android.app.animation.Interpolators.INSTANT;
25 import static com.android.app.animation.Interpolators.clampToProgress;
26 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
27 import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
28 import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
29 
30 import android.animation.ObjectAnimator;
31 import android.animation.TimeInterpolator;
32 import android.graphics.drawable.Drawable;
33 import android.util.FloatProperty;
34 import android.util.Log;
35 import android.view.View;
36 import android.view.ViewGroup;
37 import android.view.animation.Interpolator;
38 
39 import com.android.launcher3.BubbleTextView;
40 import com.android.launcher3.R;
41 import com.android.launcher3.Utilities;
42 import com.android.launcher3.config.FeatureFlags;
43 import com.android.launcher3.model.data.ItemInfo;
44 
45 /** Coordinates the transition between Search and A-Z in All Apps. */
46 public class SearchTransitionController {
47 
48     private static final String LOG_TAG = "SearchTransitionCtrl";
49 
50     // Interpolator when the user taps the QSB while already in All Apps.
51     private static final Interpolator INTERPOLATOR_WITHIN_ALL_APPS = DECELERATE_1_7;
52     // Interpolator when the user taps the QSB from home screen, so transition to all apps is
53     // happening simultaneously.
54     private static final Interpolator INTERPOLATOR_TRANSITIONING_TO_ALL_APPS = INSTANT;
55 
56     /**
57      * These values represent points on the [0, 1] animation progress spectrum. They are used to
58      * animate items in the {@link SearchRecyclerView}.
59      */
60     private static final float TOP_CONTENT_FADE_PROGRESS_START = 0.133f;
61     private static final float CONTENT_FADE_PROGRESS_DURATION = 0.083f;
62     private static final float TOP_BACKGROUND_FADE_PROGRESS_START = 0.633f;
63     private static final float BACKGROUND_FADE_PROGRESS_DURATION = 0.15f;
64     private static final float CONTENT_STAGGER = 0.01f;  // Progress before next item starts fading.
65 
66     private static final FloatProperty<SearchTransitionController> SEARCH_TO_AZ_PROGRESS =
67             new FloatProperty<SearchTransitionController>("searchToAzProgress") {
68                 @Override
69                 public Float get(SearchTransitionController controller) {
70                     return controller.getSearchToAzProgress();
71                 }
72 
73                 @Override
74                 public void setValue(SearchTransitionController controller, float progress) {
75                     controller.setSearchToAzProgress(progress);
76                 }
77             };
78 
79     private final ActivityAllAppsContainerView<?> mAllAppsContainerView;
80 
81     private ObjectAnimator mSearchToAzAnimator = null;
82     private float mSearchToAzProgress = 1f;
83     private boolean mSkipNextAnimationWithinAllApps;
84 
SearchTransitionController(ActivityAllAppsContainerView<?> allAppsContainerView)85     public SearchTransitionController(ActivityAllAppsContainerView<?> allAppsContainerView) {
86         mAllAppsContainerView = allAppsContainerView;
87     }
88 
89     /** Returns true if a transition animation is currently in progress. */
isRunning()90     public boolean isRunning() {
91         return mSearchToAzAnimator != null;
92     }
93 
94     /**
95      * Starts the transition to or from search state. If a transition is already in progress, the
96      * animation will start from that point with the new duration, and the previous onEndRunnable
97      * will not be called.
98      *
99      * @param goingToSearch true if will be showing search results, otherwise will be showing a-z
100      * @param duration time in ms for the animation to run
101      * @param onEndRunnable will be called when the animation finishes, unless another animation is
102      *                      scheduled in the meantime
103      */
animateToSearchState(boolean goingToSearch, long duration, Runnable onEndRunnable)104     public void animateToSearchState(boolean goingToSearch, long duration, Runnable onEndRunnable) {
105         float targetProgress = goingToSearch ? 0 : 1;
106 
107         if (mSearchToAzAnimator != null) {
108             mSearchToAzAnimator.cancel();
109         }
110 
111         mSearchToAzAnimator = ObjectAnimator.ofFloat(this, SEARCH_TO_AZ_PROGRESS, targetProgress);
112         boolean inAllApps = mAllAppsContainerView.isInAllApps();
113         TimeInterpolator timeInterpolator =
114                 inAllApps ? INTERPOLATOR_WITHIN_ALL_APPS : INTERPOLATOR_TRANSITIONING_TO_ALL_APPS;
115         if (mSkipNextAnimationWithinAllApps) {
116             timeInterpolator = INSTANT;
117             mSkipNextAnimationWithinAllApps = false;
118         }
119         if (timeInterpolator == INSTANT) {
120             duration = 0;  // Don't want to animate when coming from QSB.
121         }
122         mSearchToAzAnimator.setDuration(duration).setInterpolator(timeInterpolator);
123         mSearchToAzAnimator.addListener(forEndCallback(() -> mSearchToAzAnimator = null));
124         if (!goingToSearch) {
125             mSearchToAzAnimator.addListener(forSuccessCallback(() -> {
126                 mAllAppsContainerView.getFloatingHeaderView().setFloatingRowsCollapsed(false);
127                 mAllAppsContainerView.getFloatingHeaderView().reset(false /* animate */);
128                 mAllAppsContainerView.getAppsRecyclerViewContainer().setTranslationY(0);
129             }));
130         }
131         mSearchToAzAnimator.addListener(forSuccessCallback(onEndRunnable));
132 
133         mAllAppsContainerView.getFloatingHeaderView().setFloatingRowsCollapsed(true);
134         mAllAppsContainerView.getFloatingHeaderView().setVisibility(VISIBLE);
135         mAllAppsContainerView.getFloatingHeaderView().maybeSetTabVisibility(VISIBLE);
136         mAllAppsContainerView.getAppsRecyclerViewContainer().setVisibility(VISIBLE);
137         getSearchRecyclerView().setVisibility(VISIBLE);
138         getSearchRecyclerView().setChildAttachedConsumer(this::onSearchChildAttached);
139         mSearchToAzAnimator.start();
140     }
141 
getSearchRecyclerView()142     private SearchRecyclerView getSearchRecyclerView() {
143         return mAllAppsContainerView.getSearchRecyclerView();
144     }
145 
setSearchToAzProgress(float searchToAzProgress)146     private void setSearchToAzProgress(float searchToAzProgress) {
147         mSearchToAzProgress = searchToAzProgress;
148         int searchHeight = updateSearchRecyclerViewProgress();
149 
150         FloatingHeaderView headerView = mAllAppsContainerView.getFloatingHeaderView();
151 
152         // Add predictions + app divider height to account for predicted apps which will now be in
153         // the Search RV instead of the floating header view. Note `getFloatingRowsHeight` returns 0
154         // when predictions are not shown.
155         int appsTranslationY = searchHeight + headerView.getFloatingRowsHeight();
156 
157         if (headerView.usingTabs()) {
158             // Move tabs below the search results, and fade them out in 20% of the animation.
159             headerView.setTranslationY(searchHeight);
160             headerView.setAlpha(clampToProgress(searchToAzProgress, 0.8f, 1f));
161 
162             // Account for the additional padding added for the tabs.
163             appsTranslationY +=
164                     headerView.getTabsAdditionalPaddingBottom()
165                             + mAllAppsContainerView.getResources().getDimensionPixelOffset(
166                                     R.dimen.all_apps_tabs_margin_top)
167                             - headerView.getPaddingTop();
168         }
169 
170         View appsContainer = mAllAppsContainerView.getAppsRecyclerViewContainer();
171         appsContainer.setTranslationY(appsTranslationY);
172         // Fade apps out with tabs (in 20% of the total animation).
173         appsContainer.setAlpha(clampToProgress(searchToAzProgress, 0.8f, 1f));
174     }
175 
176     /**
177      * Updates the children views of SearchRecyclerView based on the current animation progress.
178      *
179      * @return the total height of animating views (excluding at most one row of app icons).
180      */
updateSearchRecyclerViewProgress()181     private int updateSearchRecyclerViewProgress() {
182         int numSearchResultsAnimated = 0;
183         int totalHeight = 0;
184         int appRowHeight = 0;
185         boolean appRowComplete = false;
186         Integer top = null;
187         SearchRecyclerView searchRecyclerView = getSearchRecyclerView();
188 
189         for (int i = 0; i < searchRecyclerView.getChildCount(); i++) {
190             View searchResultView = searchRecyclerView.getChildAt(i);
191             if (searchResultView == null) {
192                 continue;
193             }
194 
195             if (top == null) {
196                 top = searchResultView.getTop();
197             }
198 
199             int adapterPosition = searchRecyclerView.getChildAdapterPosition(searchResultView);
200             int spanIndex = getSpanIndex(searchRecyclerView, adapterPosition);
201             appRowComplete |= appRowHeight > 0 && spanIndex == 0;
202             // We don't animate the first (currently only) app row we see, as that is assumed to be
203             // predicted/prefix-matched apps.
204             boolean shouldAnimate = !isAppIcon(searchResultView) || appRowComplete;
205 
206             float contentAlpha = 1f;
207             float backgroundAlpha = 1f;
208             if (shouldAnimate) {
209                 if (spanIndex > 0) {
210                     // Animate this item with the previous item on the same row.
211                     numSearchResultsAnimated--;
212                 }
213 
214                 // Adjust content alpha based on start progress and stagger.
215                 float startContentFadeProgress = Math.max(0,
216                         TOP_CONTENT_FADE_PROGRESS_START
217                                 - CONTENT_STAGGER * numSearchResultsAnimated);
218                 float endContentFadeProgress = Math.min(1,
219                         startContentFadeProgress + CONTENT_FADE_PROGRESS_DURATION);
220                 contentAlpha = 1 - clampToProgress(mSearchToAzProgress,
221                         startContentFadeProgress, endContentFadeProgress);
222 
223                 // Adjust background (or decorator) alpha based on start progress and stagger.
224                 float startBackgroundFadeProgress = Math.max(0,
225                         TOP_BACKGROUND_FADE_PROGRESS_START
226                                 - CONTENT_STAGGER * numSearchResultsAnimated);
227                 float endBackgroundFadeProgress = Math.min(1,
228                         startBackgroundFadeProgress + BACKGROUND_FADE_PROGRESS_DURATION);
229                 backgroundAlpha = 1 - clampToProgress(mSearchToAzProgress,
230                         startBackgroundFadeProgress, endBackgroundFadeProgress);
231 
232                 numSearchResultsAnimated++;
233             }
234 
235             Drawable background = searchResultView.getBackground();
236             if (background != null
237                     && searchResultView instanceof ViewGroup
238                     && FeatureFlags.ENABLE_SEARCH_RESULT_BACKGROUND_DRAWABLES.get()) {
239                 searchResultView.setAlpha(1f);
240 
241                 // Apply content alpha to each child, since the view needs to be fully opaque for
242                 // the background to show properly.
243                 ViewGroup searchResultViewGroup = (ViewGroup) searchResultView;
244                 for (int j = 0; j < searchResultViewGroup.getChildCount(); j++) {
245                     searchResultViewGroup.getChildAt(j).setAlpha(contentAlpha);
246                 }
247 
248                 // Apply background alpha to the background drawable directly.
249                 background.setAlpha((int) (255 * backgroundAlpha));
250             } else {
251                 searchResultView.setAlpha(contentAlpha);
252 
253                 // Apply background alpha to decorator if possible.
254                 if (adapterPosition != NO_POSITION) {
255                     searchRecyclerView.getApps().getAdapterItems().get(adapterPosition)
256                             .setDecorationFillAlpha((int) (255 * backgroundAlpha));
257                 }
258 
259                 // Apply background alpha to view's background (e.g. for Search Edu card).
260                 if (background != null) {
261                     background.setAlpha((int) (255 * backgroundAlpha));
262                 }
263             }
264 
265             float scaleY = 1;
266             if (shouldAnimate) {
267                 scaleY = 1 - mSearchToAzProgress;
268             }
269             int scaledHeight = (int) (searchResultView.getHeight() * scaleY);
270             searchResultView.setScaleY(scaleY);
271 
272             // For rows with multiple elements, only count the height once and translate elements to
273             // the same y position.
274             int y = top + totalHeight;
275             if (spanIndex > 0) {
276                 // Continuation of an existing row; move this item into the row.
277                 y -= scaledHeight;
278             } else {
279                 // Start of a new row contributes to total height.
280                 totalHeight += scaledHeight;
281                 if (!shouldAnimate) {
282                     appRowHeight = scaledHeight;
283                 }
284             }
285             searchResultView.setY(y);
286         }
287 
288         return totalHeight - appRowHeight;
289     }
290 
291     /** @return the column that the view at this position is found (0 assumed if indeterminate). */
getSpanIndex(SearchRecyclerView searchRecyclerView, int adapterPosition)292     private int getSpanIndex(SearchRecyclerView searchRecyclerView, int adapterPosition) {
293         if (adapterPosition == NO_POSITION) {
294             Log.w(LOG_TAG, "Can't determine span index - child not found in adapter");
295             return 0;
296         }
297         if (!(searchRecyclerView.getAdapter() instanceof AllAppsGridAdapter<?>)) {
298             Log.e(LOG_TAG, "Search RV doesn't have an AllAppsGridAdapter?");
299             // This case shouldn't happen, but for debug devices we will continue to create a more
300             // visible crash.
301             if (!Utilities.IS_DEBUG_DEVICE) {
302                 return 0;
303             }
304         }
305         AllAppsGridAdapter<?> adapter = (AllAppsGridAdapter<?>) searchRecyclerView.getAdapter();
306         return adapter.getSpanIndex(adapterPosition);
307     }
308 
isAppIcon(View item)309     private boolean isAppIcon(View item) {
310         return item instanceof BubbleTextView && item.getTag() instanceof ItemInfo
311                 && ((ItemInfo) item.getTag()).itemType == ITEM_TYPE_APPLICATION;
312     }
313 
314     /** Called just before a child is attached to the SearchRecyclerView. */
onSearchChildAttached(View child)315     private void onSearchChildAttached(View child) {
316         // Avoid allocating hardware layers for alpha changes.
317         child.forceHasOverlappingRendering(false);
318         child.setPivotY(0);
319         if (mSearchToAzProgress > 0) {
320             // Before the child is rendered, apply the animation including it to avoid flicker.
321             updateSearchRecyclerViewProgress();
322         } else {
323             // Apply default states without processing the full layout.
324             child.setAlpha(1);
325             child.setScaleY(1);
326             child.setTranslationY(0);
327             int adapterPosition = getSearchRecyclerView().getChildAdapterPosition(child);
328             if (adapterPosition != NO_POSITION) {
329                 getSearchRecyclerView().getApps().getAdapterItems().get(adapterPosition)
330                         .setDecorationFillAlpha(255);
331             }
332             if (child instanceof ViewGroup
333                     && FeatureFlags.ENABLE_SEARCH_RESULT_BACKGROUND_DRAWABLES.get()) {
334                 ViewGroup childGroup = (ViewGroup) child;
335                 for (int i = 0; i < childGroup.getChildCount(); i++) {
336                     childGroup.getChildAt(i).setAlpha(1f);
337                 }
338             }
339             if (child.getBackground() != null) {
340                 child.getBackground().setAlpha(255);
341             }
342         }
343     }
344 
getSearchToAzProgress()345     private float getSearchToAzProgress() {
346         return mSearchToAzProgress;
347     }
348 
349     /**
350      * This should only be called from {@code LauncherSearchSessionManager} when app restarts due to
351      * theme changes.
352      */
setSkipAnimationWithinAllApps(boolean skip)353     public void setSkipAnimationWithinAllApps(boolean skip) {
354         mSkipNextAnimationWithinAllApps = skip;
355     }
356 }
357