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