diff options
author | Brandon Dayauon <brdayauon@google.com> | 2023-12-13 00:00:14 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2023-12-13 00:00:14 +0000 |
commit | d7f0ccf52eb65df7e58d58b6201c3a0fc2723494 (patch) | |
tree | 6d65c2e4ff97c7d2601e54f2402ec32abfae4cd7 | |
parent | d48279bf1dfb4a6866cbdabc12a84703c438796d (diff) | |
parent | 763e40d74795d2f2ce772e418edb26e9a114ff9b (diff) | |
download | Launcher3-d7f0ccf52eb65df7e58d58b6201c3a0fc2723494.tar.gz |
Merge "Move UnionDecorationHandler to Launcher" into main
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; + } +} |