material-components_materia.../lib/java/com/google/android/material/motion/MaterialMainContainerBackHelper.java

226 lines
8.1 KiB
Java

/*
* 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;
}
}