Skip to content

Commit

Permalink
[Predictive Back][Search] Update SearchView to support predictive bac…
Browse files Browse the repository at this point in the history
…k when set up with SearchBar

PiperOrigin-RevId: 520613990
  • Loading branch information
dsn5ft committed Mar 30, 2023
1 parent b3f7b66 commit a4b6f46
Show file tree
Hide file tree
Showing 6 changed files with 446 additions and 40 deletions.
Expand Up @@ -38,6 +38,7 @@
public class ClippableRoundedCornerLayout extends FrameLayout {

@Nullable private Path path;
private float cornerRadius;

public ClippableRoundedCornerLayout(@NonNull Context context) {
super(context);
Expand Down Expand Up @@ -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);
}
Expand All @@ -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();
Expand Down
20 changes: 20 additions & 0 deletions lib/java/com/google/android/material/internal/ViewUtils.java
Expand Up @@ -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<View> getChildren(@Nullable View view) {
List<View> children = new ArrayList<>();
Expand Down
@@ -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;
}
}
Expand Up @@ -22,4 +22,7 @@

<dimen name="m3_back_progress_bottom_container_max_scale_x_distance">48dp</dimen>
<dimen name="m3_back_progress_bottom_container_max_scale_y_distance">24dp</dimen>

<dimen name="m3_back_progress_main_container_min_edge_gap">8dp</dimen>
<dimen name="m3_back_progress_main_container_max_translation_y">24dp</dimen>
</resources>

0 comments on commit a4b6f46

Please sign in to comment.