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