blob: 6209393b202763b308dc6df63ce2c318a67acf75 [file] [log] [blame]
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.allapps;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
import static com.android.app.animation.Interpolators.DECELERATE_1_7;
import static com.android.app.animation.Interpolators.INSTANT;
import static com.android.app.animation.Interpolators.clampToProgress;
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.graphics.drawable.Drawable;
import android.util.FloatProperty;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Interpolator;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.Utilities;
import com.android.launcher3.model.data.ItemInfo;
import java.util.List;
public class RecyclerViewAnimationController {
private static final String LOG_TAG = "AnimationCtrl";
/**
* These values represent points on the [0, 1] animation progress spectrum. They are used to
* animate items in the {@link SearchRecyclerView} and private space container in
* {@link AllAppsRecyclerView}.
*/
protected static final float TOP_CONTENT_FADE_PROGRESS_START = 0.133f;
protected static final float CONTENT_FADE_PROGRESS_DURATION = 0.083f;
protected static final float TOP_BACKGROUND_FADE_PROGRESS_START = 0.633f;
protected static final float BACKGROUND_FADE_PROGRESS_DURATION = 0.15f;
// Progress before next item starts fading.
protected static final float CONTENT_STAGGER = 0.01f;
protected static final FloatProperty<RecyclerViewAnimationController> PROGRESS =
new FloatProperty<RecyclerViewAnimationController>("expansionProgress") {
@Override
public Float get(RecyclerViewAnimationController controller) {
return controller.getAnimationProgress();
}
@Override
public void setValue(RecyclerViewAnimationController controller, float progress) {
controller.setAnimationProgress(progress);
}
};
protected final ActivityAllAppsContainerView<?> mAllAppsContainerView;
protected ObjectAnimator mAnimator = null;
private float mAnimatorProgress = 1f;
public RecyclerViewAnimationController(ActivityAllAppsContainerView<?> allAppsContainerView) {
mAllAppsContainerView = allAppsContainerView;
}
/**
* Updates the children views of the current recyclerView based on the current animation
* progress.
*
* @return the total height of animating views (may exclude at most one row of app icons
* depending on which recyclerView is being acted upon).
*/
protected int onProgressUpdated(float expansionProgress) {
int numItemsAnimated = 0;
int totalHeight = 0;
int appRowHeight = 0;
boolean appRowComplete = false;
Integer top = null;
AllAppsRecyclerView allAppsRecyclerView = getRecyclerView();
for (int i = 0; i < allAppsRecyclerView.getChildCount(); i++) {
View currentView = allAppsRecyclerView.getChildAt(i);
if (currentView == null) {
continue;
}
if (top == null) {
top = currentView.getTop();
}
int adapterPosition = allAppsRecyclerView.getChildAdapterPosition(currentView);
List<BaseAllAppsAdapter.AdapterItem> allAppsAdapters = allAppsRecyclerView.getApps()
.getAdapterItems();
if (adapterPosition < 0 || adapterPosition >= allAppsAdapters.size()) {
continue;
}
BaseAllAppsAdapter.AdapterItem adapterItemAtPosition =
allAppsAdapters.get(adapterPosition);
int spanIndex = getSpanIndex(allAppsRecyclerView, adapterPosition);
appRowComplete |= appRowHeight > 0 && spanIndex == 0;
float backgroundAlpha = 1f;
boolean hasDecorationInfo = adapterItemAtPosition.getDecorationInfo() != null;
boolean shouldAnimate = shouldAnimate(currentView, hasDecorationInfo, appRowComplete);
if (shouldAnimate) {
if (spanIndex > 0) {
// Animate this item with the previous item on the same row.
numItemsAnimated--;
}
// Adjust background (or decorator) alpha based on start progress and stagger.
backgroundAlpha = getAdjustedBackgroundAlpha(numItemsAnimated);
}
Drawable background = currentView.getBackground();
if (background != null && currentView instanceof ViewGroup currentViewGroup) {
currentView.setAlpha(1f);
// Apply content alpha to each child, since the view needs to be fully opaque for
// the background to show properly.
for (int j = 0; j < currentViewGroup.getChildCount(); j++) {
setViewAdjustedContentAlpha(currentViewGroup.getChildAt(j), numItemsAnimated,
shouldAnimate);
}
// Apply background alpha to the background drawable directly.
background.setAlpha((int) (255 * backgroundAlpha));
} else {
// Adjust content alpha based on start progress and stagger.
setViewAdjustedContentAlpha(currentView, numItemsAnimated, shouldAnimate);
// Apply background alpha to decorator if possible.
setAdjustedAdapterItemDecorationBackgroundAlpha(
allAppsRecyclerView.getApps().getAdapterItems().get(adapterPosition),
numItemsAnimated);
// Apply background alpha to view's background (e.g. for Search Edu card).
if (background != null) {
background.setAlpha((int) (255 * backgroundAlpha));
}
}
float scaleY = 1;
if (shouldAnimate) {
scaleY = 1 - getAnimationProgress();
// Update number of search results that has been animated.
numItemsAnimated++;
}
int scaledHeight = (int) (currentView.getHeight() * scaleY);
currentView.setScaleY(scaleY);
// For rows with multiple elements, only count the height once and translate elements to
// the same y position.
int y = top + totalHeight;
if (spanIndex > 0) {
// Continuation of an existing row; move this item into the row.
y -= scaledHeight;
} else {
// Start of a new row contributes to total height.
totalHeight += scaledHeight;
if (!shouldAnimate) {
appRowHeight = scaledHeight;
}
}
currentView.setY(y);
}
return totalHeight - appRowHeight;
}
protected void animateToState(boolean expand, long duration, Runnable onEndRunnable) {
float targetProgress = expand ? 0 : 1;
if (mAnimator != null) {
mAnimator.cancel();
}
mAnimator = ObjectAnimator.ofFloat(this, PROGRESS, targetProgress);
TimeInterpolator timeInterpolator = getInterpolator();
if (timeInterpolator == INSTANT) {
duration = 0;
}
mAnimator.addListener(forEndCallback(() -> mAnimator = null));
mAnimator.setDuration(duration).setInterpolator(timeInterpolator);
mAnimator.addListener(forSuccessCallback(onEndRunnable));
mAnimator.start();
getRecyclerView().setChildAttachedConsumer(this::onChildAttached);
}
/** Called just before a child is attached to the RecyclerView. */
private void onChildAttached(View child) {
// Avoid allocating hardware layers for alpha changes.
child.forceHasOverlappingRendering(false);
child.setPivotY(0);
if (getAnimationProgress() > 0 && getAnimationProgress() < 1) {
// Before the child is rendered, apply the animation including it to avoid flicker.
onProgressUpdated(getAnimationProgress());
} else {
// Apply default states without processing the full layout.
child.setAlpha(1);
child.setScaleY(1);
child.setTranslationY(0);
int adapterPosition = getRecyclerView().getChildAdapterPosition(child);
List<BaseAllAppsAdapter.AdapterItem> allAppsAdapters =
getRecyclerView().getApps().getAdapterItems();
if (adapterPosition >= 0 && adapterPosition < allAppsAdapters.size()) {
allAppsAdapters.get(adapterPosition).setDecorationFillAlpha(255);
}
if (child instanceof ViewGroup childGroup) {
for (int i = 0; i < childGroup.getChildCount(); i++) {
childGroup.getChildAt(i).setAlpha(1f);
}
}
if (child.getBackground() != null) {
child.getBackground().setAlpha(255);
}
}
}
/** @return the column that the view at this position is found (0 assumed if indeterminate). */
protected int getSpanIndex(AllAppsRecyclerView appsRecyclerView, int adapterPosition) {
if (adapterPosition == NO_POSITION) {
Log.w(LOG_TAG, "Can't determine span index - child not found in adapter");
return 0;
}
if (!(appsRecyclerView.getAdapter() instanceof AllAppsGridAdapter<?>)) {
Log.e(LOG_TAG, "Search RV doesn't have an AllAppsGridAdapter?");
// This case shouldn't happen, but for debug devices we will continue to create a more
// visible crash.
if (!Utilities.IS_DEBUG_DEVICE) {
return 0;
}
}
AllAppsGridAdapter<?> adapter = (AllAppsGridAdapter<?>) appsRecyclerView.getAdapter();
return adapter.getSpanIndex(adapterPosition);
}
protected TimeInterpolator getInterpolator() {
return DECELERATE_1_7;
}
protected AllAppsRecyclerView getRecyclerView() {
return mAllAppsContainerView.mAH.get(ActivityAllAppsContainerView.AdapterHolder.MAIN)
.mRecyclerView;
}
/** Returns true if a transition animation is currently in progress. */
protected boolean isRunning() {
return mAnimator != null;
}
/** Should only animate if the view is an app icon and if it has a decoration info. */
protected boolean shouldAnimate(View view, boolean hasDecorationInfo,
boolean firstAppRowComplete) {
return isAppIcon(view) && hasDecorationInfo;
}
private float getAdjustedContentAlpha(int itemsAnimated) {
float startContentFadeProgress = Math.max(0,
TOP_CONTENT_FADE_PROGRESS_START - CONTENT_STAGGER * itemsAnimated);
float endContentFadeProgress = Math.min(1,
startContentFadeProgress + CONTENT_FADE_PROGRESS_DURATION);
return 1 - clampToProgress(mAnimatorProgress,
startContentFadeProgress, endContentFadeProgress);
}
private float getAdjustedBackgroundAlpha(int itemsAnimated) {
float startBackgroundFadeProgress = Math.max(0,
TOP_BACKGROUND_FADE_PROGRESS_START - CONTENT_STAGGER * itemsAnimated);
float endBackgroundFadeProgress = Math.min(1,
startBackgroundFadeProgress + BACKGROUND_FADE_PROGRESS_DURATION);
return 1 - clampToProgress(mAnimatorProgress,
startBackgroundFadeProgress, endBackgroundFadeProgress);
}
private void setViewAdjustedContentAlpha(View view, int numberOfItemsAnimated,
boolean shouldAnimate) {
view.setAlpha(shouldAnimate ? getAdjustedContentAlpha(numberOfItemsAnimated) : 1f);
}
private void setAdjustedAdapterItemDecorationBackgroundAlpha(
BaseAllAppsAdapter.AdapterItem adapterItem, int numberOfItemsAnimated) {
adapterItem.setDecorationFillAlpha((int)
(255 * getAdjustedBackgroundAlpha(numberOfItemsAnimated)));
}
private float getAnimationProgress() {
return mAnimatorProgress;
}
private void setAnimationProgress(float expansionProgress) {
mAnimatorProgress = expansionProgress;
onProgressUpdated(expansionProgress);
}
protected boolean isAppIcon(View item) {
return item instanceof BubbleTextView && item.getTag() instanceof ItemInfo
&& ((ItemInfo) item.getTag()).itemType == ITEM_TYPE_APPLICATION;
}
}