Skip to content

Commit a4b6f46

Browse files
committed
[Predictive Back][Search] Update SearchView to support predictive back when set up with SearchBar
PiperOrigin-RevId: 520613990
1 parent b3f7b66 commit a4b6f46

File tree

6 files changed

+446
-40
lines changed

6 files changed

+446
-40
lines changed

lib/java/com/google/android/material/internal/ClippableRoundedCornerLayout.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
public class ClippableRoundedCornerLayout extends FrameLayout {
3939

4040
@Nullable private Path path;
41+
private float cornerRadius;
4142

4243
public ClippableRoundedCornerLayout(@NonNull Context context) {
4344
super(context);
@@ -66,9 +67,18 @@ protected void dispatchDraw(Canvas canvas) {
6667

6768
public void resetClipBoundsAndCornerRadius() {
6869
path = null;
70+
cornerRadius = 0f;
6971
invalidate();
7072
}
7173

74+
public float getCornerRadius() {
75+
return cornerRadius;
76+
}
77+
78+
public void updateCornerRadius(float cornerRadius) {
79+
updateClipBoundsAndCornerRadius(getLeft(), getTop(), getRight(), getBottom(), cornerRadius);
80+
}
81+
7282
public void updateClipBoundsAndCornerRadius(@NonNull Rect rect, float cornerRadius) {
7383
updateClipBoundsAndCornerRadius(rect.left, rect.top, rect.right, rect.bottom, cornerRadius);
7484
}
@@ -82,6 +92,7 @@ public void updateClipBoundsAndCornerRadius(@NonNull RectF rectF, float cornerRa
8292
if (path == null) {
8393
path = new Path();
8494
}
95+
this.cornerRadius = cornerRadius;
8596
path.reset();
8697
path.addRoundRect(rectF, cornerRadius, cornerRadius, Path.Direction.CW);
8798
path.close();

lib/java/com/google/android/material/internal/ViewUtils.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,26 @@ public static Rect calculateRectFromBounds(@NonNull View view, int offsetY) {
131131
view.getLeft(), view.getTop() + offsetY, view.getRight(), view.getBottom() + offsetY);
132132
}
133133

134+
@NonNull
135+
public static Rect calculateOffsetRectFromBounds(@NonNull View view, @NonNull View offsetView) {
136+
int[] offsetViewAbsolutePosition = new int[2];
137+
offsetView.getLocationOnScreen(offsetViewAbsolutePosition);
138+
int offsetViewAbsoluteLeft = offsetViewAbsolutePosition[0];
139+
int offsetViewAbsoluteTop = offsetViewAbsolutePosition[1];
140+
141+
int[] viewAbsolutePosition = new int[2];
142+
view.getLocationOnScreen(viewAbsolutePosition);
143+
int viewAbsoluteLeft = viewAbsolutePosition[0];
144+
int viewAbsoluteTop = viewAbsolutePosition[1];
145+
146+
int fromLeft = offsetViewAbsoluteLeft - viewAbsoluteLeft;
147+
int fromTop = offsetViewAbsoluteTop - viewAbsoluteTop;
148+
int fromRight = fromLeft + offsetView.getWidth();
149+
int fromBottom = fromTop + offsetView.getHeight();
150+
151+
return new Rect(fromLeft, fromTop, fromRight, fromBottom);
152+
}
153+
134154
@NonNull
135155
public static List<View> getChildren(@Nullable View view) {
136156
List<View> children = new ArrayList<>();
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
/*
2+
* Copyright 2023 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.android.material.motion;
17+
18+
import com.google.android.material.R;
19+
20+
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
21+
import static com.google.android.material.animation.AnimationUtils.lerp;
22+
import static java.lang.Math.max;
23+
import static java.lang.Math.min;
24+
25+
import android.animation.Animator;
26+
import android.animation.AnimatorListenerAdapter;
27+
import android.animation.AnimatorSet;
28+
import android.animation.ObjectAnimator;
29+
import android.animation.ValueAnimator;
30+
import android.content.res.Resources;
31+
import android.graphics.Rect;
32+
import android.os.Build.VERSION;
33+
import android.os.Build.VERSION_CODES;
34+
import android.view.RoundedCorner;
35+
import android.view.View;
36+
import android.view.WindowInsets;
37+
import android.window.BackEvent;
38+
import androidx.annotation.NonNull;
39+
import androidx.annotation.Nullable;
40+
import androidx.annotation.RequiresApi;
41+
import androidx.annotation.RestrictTo;
42+
import androidx.annotation.VisibleForTesting;
43+
import com.google.android.material.animation.AnimationUtils;
44+
import com.google.android.material.internal.ClippableRoundedCornerLayout;
45+
import com.google.android.material.internal.ViewUtils;
46+
47+
/**
48+
* Utility class for main container views usually filling the entire screen (e.g., search view) that
49+
* support back progress animations.
50+
*
51+
* @hide
52+
*/
53+
@RestrictTo(LIBRARY_GROUP)
54+
public class MaterialMainContainerBackHelper extends MaterialBackAnimationHelper {
55+
56+
private static final float MIN_SCALE = 0.9f;
57+
58+
private final float minEdgeGap;
59+
private final float maxTranslationY;
60+
61+
private float initialTouchY;
62+
@Nullable private Rect initialHideToClipBounds;
63+
@Nullable private Rect initialHideFromClipBounds;
64+
@Nullable private Integer deviceCornerRadius;
65+
66+
public MaterialMainContainerBackHelper(@NonNull View view) {
67+
super(view);
68+
69+
Resources resources = view.getResources();
70+
minEdgeGap = resources.getDimension(R.dimen.m3_back_progress_main_container_min_edge_gap);
71+
maxTranslationY =
72+
resources.getDimension(R.dimen.m3_back_progress_main_container_max_translation_y);
73+
}
74+
75+
@Nullable
76+
public Rect getInitialHideToClipBounds() {
77+
return initialHideToClipBounds;
78+
}
79+
80+
@Nullable
81+
public Rect getInitialHideFromClipBounds() {
82+
return initialHideFromClipBounds;
83+
}
84+
85+
@RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
86+
public void startBackProgress(@NonNull BackEvent backEvent, @NonNull View collapsedView) {
87+
super.onStartBackProgress(backEvent);
88+
89+
startBackProgress(backEvent.getTouchY(), collapsedView);
90+
}
91+
92+
@VisibleForTesting
93+
@RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
94+
public void startBackProgress(float touchY, @NonNull View collapsedView) {
95+
collapsedView.setVisibility(View.INVISIBLE);
96+
97+
initialHideToClipBounds = ViewUtils.calculateRectFromBounds(view);
98+
initialHideFromClipBounds = ViewUtils.calculateOffsetRectFromBounds(view, collapsedView);
99+
initialTouchY = touchY;
100+
}
101+
102+
@RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
103+
public void updateBackProgress(@NonNull BackEvent backEvent, float collapsedCornerSize) {
104+
super.onUpdateBackProgress(backEvent);
105+
106+
boolean leftSwipeEdge = backEvent.getSwipeEdge() == BackEvent.EDGE_LEFT;
107+
updateBackProgress(
108+
backEvent.getProgress(), leftSwipeEdge, backEvent.getTouchY(), collapsedCornerSize);
109+
}
110+
111+
@VisibleForTesting
112+
@RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
113+
public void updateBackProgress(
114+
float progress, boolean leftSwipeEdge, float touchY, float collapsedCornerSize) {
115+
float width = view.getWidth();
116+
float height = view.getHeight();
117+
float scale = lerp(1, MIN_SCALE, progress);
118+
119+
float availableHorizontalSpace = max(0, (width - MIN_SCALE * width) / 2 - minEdgeGap);
120+
float translationX = lerp(0, availableHorizontalSpace, progress) * (leftSwipeEdge ? 1 : -1);
121+
122+
float availableVerticalSpace = max(0, (height - scale * height) / 2 - minEdgeGap);
123+
float maxTranslationY = min(availableVerticalSpace, this.maxTranslationY);
124+
float yDelta = touchY - initialTouchY;
125+
float yProgress = Math.abs(yDelta) / height;
126+
float translationYDirection = Math.signum(yDelta);
127+
float translationY = AnimationUtils.lerp(0, maxTranslationY, yProgress) * translationYDirection;
128+
129+
view.setScaleX(scale);
130+
view.setScaleY(scale);
131+
view.setTranslationX(translationX);
132+
view.setTranslationY(translationY);
133+
if (view instanceof ClippableRoundedCornerLayout) {
134+
((ClippableRoundedCornerLayout) view)
135+
.updateCornerRadius(lerp(getDeviceCornerRadius(), collapsedCornerSize, progress));
136+
}
137+
}
138+
139+
@RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
140+
public void finishBackProgress(long duration, @NonNull View collapsedView) {
141+
AnimatorSet resetAnimator = createResetScaleAndTranslationAnimator(collapsedView);
142+
resetAnimator.setDuration(duration);
143+
resetAnimator.start();
144+
145+
resetInitialValues();
146+
}
147+
148+
@RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
149+
public void cancelBackProgress(@NonNull View collapsedView) {
150+
super.onCancelBackProgress();
151+
152+
AnimatorSet cancelAnimatorSet = createResetScaleAndTranslationAnimator(collapsedView);
153+
if (view instanceof ClippableRoundedCornerLayout) {
154+
cancelAnimatorSet.playTogether(createCornerAnimator((ClippableRoundedCornerLayout) view));
155+
}
156+
cancelAnimatorSet.setDuration(cancelDuration);
157+
cancelAnimatorSet.start();
158+
159+
resetInitialValues();
160+
}
161+
162+
private void resetInitialValues() {
163+
initialTouchY = 0f;
164+
initialHideToClipBounds = null;
165+
initialHideFromClipBounds = null;
166+
}
167+
168+
@NonNull
169+
private AnimatorSet createResetScaleAndTranslationAnimator(@NonNull View collapsedView) {
170+
AnimatorSet animatorSet = new AnimatorSet();
171+
animatorSet.playTogether(
172+
ObjectAnimator.ofFloat(view, View.SCALE_X, 1),
173+
ObjectAnimator.ofFloat(view, View.SCALE_Y, 1),
174+
ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 0),
175+
ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, 0));
176+
animatorSet.addListener(new AnimatorListenerAdapter() {
177+
@Override
178+
public void onAnimationEnd(Animator animation) {
179+
collapsedView.setVisibility(View.VISIBLE);
180+
}
181+
});
182+
return animatorSet;
183+
}
184+
185+
@NonNull
186+
private ValueAnimator createCornerAnimator(
187+
ClippableRoundedCornerLayout clippableRoundedCornerLayout) {
188+
ValueAnimator cornerAnimator =
189+
ValueAnimator.ofFloat(
190+
clippableRoundedCornerLayout.getCornerRadius(), getDeviceCornerRadius());
191+
cornerAnimator.addUpdateListener(
192+
animation ->
193+
clippableRoundedCornerLayout.updateCornerRadius((Float) animation.getAnimatedValue()));
194+
return cornerAnimator;
195+
}
196+
197+
public int getDeviceCornerRadius() {
198+
if (deviceCornerRadius == null) {
199+
deviceCornerRadius = getMaxDeviceCornerRadius();
200+
}
201+
return deviceCornerRadius;
202+
}
203+
204+
private int getMaxDeviceCornerRadius() {
205+
if (VERSION.SDK_INT >= VERSION_CODES.S) {
206+
final WindowInsets insets = view.getRootWindowInsets();
207+
if (insets != null) {
208+
return max(
209+
max(
210+
getRoundedCornerRadius(insets, RoundedCorner.POSITION_TOP_LEFT),
211+
getRoundedCornerRadius(insets, RoundedCorner.POSITION_TOP_RIGHT)),
212+
max(
213+
getRoundedCornerRadius(insets, RoundedCorner.POSITION_BOTTOM_LEFT),
214+
getRoundedCornerRadius(insets, RoundedCorner.POSITION_BOTTOM_RIGHT)));
215+
}
216+
}
217+
return 0;
218+
}
219+
220+
@RequiresApi(VERSION_CODES.S)
221+
private int getRoundedCornerRadius(WindowInsets insets, int position) {
222+
final RoundedCorner roundedCorner = insets.getRoundedCorner(position);
223+
return roundedCorner != null ? roundedCorner.getRadius() : 0;
224+
}
225+
}

lib/java/com/google/android/material/motion/res/values/dimens.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,7 @@
2222

2323
<dimen name="m3_back_progress_bottom_container_max_scale_x_distance">48dp</dimen>
2424
<dimen name="m3_back_progress_bottom_container_max_scale_y_distance">24dp</dimen>
25+
26+
<dimen name="m3_back_progress_main_container_min_edge_gap">8dp</dimen>
27+
<dimen name="m3_back_progress_main_container_max_translation_y">24dp</dimen>
2528
</resources>

0 commit comments

Comments
 (0)