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