summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrandon Dayauon <brdayauon@google.com>2023-12-13 00:00:14 +0000
committerAndroid (Google) Code Review <android-gerrit@google.com>2023-12-13 00:00:14 +0000
commitd7f0ccf52eb65df7e58d58b6201c3a0fc2723494 (patch)
tree6d65c2e4ff97c7d2601e54f2402ec32abfae4cd7
parentd48279bf1dfb4a6866cbdabc12a84703c438796d (diff)
parent763e40d74795d2f2ce772e418edb26e9a114ff9b (diff)
downloadLauncher3-d7f0ccf52eb65df7e58d58b6201c3a0fc2723494.tar.gz
Merge "Move UnionDecorationHandler to Launcher" into main
-rw-r--r--res/values/dimens.xml3
-rw-r--r--src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java2
-rw-r--r--src/com/android/launcher3/allapps/AllAppsRecyclerView.java21
-rw-r--r--src/com/android/launcher3/allapps/BaseAllAppsAdapter.java18
-rw-r--r--src/com/android/launcher3/allapps/RecyclerViewAnimationController.java309
-rw-r--r--src/com/android/launcher3/allapps/SearchRecyclerView.java15
-rw-r--r--src/com/android/launcher3/allapps/SearchTransitionController.java269
-rw-r--r--src/com/android/launcher3/allapps/SectionDecorationHandler.java206
-rw-r--r--src/com/android/launcher3/allapps/SectionDecorationInfo.java77
9 files changed, 657 insertions, 263 deletions
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 242c4396a5..8cb6c71616 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -125,6 +125,9 @@
<dimen name="all_apps_tip_bottom_margin">8dp</dimen>
<dimen name="all_apps_height_extra">6dp</dimen>
<dimen name="all_apps_paged_view_top_padding">40dp</dimen>
+ <dimen name="all_apps_recycler_view_decorator_padding">1dp</dimen>
+ <dimen name="all_apps_recycler_view_decorator_group_radius">28dp</dimen>
+ <dimen name="all_apps_recycler_view_decorator_result_radius">4dp</dimen>
<dimen name="all_apps_icon_drawable_padding">8dp</dimen>
<dimen name="all_apps_predicted_icon_vertical_padding">8dp</dimen>
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index e5a223a9f9..7f1d216c95 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -407,7 +407,7 @@ public class ActivityAllAppsContainerView<T extends Context & ActivityContext>
// If exiting search, revert predictive back scale on all apps
mAllAppsTransitionController.animateAllAppsToNoScale();
}
- mSearchTransitionController.animateToSearchState(goingToSearch, durationMs,
+ mSearchTransitionController.animateToState(goingToSearch, durationMs,
/* onEndRunnable = */ () -> {
mIsSearching = goingToSearch;
updateSearchResultsVisibility();
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index b0f13ef863..36a44cc345 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -36,7 +36,11 @@ import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.Log;
+import android.view.View;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.util.Consumer;
import androidx.recyclerview.widget.RecyclerView;
import com.android.launcher3.DeviceProfile;
@@ -57,6 +61,7 @@ public class AllAppsRecyclerView extends FastScrollRecyclerView {
protected static final String TAG = "AllAppsRecyclerView";
private static final boolean DEBUG = false;
private static final boolean DEBUG_LATENCY = Utilities.isPropertyEnabled(SEARCH_LOGGING);
+ private Consumer<View> mChildAttachedConsumer;
protected final int mNumAppsPerRow;
private final AllAppsFastScrollHelper mFastScrollHelper;
@@ -282,6 +287,22 @@ public class AllAppsRecyclerView extends FastScrollRecyclerView {
}
}
+ /**
+ * This will be called just before a new child is attached to the window. Passing in null will
+ * remove the consumer.
+ */
+ protected void setChildAttachedConsumer(@Nullable Consumer<View> childAttachedConsumer) {
+ mChildAttachedConsumer = childAttachedConsumer;
+ }
+
+ @Override
+ public void onChildAttachedToWindow(@NonNull View child) {
+ if (mChildAttachedConsumer != null) {
+ mChildAttachedConsumer.accept(child);
+ }
+ super.onChildAttachedToWindow(child);
+ }
+
@Override
public int getScrollBarTop() {
return ActivityContext.lookupContext(getContext()).getAppsView().isSearchSupported()
diff --git a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
index 5e26ea5a9a..035c30f6cd 100644
--- a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
+++ b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
@@ -25,6 +25,7 @@ import android.view.ViewGroup;
import android.widget.RelativeLayout;
import android.widget.TextView;
+import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.android.launcher3.BubbleTextView;
@@ -92,7 +93,8 @@ public abstract class BaseAllAppsAdapter<T extends Context & ActivityContext> ex
public int rowAppIndex;
// The associated ItemInfoWithIcon for the item
public AppInfo itemInfo = null;
-
+ // Private App Decorator
+ public SectionDecorationInfo decorationInfo = null;
public AdapterItem(int viewType) {
this.viewType = viewType;
}
@@ -125,9 +127,17 @@ public abstract class BaseAllAppsAdapter<T extends Context & ActivityContext> ex
return itemInfo == null && other.itemInfo == null;
}
- /** Sets the alpha of the decorator for this item. Returns true if successful. */
- public boolean setDecorationFillAlpha(int alpha) {
- return false;
+ @Nullable
+ public SectionDecorationInfo getDecorationInfo() {
+ return decorationInfo;
+ }
+
+ /** Sets the alpha of the decorator for this item. */
+ protected void setDecorationFillAlpha(int alpha) {
+ if (decorationInfo == null || decorationInfo.getDecorationHandler() == null) {
+ return;
+ }
+ decorationInfo.getDecorationHandler().setFillAlpha(alpha);
}
}
diff --git a/src/com/android/launcher3/allapps/RecyclerViewAnimationController.java b/src/com/android/launcher3/allapps/RecyclerViewAnimationController.java
new file mode 100644
index 0000000000..6209393b20
--- /dev/null
+++ b/src/com/android/launcher3/allapps/RecyclerViewAnimationController.java
@@ -0,0 +1,309 @@
+/*
+ * 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;
+ }
+}
diff --git a/src/com/android/launcher3/allapps/SearchRecyclerView.java b/src/com/android/launcher3/allapps/SearchRecyclerView.java
index 9d1dfc0de3..68f9f11d9f 100644
--- a/src/com/android/launcher3/allapps/SearchRecyclerView.java
+++ b/src/com/android/launcher3/allapps/SearchRecyclerView.java
@@ -27,8 +27,6 @@ import com.android.launcher3.views.RecyclerViewFastScroller;
/** A RecyclerView for AllApps Search results. */
public class SearchRecyclerView extends AllAppsRecyclerView {
- private Consumer<View> mChildAttachedConsumer;
-
public SearchRecyclerView(Context context) {
this(context, null);
}
@@ -46,11 +44,6 @@ public class SearchRecyclerView extends AllAppsRecyclerView {
super(context, attrs, defStyleAttr, defStyleRes);
}
- /** This will be called just before a new child is attached to the window. */
- public void setChildAttachedConsumer(Consumer<View> childAttachedConsumer) {
- mChildAttachedConsumer = childAttachedConsumer;
- }
-
@Override
protected void updatePoolSize() {
RecycledViewPool pool = getRecycledViewPool();
@@ -67,12 +60,4 @@ public class SearchRecyclerView extends AllAppsRecyclerView {
public RecyclerViewFastScroller getScrollbar() {
return null;
}
-
- @Override
- public void onChildAttachedToWindow(@NonNull View child) {
- if (mChildAttachedConsumer != null) {
- mChildAttachedConsumer.accept(child);
- }
- super.onChildAttachedToWindow(child);
- }
}
diff --git a/src/com/android/launcher3/allapps/SearchTransitionController.java b/src/com/android/launcher3/allapps/SearchTransitionController.java
index eb1bc0a4be..d5c3b57788 100644
--- a/src/com/android/launcher3/allapps/SearchTransitionController.java
+++ b/src/com/android/launcher3/allapps/SearchTransitionController.java
@@ -18,34 +18,21 @@ package com.android.launcher3.allapps;
import static android.view.View.VISIBLE;
-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.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.model.data.ItemInfo;
/** Coordinates the transition between Search and A-Z in All Apps. */
-public class SearchTransitionController {
-
- private static final String LOG_TAG = "SearchTransitionCtrl";
+public class SearchTransitionController extends RecyclerViewAnimationController {
// Interpolator when the user taps the QSB while already in All Apps.
private static final Interpolator INTERPOLATOR_WITHIN_ALL_APPS = DECELERATE_1_7;
@@ -53,42 +40,10 @@ public class SearchTransitionController {
// happening simultaneously.
private static final Interpolator INTERPOLATOR_TRANSITIONING_TO_ALL_APPS = INSTANT;
- /**
- * These values represent points on the [0, 1] animation progress spectrum. They are used to
- * animate items in the {@link SearchRecyclerView}.
- */
- private static final float TOP_CONTENT_FADE_PROGRESS_START = 0.133f;
- private static final float CONTENT_FADE_PROGRESS_DURATION = 0.083f;
- private static final float TOP_BACKGROUND_FADE_PROGRESS_START = 0.633f;
- private static final float BACKGROUND_FADE_PROGRESS_DURATION = 0.15f;
- private static final float CONTENT_STAGGER = 0.01f; // Progress before next item starts fading.
-
- private static final FloatProperty<SearchTransitionController> SEARCH_TO_AZ_PROGRESS =
- new FloatProperty<SearchTransitionController>("searchToAzProgress") {
- @Override
- public Float get(SearchTransitionController controller) {
- return controller.getSearchToAzProgress();
- }
-
- @Override
- public void setValue(SearchTransitionController controller, float progress) {
- controller.setSearchToAzProgress(progress);
- }
- };
-
- private final ActivityAllAppsContainerView<?> mAllAppsContainerView;
-
- private ObjectAnimator mSearchToAzAnimator = null;
- private float mSearchToAzProgress = 1f;
private boolean mSkipNextAnimationWithinAllApps;
public SearchTransitionController(ActivityAllAppsContainerView<?> allAppsContainerView) {
- mAllAppsContainerView = allAppsContainerView;
- }
-
- /** Returns true if a transition animation is currently in progress. */
- public boolean isRunning() {
- return mSearchToAzAnimator != null;
+ super(allAppsContainerView);
}
/**
@@ -101,51 +56,31 @@ public class SearchTransitionController {
* @param onEndRunnable will be called when the animation finishes, unless another animation is
* scheduled in the meantime
*/
- public void animateToSearchState(boolean goingToSearch, long duration, Runnable onEndRunnable) {
- float targetProgress = goingToSearch ? 0 : 1;
-
- if (mSearchToAzAnimator != null) {
- mSearchToAzAnimator.cancel();
- }
-
- mSearchToAzAnimator = ObjectAnimator.ofFloat(this, SEARCH_TO_AZ_PROGRESS, targetProgress);
- boolean inAllApps = mAllAppsContainerView.isInAllApps();
- TimeInterpolator timeInterpolator =
- inAllApps ? INTERPOLATOR_WITHIN_ALL_APPS : INTERPOLATOR_TRANSITIONING_TO_ALL_APPS;
- if (mSkipNextAnimationWithinAllApps) {
- timeInterpolator = INSTANT;
- mSkipNextAnimationWithinAllApps = false;
- }
- if (timeInterpolator == INSTANT) {
- duration = 0; // Don't want to animate when coming from QSB.
- }
- mSearchToAzAnimator.setDuration(duration).setInterpolator(timeInterpolator);
- mSearchToAzAnimator.addListener(forEndCallback(() -> mSearchToAzAnimator = null));
+ @Override
+ protected void animateToState(boolean goingToSearch, long duration, Runnable onEndRunnable) {
+ super.animateToState(goingToSearch, duration, onEndRunnable);
if (!goingToSearch) {
- mSearchToAzAnimator.addListener(forSuccessCallback(() -> {
+ mAnimator.addListener(forSuccessCallback(() -> {
mAllAppsContainerView.getFloatingHeaderView().setFloatingRowsCollapsed(false);
mAllAppsContainerView.getFloatingHeaderView().reset(false /* animate */);
mAllAppsContainerView.getAppsRecyclerViewContainer().setTranslationY(0);
}));
}
- mSearchToAzAnimator.addListener(forSuccessCallback(onEndRunnable));
-
mAllAppsContainerView.getFloatingHeaderView().setFloatingRowsCollapsed(true);
mAllAppsContainerView.getFloatingHeaderView().setVisibility(VISIBLE);
mAllAppsContainerView.getFloatingHeaderView().maybeSetTabVisibility(VISIBLE);
mAllAppsContainerView.getAppsRecyclerViewContainer().setVisibility(VISIBLE);
- getSearchRecyclerView().setVisibility(VISIBLE);
- getSearchRecyclerView().setChildAttachedConsumer(this::onSearchChildAttached);
- mSearchToAzAnimator.start();
+ getRecyclerView().setVisibility(VISIBLE);
}
- private SearchRecyclerView getSearchRecyclerView() {
+ @Override
+ protected SearchRecyclerView getRecyclerView() {
return mAllAppsContainerView.getSearchRecyclerView();
}
- private void setSearchToAzProgress(float searchToAzProgress) {
- mSearchToAzProgress = searchToAzProgress;
- int searchHeight = updateSearchRecyclerViewProgress();
+ @Override
+ protected int onProgressUpdated(float searchToAzProgress) {
+ int searchHeight = super.onProgressUpdated(searchToAzProgress);
FloatingHeaderView headerView = mAllAppsContainerView.getFloatingHeaderView();
@@ -171,179 +106,27 @@ public class SearchTransitionController {
appsContainer.setTranslationY(appsTranslationY);
// Fade apps out with tabs (in 20% of the total animation).
appsContainer.setAlpha(clampToProgress(searchToAzProgress, 0.8f, 1f));
+ return searchHeight;
}
/**
- * Updates the children views of SearchRecyclerView based on the current animation progress.
- *
- * @return the total height of animating views (excluding at most one row of app icons).
+ * Should only animate if the view is not an app icon or if the app row is complete.
*/
- private int updateSearchRecyclerViewProgress() {
- int numSearchResultsAnimated = 0;
- int totalHeight = 0;
- int appRowHeight = 0;
- boolean appRowComplete = false;
- Integer top = null;
- SearchRecyclerView searchRecyclerView = getSearchRecyclerView();
-
- for (int i = 0; i < searchRecyclerView.getChildCount(); i++) {
- View searchResultView = searchRecyclerView.getChildAt(i);
- if (searchResultView == null) {
- continue;
- }
-
- if (top == null) {
- top = searchResultView.getTop();
- }
-
- int adapterPosition = searchRecyclerView.getChildAdapterPosition(searchResultView);
- int spanIndex = getSpanIndex(searchRecyclerView, adapterPosition);
- appRowComplete |= appRowHeight > 0 && spanIndex == 0;
- // We don't animate the first (currently only) app row we see, as that is assumed to be
- // predicted/prefix-matched apps.
- boolean shouldAnimate = !isAppIcon(searchResultView) || appRowComplete;
-
- float contentAlpha = 1f;
- float backgroundAlpha = 1f;
- if (shouldAnimate) {
- if (spanIndex > 0) {
- // Animate this item with the previous item on the same row.
- numSearchResultsAnimated--;
- }
-
- // Adjust content alpha based on start progress and stagger.
- float startContentFadeProgress = Math.max(0,
- TOP_CONTENT_FADE_PROGRESS_START
- - CONTENT_STAGGER * numSearchResultsAnimated);
- float endContentFadeProgress = Math.min(1,
- startContentFadeProgress + CONTENT_FADE_PROGRESS_DURATION);
- contentAlpha = 1 - clampToProgress(mSearchToAzProgress,
- startContentFadeProgress, endContentFadeProgress);
-
- // Adjust background (or decorator) alpha based on start progress and stagger.
- float startBackgroundFadeProgress = Math.max(0,
- TOP_BACKGROUND_FADE_PROGRESS_START
- - CONTENT_STAGGER * numSearchResultsAnimated);
- float endBackgroundFadeProgress = Math.min(1,
- startBackgroundFadeProgress + BACKGROUND_FADE_PROGRESS_DURATION);
- backgroundAlpha = 1 - clampToProgress(mSearchToAzProgress,
- startBackgroundFadeProgress, endBackgroundFadeProgress);
-
- numSearchResultsAnimated++;
- }
-
- Drawable background = searchResultView.getBackground();
- if (background != null
- && searchResultView instanceof ViewGroup
- && FeatureFlags.ENABLE_SEARCH_RESULT_BACKGROUND_DRAWABLES.get()) {
- searchResultView.setAlpha(1f);
-
- // Apply content alpha to each child, since the view needs to be fully opaque for
- // the background to show properly.
- ViewGroup searchResultViewGroup = (ViewGroup) searchResultView;
- for (int j = 0; j < searchResultViewGroup.getChildCount(); j++) {
- searchResultViewGroup.getChildAt(j).setAlpha(contentAlpha);
- }
-
- // Apply background alpha to the background drawable directly.
- background.setAlpha((int) (255 * backgroundAlpha));
- } else {
- searchResultView.setAlpha(contentAlpha);
-
- // Apply background alpha to decorator if possible.
- if (adapterPosition != NO_POSITION) {
- searchRecyclerView.getApps().getAdapterItems().get(adapterPosition)
- .setDecorationFillAlpha((int) (255 * backgroundAlpha));
- }
-
- // 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 - mSearchToAzProgress;
- }
- int scaledHeight = (int) (searchResultView.getHeight() * scaleY);
- searchResultView.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;
- }
- }
- searchResultView.setY(y);
- }
-
- return totalHeight - appRowHeight;
+ @Override
+ protected boolean shouldAnimate(View view, boolean hasDecorationInfo, boolean appRowComplete) {
+ return !isAppIcon(view) || appRowComplete;
}
- /** @return the column that the view at this position is found (0 assumed if indeterminate). */
- private int getSpanIndex(SearchRecyclerView searchRecyclerView, int adapterPosition) {
- if (adapterPosition == NO_POSITION) {
- Log.w(LOG_TAG, "Can't determine span index - child not found in adapter");
- return 0;
- }
- if (!(searchRecyclerView.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<?>) searchRecyclerView.getAdapter();
- return adapter.getSpanIndex(adapterPosition);
- }
-
- private boolean isAppIcon(View item) {
- return item instanceof BubbleTextView && item.getTag() instanceof ItemInfo
- && ((ItemInfo) item.getTag()).itemType == ITEM_TYPE_APPLICATION;
- }
-
- /** Called just before a child is attached to the SearchRecyclerView. */
- private void onSearchChildAttached(View child) {
- // Avoid allocating hardware layers for alpha changes.
- child.forceHasOverlappingRendering(false);
- child.setPivotY(0);
- if (mSearchToAzProgress > 0) {
- // Before the child is rendered, apply the animation including it to avoid flicker.
- updateSearchRecyclerViewProgress();
- } else {
- // Apply default states without processing the full layout.
- child.setAlpha(1);
- child.setScaleY(1);
- child.setTranslationY(0);
- int adapterPosition = getSearchRecyclerView().getChildAdapterPosition(child);
- if (adapterPosition != NO_POSITION) {
- getSearchRecyclerView().getApps().getAdapterItems().get(adapterPosition)
- .setDecorationFillAlpha(255);
- }
- if (child instanceof ViewGroup
- && FeatureFlags.ENABLE_SEARCH_RESULT_BACKGROUND_DRAWABLES.get()) {
- ViewGroup childGroup = (ViewGroup) child;
- for (int i = 0; i < childGroup.getChildCount(); i++) {
- childGroup.getChildAt(i).setAlpha(1f);
- }
- }
- if (child.getBackground() != null) {
- child.getBackground().setAlpha(255);
- }
+ @Override
+ protected TimeInterpolator getInterpolator() {
+ TimeInterpolator timeInterpolator =
+ mAllAppsContainerView.isInAllApps()
+ ? INTERPOLATOR_WITHIN_ALL_APPS : INTERPOLATOR_TRANSITIONING_TO_ALL_APPS;
+ if (mSkipNextAnimationWithinAllApps) {
+ timeInterpolator = INSTANT;
+ mSkipNextAnimationWithinAllApps = false;
}
- }
-
- private float getSearchToAzProgress() {
- return mSearchToAzProgress;
+ return timeInterpolator;
}
/**
diff --git a/src/com/android/launcher3/allapps/SectionDecorationHandler.java b/src/com/android/launcher3/allapps/SectionDecorationHandler.java
new file mode 100644
index 0000000000..f79b82cebb
--- /dev/null
+++ b/src/com/android/launcher3/allapps/SectionDecorationHandler.java
@@ -0,0 +1,206 @@
+/*
+ * 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 android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.InsetDrawable;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+
+import com.android.launcher3.R;
+import com.android.launcher3.util.Themes;
+
+public class SectionDecorationHandler {
+
+ protected final Path mTmpPath = new Path();
+ protected final RectF mTmpRect = new RectF();
+
+ protected final int mCornerGroupRadius;
+ protected final int mCornerResultRadius;
+ protected final RectF mBounds = new RectF();
+ protected final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ protected final int mFocusAlpha = 255; // main focused item alpha
+ protected int mFillColor; // grouping color
+ protected int mFocusColor; // main focused item color
+ protected float mFillSpacing;
+ protected int mInlineRadius;
+ protected Context mContext;
+ protected float[] mCorners;
+ protected int mFillAlpha;
+ protected boolean mIsTopLeftRound;
+ protected boolean mIsTopRightRound;
+ protected boolean mIsBottomLeftRound;
+ protected boolean mIsBottomRightRound;
+ protected boolean mIsBottomRound;
+ protected boolean mIsTopRound;
+
+ public SectionDecorationHandler(Context context, int fillAlpha, boolean isTopLeftRound,
+ boolean isTopRightRound, boolean isBottomLeftRound,
+ boolean isBottomRightRound) {
+
+ mContext = context;
+ mFillAlpha = fillAlpha;
+ mFocusColor = ContextCompat.getColor(context,
+ R.color.material_color_surface_bright); // UX recommended
+ mFillColor = ContextCompat.getColor(context,
+ R.color.material_color_surface_container_high); // UX recommended
+
+ mIsTopLeftRound = isTopLeftRound;
+ mIsTopRightRound = isTopRightRound;
+ mIsBottomLeftRound = isBottomLeftRound;
+ mIsBottomRightRound = isBottomRightRound;
+ mIsBottomRound = mIsBottomLeftRound && mIsBottomRightRound;
+ mIsTopRound = mIsTopLeftRound && mIsTopRightRound;
+
+ mCornerGroupRadius = context.getResources().getDimensionPixelSize(
+ R.dimen.all_apps_recycler_view_decorator_group_radius);
+ mCornerResultRadius = context.getResources().getDimensionPixelSize(
+ R.dimen.all_apps_recycler_view_decorator_result_radius);
+
+ mInlineRadius = 0;
+ mFillSpacing = 0;
+ initCorners();
+ }
+
+ protected void initCorners() {
+ mCorners = new float[]{
+ mIsTopLeftRound ? mCornerGroupRadius : 0,
+ mIsTopLeftRound ? mCornerGroupRadius : 0, // Top left radius in px
+ mIsTopRightRound ? mCornerGroupRadius : 0,
+ mIsTopRightRound ? mCornerGroupRadius : 0, // Top right radius in px
+ mIsBottomRightRound ? mCornerGroupRadius : 0,
+ mIsBottomRightRound ? mCornerGroupRadius : 0, // Bottom right
+ mIsBottomLeftRound ? mCornerGroupRadius : 0,
+ mIsBottomLeftRound ? mCornerGroupRadius : 0 // Bottom left
+ };
+ }
+
+ protected void setFillAlpha(int fillAlpha) {
+ mFillAlpha = fillAlpha;
+ mPaint.setAlpha(mFillAlpha);
+ }
+
+ protected void onFocusDraw(Canvas canvas, @Nullable View view) {
+ if (view == null) {
+ return;
+ }
+ mPaint.setColor(mFillColor);
+ mPaint.setAlpha(mFillAlpha);
+ int scaledHeight = (int) (view.getHeight() * view.getScaleY());
+ mBounds.set(view.getLeft(), view.getY(), view.getRight(), view.getY() + scaledHeight);
+ onDraw(canvas);
+ }
+
+ protected void onDraw(Canvas canvas) {
+ mTmpPath.reset();
+ mTmpRect.set(mBounds.left + mFillSpacing,
+ mBounds.top + mFillSpacing,
+ mBounds.right - mFillSpacing,
+ mBounds.bottom - mFillSpacing);
+ mTmpPath.addRoundRect(mTmpRect, mCorners, Path.Direction.CW);
+ canvas.drawPath(mTmpPath, mPaint);
+ }
+
+ /** Sets the right background drawable to the view based on the give decoration info. */
+ public void applyBackground(View view, Context context,
+ @Nullable SectionDecorationInfo decorationInfo, boolean isHighlighted) {
+ int inset = context.getResources().getDimensionPixelSize(
+ R.dimen.all_apps_recycler_view_decorator_padding);
+ float radiusBottom = (decorationInfo == null || decorationInfo.isBottomRound()) ?
+ mCornerGroupRadius : mCornerResultRadius;
+ float radiusTop =
+ (decorationInfo == null || decorationInfo.isTopRound()) ?
+ mCornerGroupRadius : mCornerResultRadius;
+ int color = isHighlighted ? mFocusColor : mFillColor;
+
+ GradientDrawable shape = new GradientDrawable();
+ shape.setShape(GradientDrawable.RECTANGLE);
+ shape.setCornerRadii(new float[] {
+ radiusTop, radiusTop, // top-left
+ radiusTop, radiusTop, // top-right
+ radiusBottom, radiusBottom, // bottom-right
+ radiusBottom, radiusBottom // bottom-left
+ });
+ shape.setColor(color);
+
+ // Setting the background resets the padding, so we cache it and reset it afterwards.
+ int paddingLeft = view.getPaddingLeft();
+ int paddingTop = view.getPaddingTop();
+ int paddingRight = view.getPaddingRight();
+ int paddingBottom = view.getPaddingBottom();
+
+ view.setBackground(new InsetDrawable(shape, inset));
+
+ view.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
+ }
+
+ /**
+ * Section decorator that combines views and draws a single block decoration
+ */
+ public static class UnionDecorationHandler extends SectionDecorationHandler {
+
+ private final int mPaddingLeft;
+ private final int mPaddingRight;
+
+ public UnionDecorationHandler(
+ SectionDecorationHandler decorationHandler,
+ int paddingLeft, int paddingRight) {
+ super(decorationHandler.mContext, decorationHandler.mFillAlpha,
+ decorationHandler.mIsTopLeftRound, decorationHandler.mIsTopRightRound,
+ decorationHandler.mIsBottomLeftRound, decorationHandler.mIsBottomRightRound);
+ mPaddingLeft = paddingLeft;
+ mPaddingRight = paddingRight;
+ }
+
+ /**
+ * Expands decoration bounds to include child {@link PrivateAppsSectionDecorator}
+ */
+ public void addChild(SectionDecorationHandler child, View view, boolean applyBackground) {
+ int scaledHeight = (int) (view.getHeight() * view.getScaleY());
+ mBounds.union(view.getLeft(), view.getY(),
+ view.getRight(), view.getY() + scaledHeight);
+ if (applyBackground) {
+ applyBackground(view, mContext, null, false);
+ }
+ mIsBottomRound |= child.mIsBottomRound;
+ mIsBottomLeftRound |= child.mIsBottomLeftRound;
+ mIsBottomRightRound |= child.mIsBottomRightRound;
+ mIsTopRound |= child.mIsTopRound;
+ mIsTopLeftRound |= child.mIsTopLeftRound;
+ mIsTopRightRound |= child.mIsTopRightRound;
+ }
+
+ /**
+ * Draws group decoration to canvas
+ */
+ public void onGroupDecorate(Canvas canvas) {
+ initCorners();
+ mBounds.left = mPaddingLeft;
+ mBounds.right = canvas.getWidth() - mPaddingRight;
+ mPaint.setColor(mFillColor);
+ mPaint.setAlpha(mFillAlpha);
+ onDraw(canvas);
+ }
+ }
+}
diff --git a/src/com/android/launcher3/allapps/SectionDecorationInfo.java b/src/com/android/launcher3/allapps/SectionDecorationInfo.java
new file mode 100644
index 0000000000..1fed2b654e
--- /dev/null
+++ b/src/com/android/launcher3/allapps/SectionDecorationInfo.java
@@ -0,0 +1,77 @@
+/*
+ * 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 android.content.Context;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+
+public class SectionDecorationInfo {
+
+ public static final int ROUND_NOTHING = 1 << 1;
+ public static final int ROUND_TOP_LEFT = 1 << 2;
+ public static final int ROUND_TOP_RIGHT = 1 << 3;
+ public static final int ROUND_BOTTOM_LEFT = 1 << 4;
+ public static final int ROUND_BOTTOM_RIGHT = 1 << 5;
+ public static final int DECORATOR_ALPHA = 255;
+
+ protected boolean mShouldDecorateItemsTogether;
+ private SectionDecorationHandler mDecorationHandler;
+ protected boolean mIsTopRound;
+ protected boolean mIsBottomRound;
+
+ public SectionDecorationInfo(Context context, int roundRegions, boolean decorateTogether) {
+ mDecorationHandler =
+ new SectionDecorationHandler(context, DECORATOR_ALPHA,
+ isFlagEnabled(roundRegions, ROUND_TOP_LEFT),
+ isFlagEnabled(roundRegions, ROUND_TOP_RIGHT),
+ isFlagEnabled(roundRegions, ROUND_BOTTOM_LEFT),
+ isFlagEnabled(roundRegions, ROUND_BOTTOM_RIGHT));
+ mShouldDecorateItemsTogether = decorateTogether;
+ mIsTopRound = isFlagEnabled(roundRegions, ROUND_TOP_LEFT) &&
+ isFlagEnabled(roundRegions, ROUND_TOP_RIGHT);
+ mIsBottomRound = isFlagEnabled(roundRegions, ROUND_BOTTOM_LEFT) &&
+ isFlagEnabled(roundRegions, ROUND_BOTTOM_RIGHT);
+ }
+
+ public SectionDecorationInfo(Context context, @NonNull Bundle target,
+ String targetLayoutType, @NonNull Bundle prevTarget, @NonNull Bundle nextTarget) {}
+
+ public SectionDecorationHandler getDecorationHandler() {
+ return mDecorationHandler;
+ }
+
+ private boolean isFlagEnabled(int canonicalFlag, int comparison) {
+ return (canonicalFlag & comparison) != 0;
+ }
+
+ /**
+ * Returns whether multiple {@link SectionDecorationInfo}s with the same sectionId should
+ * be grouped together.
+ */
+ public boolean shouldDecorateItemsTogether() {
+ return mShouldDecorateItemsTogether;
+ }
+
+ public boolean isTopRound() {
+ return mIsTopRound;
+ }
+
+ public boolean isBottomRound() {
+ return mIsBottomRound;
+ }
+}