From a4b6f46f0cbeaf4b4eac421db492e8ba74f2f94a Mon Sep 17 00:00:00 2001 From: dsn5ft <1420597+dsn5ft@users.noreply.github.com> Date: Thu, 30 Mar 2023 12:45:22 +0000 Subject: [PATCH] [Predictive Back][Search] Update SearchView to support predictive back when set up with SearchBar PiperOrigin-RevId: 520613990 --- .../ClippableRoundedCornerLayout.java | 11 + .../android/material/internal/ViewUtils.java | 20 ++ .../MaterialMainContainerBackHelper.java | 225 ++++++++++++++++++ .../material/motion/res/values/dimens.xml | 3 + .../android/material/search/SearchView.java | 72 +++++- .../search/SearchViewAnimationHelper.java | 155 +++++++++--- 6 files changed, 446 insertions(+), 40 deletions(-) create mode 100644 lib/java/com/google/android/material/motion/MaterialMainContainerBackHelper.java diff --git a/lib/java/com/google/android/material/internal/ClippableRoundedCornerLayout.java b/lib/java/com/google/android/material/internal/ClippableRoundedCornerLayout.java index cdf852aaf0c..216b4519736 100644 --- a/lib/java/com/google/android/material/internal/ClippableRoundedCornerLayout.java +++ b/lib/java/com/google/android/material/internal/ClippableRoundedCornerLayout.java @@ -38,6 +38,7 @@ public class ClippableRoundedCornerLayout extends FrameLayout { @Nullable private Path path; + private float cornerRadius; public ClippableRoundedCornerLayout(@NonNull Context context) { super(context); @@ -66,9 +67,18 @@ protected void dispatchDraw(Canvas canvas) { public void resetClipBoundsAndCornerRadius() { path = null; + cornerRadius = 0f; invalidate(); } + public float getCornerRadius() { + return cornerRadius; + } + + public void updateCornerRadius(float cornerRadius) { + updateClipBoundsAndCornerRadius(getLeft(), getTop(), getRight(), getBottom(), cornerRadius); + } + public void updateClipBoundsAndCornerRadius(@NonNull Rect rect, float cornerRadius) { updateClipBoundsAndCornerRadius(rect.left, rect.top, rect.right, rect.bottom, cornerRadius); } @@ -82,6 +92,7 @@ public void updateClipBoundsAndCornerRadius(@NonNull RectF rectF, float cornerRa if (path == null) { path = new Path(); } + this.cornerRadius = cornerRadius; path.reset(); path.addRoundRect(rectF, cornerRadius, cornerRadius, Path.Direction.CW); path.close(); diff --git a/lib/java/com/google/android/material/internal/ViewUtils.java b/lib/java/com/google/android/material/internal/ViewUtils.java index 70bc3db6555..9200d80f4e7 100644 --- a/lib/java/com/google/android/material/internal/ViewUtils.java +++ b/lib/java/com/google/android/material/internal/ViewUtils.java @@ -131,6 +131,26 @@ public static Rect calculateRectFromBounds(@NonNull View view, int offsetY) { view.getLeft(), view.getTop() + offsetY, view.getRight(), view.getBottom() + offsetY); } + @NonNull + public static Rect calculateOffsetRectFromBounds(@NonNull View view, @NonNull View offsetView) { + int[] offsetViewAbsolutePosition = new int[2]; + offsetView.getLocationOnScreen(offsetViewAbsolutePosition); + int offsetViewAbsoluteLeft = offsetViewAbsolutePosition[0]; + int offsetViewAbsoluteTop = offsetViewAbsolutePosition[1]; + + int[] viewAbsolutePosition = new int[2]; + view.getLocationOnScreen(viewAbsolutePosition); + int viewAbsoluteLeft = viewAbsolutePosition[0]; + int viewAbsoluteTop = viewAbsolutePosition[1]; + + int fromLeft = offsetViewAbsoluteLeft - viewAbsoluteLeft; + int fromTop = offsetViewAbsoluteTop - viewAbsoluteTop; + int fromRight = fromLeft + offsetView.getWidth(); + int fromBottom = fromTop + offsetView.getHeight(); + + return new Rect(fromLeft, fromTop, fromRight, fromBottom); + } + @NonNull public static List getChildren(@Nullable View view) { List children = new ArrayList<>(); diff --git a/lib/java/com/google/android/material/motion/MaterialMainContainerBackHelper.java b/lib/java/com/google/android/material/motion/MaterialMainContainerBackHelper.java new file mode 100644 index 00000000000..19747013a9b --- /dev/null +++ b/lib/java/com/google/android/material/motion/MaterialMainContainerBackHelper.java @@ -0,0 +1,225 @@ +/* + * Copyright 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.google.android.material.motion; + +import com.google.android.material.R; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; +import static com.google.android.material.animation.AnimationUtils.lerp; +import static java.lang.Math.max; +import static java.lang.Math.min; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.content.res.Resources; +import android.graphics.Rect; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.view.RoundedCorner; +import android.view.View; +import android.view.WindowInsets; +import android.window.BackEvent; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.RestrictTo; +import androidx.annotation.VisibleForTesting; +import com.google.android.material.animation.AnimationUtils; +import com.google.android.material.internal.ClippableRoundedCornerLayout; +import com.google.android.material.internal.ViewUtils; + +/** + * Utility class for main container views usually filling the entire screen (e.g., search view) that + * support back progress animations. + * + * @hide + */ +@RestrictTo(LIBRARY_GROUP) +public class MaterialMainContainerBackHelper extends MaterialBackAnimationHelper { + + private static final float MIN_SCALE = 0.9f; + + private final float minEdgeGap; + private final float maxTranslationY; + + private float initialTouchY; + @Nullable private Rect initialHideToClipBounds; + @Nullable private Rect initialHideFromClipBounds; + @Nullable private Integer deviceCornerRadius; + + public MaterialMainContainerBackHelper(@NonNull View view) { + super(view); + + Resources resources = view.getResources(); + minEdgeGap = resources.getDimension(R.dimen.m3_back_progress_main_container_min_edge_gap); + maxTranslationY = + resources.getDimension(R.dimen.m3_back_progress_main_container_max_translation_y); + } + + @Nullable + public Rect getInitialHideToClipBounds() { + return initialHideToClipBounds; + } + + @Nullable + public Rect getInitialHideFromClipBounds() { + return initialHideFromClipBounds; + } + + @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) + public void startBackProgress(@NonNull BackEvent backEvent, @NonNull View collapsedView) { + super.onStartBackProgress(backEvent); + + startBackProgress(backEvent.getTouchY(), collapsedView); + } + + @VisibleForTesting + @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) + public void startBackProgress(float touchY, @NonNull View collapsedView) { + collapsedView.setVisibility(View.INVISIBLE); + + initialHideToClipBounds = ViewUtils.calculateRectFromBounds(view); + initialHideFromClipBounds = ViewUtils.calculateOffsetRectFromBounds(view, collapsedView); + initialTouchY = touchY; + } + + @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) + public void updateBackProgress(@NonNull BackEvent backEvent, float collapsedCornerSize) { + super.onUpdateBackProgress(backEvent); + + boolean leftSwipeEdge = backEvent.getSwipeEdge() == BackEvent.EDGE_LEFT; + updateBackProgress( + backEvent.getProgress(), leftSwipeEdge, backEvent.getTouchY(), collapsedCornerSize); + } + + @VisibleForTesting + @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) + public void updateBackProgress( + float progress, boolean leftSwipeEdge, float touchY, float collapsedCornerSize) { + float width = view.getWidth(); + float height = view.getHeight(); + float scale = lerp(1, MIN_SCALE, progress); + + float availableHorizontalSpace = max(0, (width - MIN_SCALE * width) / 2 - minEdgeGap); + float translationX = lerp(0, availableHorizontalSpace, progress) * (leftSwipeEdge ? 1 : -1); + + float availableVerticalSpace = max(0, (height - scale * height) / 2 - minEdgeGap); + float maxTranslationY = min(availableVerticalSpace, this.maxTranslationY); + float yDelta = touchY - initialTouchY; + float yProgress = Math.abs(yDelta) / height; + float translationYDirection = Math.signum(yDelta); + float translationY = AnimationUtils.lerp(0, maxTranslationY, yProgress) * translationYDirection; + + view.setScaleX(scale); + view.setScaleY(scale); + view.setTranslationX(translationX); + view.setTranslationY(translationY); + if (view instanceof ClippableRoundedCornerLayout) { + ((ClippableRoundedCornerLayout) view) + .updateCornerRadius(lerp(getDeviceCornerRadius(), collapsedCornerSize, progress)); + } + } + + @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) + public void finishBackProgress(long duration, @NonNull View collapsedView) { + AnimatorSet resetAnimator = createResetScaleAndTranslationAnimator(collapsedView); + resetAnimator.setDuration(duration); + resetAnimator.start(); + + resetInitialValues(); + } + + @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) + public void cancelBackProgress(@NonNull View collapsedView) { + super.onCancelBackProgress(); + + AnimatorSet cancelAnimatorSet = createResetScaleAndTranslationAnimator(collapsedView); + if (view instanceof ClippableRoundedCornerLayout) { + cancelAnimatorSet.playTogether(createCornerAnimator((ClippableRoundedCornerLayout) view)); + } + cancelAnimatorSet.setDuration(cancelDuration); + cancelAnimatorSet.start(); + + resetInitialValues(); + } + + private void resetInitialValues() { + initialTouchY = 0f; + initialHideToClipBounds = null; + initialHideFromClipBounds = null; + } + + @NonNull + private AnimatorSet createResetScaleAndTranslationAnimator(@NonNull View collapsedView) { + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether( + ObjectAnimator.ofFloat(view, View.SCALE_X, 1), + ObjectAnimator.ofFloat(view, View.SCALE_Y, 1), + ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 0), + ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, 0)); + animatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + collapsedView.setVisibility(View.VISIBLE); + } + }); + return animatorSet; + } + + @NonNull + private ValueAnimator createCornerAnimator( + ClippableRoundedCornerLayout clippableRoundedCornerLayout) { + ValueAnimator cornerAnimator = + ValueAnimator.ofFloat( + clippableRoundedCornerLayout.getCornerRadius(), getDeviceCornerRadius()); + cornerAnimator.addUpdateListener( + animation -> + clippableRoundedCornerLayout.updateCornerRadius((Float) animation.getAnimatedValue())); + return cornerAnimator; + } + + public int getDeviceCornerRadius() { + if (deviceCornerRadius == null) { + deviceCornerRadius = getMaxDeviceCornerRadius(); + } + return deviceCornerRadius; + } + + private int getMaxDeviceCornerRadius() { + if (VERSION.SDK_INT >= VERSION_CODES.S) { + final WindowInsets insets = view.getRootWindowInsets(); + if (insets != null) { + return max( + max( + getRoundedCornerRadius(insets, RoundedCorner.POSITION_TOP_LEFT), + getRoundedCornerRadius(insets, RoundedCorner.POSITION_TOP_RIGHT)), + max( + getRoundedCornerRadius(insets, RoundedCorner.POSITION_BOTTOM_LEFT), + getRoundedCornerRadius(insets, RoundedCorner.POSITION_BOTTOM_RIGHT))); + } + } + return 0; + } + + @RequiresApi(VERSION_CODES.S) + private int getRoundedCornerRadius(WindowInsets insets, int position) { + final RoundedCorner roundedCorner = insets.getRoundedCorner(position); + return roundedCorner != null ? roundedCorner.getRadius() : 0; + } +} diff --git a/lib/java/com/google/android/material/motion/res/values/dimens.xml b/lib/java/com/google/android/material/motion/res/values/dimens.xml index b3a666cf8aa..f4797d786de 100644 --- a/lib/java/com/google/android/material/motion/res/values/dimens.xml +++ b/lib/java/com/google/android/material/motion/res/values/dimens.xml @@ -22,4 +22,7 @@ 48dp 24dp + + 8dp + 24dp diff --git a/lib/java/com/google/android/material/search/SearchView.java b/lib/java/com/google/android/material/search/SearchView.java index 1c2fbf509a4..dc9146f21c8 100644 --- a/lib/java/com/google/android/material/search/SearchView.java +++ b/lib/java/com/google/android/material/search/SearchView.java @@ -48,6 +48,7 @@ import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.TextView; +import android.window.BackEvent; import androidx.annotation.DrawableRes; import androidx.annotation.MenuRes; import androidx.annotation.NonNull; @@ -57,8 +58,10 @@ import androidx.annotation.RestrictTo; import androidx.annotation.StringRes; import androidx.annotation.StyleRes; +import androidx.annotation.VisibleForTesting; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.graphics.drawable.DrawableCompat; +import androidx.core.os.BuildCompat; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.core.widget.TextViewCompat; @@ -73,6 +76,9 @@ import com.google.android.material.internal.ToolbarUtils; import com.google.android.material.internal.TouchObserverFrameLayout; import com.google.android.material.internal.ViewUtils; +import com.google.android.material.motion.MaterialBackHandler; +import com.google.android.material.motion.MaterialBackOrchestrator; +import com.google.android.material.motion.MaterialMainContainerBackHelper; import com.google.android.material.shape.MaterialShapeUtils; import java.util.HashMap; import java.util.LinkedHashSet; @@ -119,7 +125,8 @@ * */ @SuppressWarnings("RestrictTo") -public class SearchView extends FrameLayout implements CoordinatorLayout.AttachedBehavior { +public class SearchView extends FrameLayout implements CoordinatorLayout.AttachedBehavior, + MaterialBackHandler { private static final long TALKBACK_FOCUS_CHANGE_DELAY_MS = 100; private static final int DEF_STYLE_RES = R.style.Widget_Material3_SearchView; @@ -140,6 +147,8 @@ public class SearchView extends FrameLayout implements CoordinatorLayout.Attache private final boolean layoutInflated; private final SearchViewAnimationHelper searchViewAnimationHelper; + @NonNull private final MaterialBackOrchestrator backOrchestrator = + new MaterialBackOrchestrator(this); private final ElevationOverlayProvider elevationOverlayProvider; private final Set transitionListeners = new LinkedHashSet<>(); @@ -251,6 +260,58 @@ public CoordinatorLayout.Behavior getBehavior() { return new SearchView.Behavior(); } + @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) + @Override + public void startBackProgress(@NonNull BackEvent backEvent) { + if (isHiddenOrHiding() || searchBar == null) { + return; + } + searchViewAnimationHelper.startBackProgress(backEvent); + } + + @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) + @Override + public void updateBackProgress(@NonNull BackEvent backEvent) { + if (isHiddenOrHiding() || searchBar == null) { + return; + } + searchViewAnimationHelper.updateBackProgress(backEvent); + } + + @Override + public void handleBackInvoked() { + if (isHiddenOrHiding()) { + return; + } + + BackEvent backEvent = searchViewAnimationHelper.onHandleBackInvoked(); + if (searchBar == null || backEvent == null || !BuildCompat.isAtLeastU()) { + hide(); + return; + } + + searchViewAnimationHelper.finishBackProgress(); + } + + @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) + @Override + public void cancelBackProgress() { + if (isHiddenOrHiding() || searchBar == null) { + return; + } + searchViewAnimationHelper.cancelBackProgress(); + } + + @VisibleForTesting + MaterialMainContainerBackHelper getBackHelper() { + return searchViewAnimationHelper.getBackHelper(); + } + + private boolean isHiddenOrHiding() { + return currentTransitionState.equals(TransitionState.HIDDEN) + || currentTransitionState.equals(TransitionState.HIDING); + } + @Nullable private Window getActivityWindow() { Activity activity = ContextUtils.getActivity(getContext()); @@ -717,6 +778,15 @@ void setTransitionState(@NonNull TransitionState state) { for (TransitionListener listener : listeners) { listener.onStateChanged(this, previousState, state); } + + // Only automatically handle back if we have a search bar to collapse to. + if (searchBar != null) { + if (state.equals(TransitionState.SHOWN)) { + backOrchestrator.startListeningForBackCallbacks(); + } else if (state.equals(TransitionState.HIDDEN)) { + backOrchestrator.stopListeningForBackCallbacks(); + } + } } /** Returns whether the {@link SearchView}'s main content view is shown or showing. */ diff --git a/lib/java/com/google/android/material/search/SearchViewAnimationHelper.java b/lib/java/com/google/android/material/search/SearchViewAnimationHelper.java index 18858d13b37..431705c87a0 100644 --- a/lib/java/com/google/android/material/search/SearchViewAnimationHelper.java +++ b/lib/java/com/google/android/material/search/SearchViewAnimationHelper.java @@ -16,6 +16,9 @@ package com.google.android.material.search; +import static com.google.android.material.animation.AnimationUtils.lerp; +import static java.lang.Math.max; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; @@ -23,6 +26,7 @@ import android.animation.ValueAnimator; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.os.Build.VERSION_CODES; import androidx.appcompat.graphics.drawable.DrawerArrowDrawable; import androidx.appcompat.widget.ActionMenuView; import androidx.appcompat.widget.Toolbar; @@ -33,6 +37,10 @@ import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.TextView; +import android.window.BackEvent; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.core.graphics.drawable.DrawableCompat; import androidx.core.view.MarginLayoutParamsCompat; import androidx.core.view.ViewCompat; @@ -46,6 +54,8 @@ import com.google.android.material.internal.ToolbarUtils; import com.google.android.material.internal.TouchObserverFrameLayout; import com.google.android.material.internal.ViewUtils; +import com.google.android.material.motion.MaterialMainContainerBackHelper; +import com.google.errorprone.annotations.CanIgnoreReturnValue; /** Helper class for {@link SearchView} animations. */ @SuppressWarnings("RestrictTo") @@ -89,6 +99,9 @@ class SearchViewAnimationHelper { private final View divider; private final TouchObserverFrameLayout contentContainer; + private final MaterialMainContainerBackHelper backHelper; + @Nullable private AnimatorSet backProgressAnimatorSet; + private SearchBar searchBar; SearchViewAnimationHelper(SearchView searchView) { @@ -104,6 +117,8 @@ class SearchViewAnimationHelper { this.clearButton = searchView.clearButton; this.divider = searchView.divider; this.contentContainer = searchView.contentContainer; + + backHelper = new MaterialMainContainerBackHelper(rootView); } void setSearchBar(SearchBar searchBar) { @@ -118,11 +133,12 @@ void show() { } } - void hide() { + @CanIgnoreReturnValue + AnimatorSet hide() { if (searchBar != null) { - startHideAnimationCollapse(); + return startHideAnimationCollapse(); } else { - startHideAnimationTranslate(); + return startHideAnimationTranslate(); } } @@ -158,7 +174,7 @@ public void onAnimationEnd(Animator animation) { }); } - private void startHideAnimationCollapse() { + private AnimatorSet startHideAnimationCollapse() { if (searchView.isAdjustNothingSoftInputMode()) { searchView.clearFocusAndHideKeyboard(); } @@ -180,6 +196,7 @@ public void onAnimationEnd(Animator animation) { } }); animatorSet.start(); + return animatorSet; } private void startShowAnimationTranslate() { @@ -213,7 +230,7 @@ public void onAnimationEnd(Animator animation) { }); } - private void startHideAnimationTranslate() { + private AnimatorSet startHideAnimationTranslate() { if (searchView.isAdjustNothingSoftInputMode()) { searchView.clearFocusAndHideKeyboard(); } @@ -235,6 +252,7 @@ public void onAnimationEnd(Animator animation) { } }); animatorSet.start(); + return animatorSet; } private AnimatorSet getTranslateAnimatorSet(boolean show) { @@ -255,12 +273,16 @@ private Animator getTranslationYAnimator() { private AnimatorSet getExpandCollapseAnimatorSet(boolean show) { AnimatorSet animatorSet = new AnimatorSet(); + boolean backProgress = backProgressAnimatorSet != null; + if (!backProgress) { + animatorSet.playTogether( + getButtonsProgressAnimator(show), getButtonsTranslationAnimator(show)); + } animatorSet.playTogether( getScrimAlphaAnimator(show), getRootViewAnimator(show), getClearButtonAnimator(show), getContentAnimator(show), - getButtonsAnimator(show), getHeaderContainerAnimator(show), getDummyToolbarAnimator(show), getActionMenuViewsAlphaAnimator(show), @@ -276,11 +298,9 @@ public void onAnimationStart(Animator animation) { @Override public void onAnimationEnd(Animator animation) { setContentViewsAlpha(show ? 1 : 0); - if (show) { - // After expanding, we should reset the clip bounds so it can react to screen or - // layout changes. Otherwise it will result in wrong clipping on the layout. - rootView.resetClipBoundsAndCornerRadius(); - } + // After expanding or collapsing, we should reset the clip bounds so it can react to the + // screen or layout changes. Otherwise it will result in wrong clipping on the layout. + rootView.resetClipBoundsAndCornerRadius(); } }); return animatorSet; @@ -314,17 +334,27 @@ private Animator getScrimAlphaAnimator(boolean show) { } private Animator getRootViewAnimator(boolean show) { - Rect toClipBounds = ViewUtils.calculateRectFromBounds(searchView); - Rect fromClipBounds = calculateFromClipBounds(); + Rect initialHideToClipBounds = backHelper.getInitialHideToClipBounds(); + Rect initialHideFromClipBounds = backHelper.getInitialHideFromClipBounds(); + Rect toClipBounds = + initialHideToClipBounds != null + ? initialHideToClipBounds + : ViewUtils.calculateRectFromBounds(rootView); + Rect fromClipBounds = + initialHideFromClipBounds != null + ? initialHideFromClipBounds + : ViewUtils.calculateOffsetRectFromBounds(rootView, searchBar); Rect clipBounds = new Rect(fromClipBounds); - float initialCornerRadius = searchBar.getCornerSize(); + float fromCornerRadius = searchBar.getCornerSize(); + float toCornerRadius = max(rootView.getCornerRadius(), backHelper.getDeviceCornerRadius()); ValueAnimator animator = ValueAnimator.ofObject(new RectEvaluator(clipBounds), fromClipBounds, toClipBounds); animator.addUpdateListener( valueAnimator -> { - float cornerRadius = initialCornerRadius * (1 - valueAnimator.getAnimatedFraction()); + float cornerRadius = + lerp(fromCornerRadius, toCornerRadius, valueAnimator.getAnimatedFraction()); rootView.updateClipBoundsAndCornerRadius(clipBounds, cornerRadius); }); animator.setDuration(show ? SHOW_DURATION_MS : HIDE_DURATION_MS); @@ -333,26 +363,6 @@ private Animator getRootViewAnimator(boolean show) { return animator; } - private Rect calculateFromClipBounds() { - int[] searchBarAbsolutePosition = new int[2]; - searchBar.getLocationOnScreen(searchBarAbsolutePosition); - int searchBarAbsoluteLeft = searchBarAbsolutePosition[0]; - int searchBarAbsoluteTop = searchBarAbsolutePosition[1]; - - // Use rootView to handle potential fitsSystemWindows padding applied to parent searchView. - int[] searchViewAbsolutePosition = new int[2]; - rootView.getLocationOnScreen(searchViewAbsolutePosition); - int searchViewAbsoluteLeft = searchViewAbsolutePosition[0]; - int searchViewAbsoluteTop = searchViewAbsolutePosition[1]; - - int fromLeft = searchBarAbsoluteLeft - searchViewAbsoluteLeft; - int fromTop = searchBarAbsoluteTop - searchViewAbsoluteTop; - int fromRight = fromLeft + searchBar.getWidth(); - int fromBottom = fromTop + searchBar.getHeight(); - - return new Rect(fromLeft, fromTop, fromRight, fromBottom); - } - private Animator getClearButtonAnimator(boolean show) { ValueAnimator animator = ValueAnimator.ofFloat(0, 1); animator.setDuration( @@ -365,10 +375,18 @@ private Animator getClearButtonAnimator(boolean show) { return animator; } - private Animator getButtonsAnimator(boolean show) { + private AnimatorSet getButtonsProgressAnimator(boolean show) { AnimatorSet animatorSet = new AnimatorSet(); - addBackButtonTranslationAnimatorIfNeeded(animatorSet); addBackButtonProgressAnimatorIfNeeded(animatorSet); + animatorSet.setDuration(show ? SHOW_DURATION_MS : HIDE_DURATION_MS); + animatorSet.setInterpolator( + ReversableAnimatedValueInterpolator.of(show, AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR)); + return animatorSet; + } + + private AnimatorSet getButtonsTranslationAnimator(boolean show) { + AnimatorSet animatorSet = new AnimatorSet(); + addBackButtonTranslationAnimatorIfNeeded(animatorSet); addActionMenuViewAnimatorIfNeeded(animatorSet); animatorSet.setDuration(show ? SHOW_DURATION_MS : HIDE_DURATION_MS); animatorSet.setInterpolator( @@ -412,7 +430,7 @@ private void addDrawerArrowDrawableAnimatorIfNeeded(AnimatorSet animatorSet, Dra DrawerArrowDrawable drawerArrowDrawable = (DrawerArrowDrawable) drawable; ValueAnimator animator = ValueAnimator.ofFloat(0, 1); animator.addUpdateListener( - animation -> drawerArrowDrawable.setProgress(animation.getAnimatedFraction())); + animation -> drawerArrowDrawable.setProgress((Float) animation.getAnimatedValue())); animatorSet.playTogether(animator); } } @@ -422,7 +440,7 @@ private void addFadeThroughDrawableAnimatorIfNeeded(AnimatorSet animatorSet, Dra FadeThroughDrawable fadeThroughDrawable = (FadeThroughDrawable) drawable; ValueAnimator animator = ValueAnimator.ofFloat(0, 1); animator.addUpdateListener( - animation -> fadeThroughDrawable.setProgress(animation.getAnimatedFraction())); + animation -> fadeThroughDrawable.setProgress((Float) animation.getAnimatedValue())); animatorSet.playTogether(animator); } } @@ -593,4 +611,63 @@ private void setMenuItemsNotClickable(Toolbar toolbar) { } } } + + @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) + void startBackProgress(@NonNull BackEvent backEvent) { + backHelper.startBackProgress(backEvent, searchBar); + + if (searchView.isAdjustNothingSoftInputMode()) { + searchView.clearFocusAndHideKeyboard(); + } + + // Start and immediately pause the animator set so that we can seek it with setCurrentPlayTime() + // in updateBackProgress() when the progress value changes. + backProgressAnimatorSet = getButtonsProgressAnimator(/* show= */ false); + backProgressAnimatorSet.start(); + backProgressAnimatorSet.pause(); + } + + @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) + public void updateBackProgress(@NonNull BackEvent backEvent) { + backHelper.updateBackProgress(backEvent, searchBar.getCornerSize()); + + if (backProgressAnimatorSet != null) { + backProgressAnimatorSet.setCurrentPlayTime( + (long) (backEvent.getProgress() * backProgressAnimatorSet.getDuration())); + } + } + + @Nullable + public BackEvent onHandleBackInvoked() { + return backHelper.onHandleBackInvoked(); + } + + @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) + public void finishBackProgress() { + AnimatorSet hideAnimatorSet = hide(); + long totalDuration = hideAnimatorSet.getTotalDuration(); + + backHelper.finishBackProgress(totalDuration, searchBar); + + if (backProgressAnimatorSet != null) { + getButtonsTranslationAnimator(/* show= */ false).start(); + backProgressAnimatorSet.resume(); + } + + backProgressAnimatorSet = null; + } + + @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) + public void cancelBackProgress() { + backHelper.cancelBackProgress(searchBar); + + if (backProgressAnimatorSet != null) { + backProgressAnimatorSet.reverse(); + } + backProgressAnimatorSet = null; + } + + MaterialMainContainerBackHelper getBackHelper() { + return backHelper; + } }