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