mirror of
https://github.com/material-components/material-components-android.git
synced 2026-01-17 10:21:51 +08:00
226 lines
8.1 KiB
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;
|
|
}
|
|
}
|