blob: 7f0a52c930afcd25b133a08de699c234c16308b1 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.autofill_assistant;
import android.animation.ValueAnimator;
import android.view.animation.DecelerateInterpolator;
import androidx.annotation.Nullable;
import org.chromium.base.Callback;
import org.chromium.base.MathUtils;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController.SheetState;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetObserver;
import org.chromium.components.browser_ui.bottomsheet.EmptyBottomSheetObserver;
import org.chromium.content_public.browser.GestureStateListenerWithScroll;
/**
* A Gesture listener that implements scroll-to-hide for the assistant bottomsheet when in FULL
* state.
*/
public class ScrollToHideGestureListener implements GestureStateListenerWithScroll {
/** Base duration of the animation of the sheet. 218 ms is a spec for material design. */
private static final int BASE_ANIMATION_DURATION_MS = 218;
private final BottomSheetController mBottomSheetController;
private final AssistantBottomSheetContent mContent;
@Nullable
private final BottomSheetObserver mStateChangeTracker = new StateChangeTracker();
private boolean mScrolling;
/** Remembers the last value of scroll offset, to compute the delta for the next move. */
private int mLastScrollOffsetY;
/**
* A capture of {@code mBottomSheetController.getCurrentOffset()}. At the end of a scroll, it is
* compared with the current value to figure out whether the sheet was overall scrolled up or
* down.
*/
private float mOffsetMarkPx;
/** This animator moves the sheet to its final position after scrolling ended. */
private ValueAnimator mAnimator;
public ScrollToHideGestureListener(
BottomSheetController bottomSheetController, AssistantBottomSheetContent content) {
mBottomSheetController = bottomSheetController;
mContent = content;
}
@Override
public void onScrollStarted(int scrollOffsetY, int scrollExtentY) {
Callback<Integer> offsetController = mContent.getOffsetController();
if (offsetController == null) return;
// Scroll to hide only applies if the sheet is fully opened, and state is FULL or is being
// opened, and target state is FULL.
if (mBottomSheetController.getTargetSheetState() == SheetState.FULL) {
// This stops animation and freezes the sheet in place.
offsetController.onResult(mBottomSheetController.getCurrentOffset());
}
if (mBottomSheetController.getSheetState() != SheetState.FULL) return;
resetScrollingState(); // also cancels any running animations
mScrolling = true;
mLastScrollOffsetY = scrollOffsetY;
mOffsetMarkPx = mBottomSheetController.getCurrentOffset();
mBottomSheetController.addObserver(mStateChangeTracker);
}
@Override
public void onScrollEnded(int scrollOffsetY, int scrollExtentY) {
onScrollOffsetOrExtentChanged(scrollOffsetY, scrollExtentY);
if (!mScrolling) return;
resetScrollingState();
int maxOffsetPx = getMaxOffsetPx();
int currentOffsetPx = mBottomSheetController.getCurrentOffset();
if (currentOffsetPx == 0 || currentOffsetPx == maxOffsetPx) {
return;
}
if (currentOffsetPx >= mOffsetMarkPx || scrollOffsetY == 0) {
animateTowards(maxOffsetPx);
} else {
animateTowards(0);
}
}
@Override
public void onScrollOffsetOrExtentChanged(int scrollOffsetY, int scrollExtentY) {
if (!mScrolling) {
// It's possible for the scroll offset to reset to 0 outside of a scroll, if the page or
// viewport size change. Scrolling up is not possible so if the sheet is hidden or about
// to be hidden, show it.
if (scrollOffsetY == 0 && mBottomSheetController.getSheetState() == SheetState.FULL
&& (mBottomSheetController.getCurrentOffset() == 0 || mAnimator != null)) {
animateTowards(getMaxOffsetPx());
}
return;
}
Callback<Integer> offsetController = mContent.getOffsetController();
if (offsetController == null) {
resetScrollingState();
return;
}
// deltaPx is the value to add to the current sheet offset (height). It is negative when
// scrolling down, that is, when scrollOffsetY increases.
int deltaPx = mLastScrollOffsetY - scrollOffsetY;
mLastScrollOffsetY = scrollOffsetY;
int maxOffsetPx = getMaxOffsetPx();
int offsetPx = MathUtils.clamp(
mBottomSheetController.getCurrentOffset() + deltaPx, 0, maxOffsetPx);
offsetController.onResult(offsetPx);
// If either extremes were reached, update the mark. The decision to fully show or hide will
// be relative to that point.
if (offsetPx == 0) {
mOffsetMarkPx = 0;
} else if (offsetPx >= maxOffsetPx) {
mOffsetMarkPx = maxOffsetPx;
}
}
@Override
public void onFlingStartGesture(int scrollOffsetY, int scrollExtentY) {
// Flinging and scrolling are handled the same, the sheet follows the movement of the
// browser page.
onScrollStarted(scrollOffsetY, scrollExtentY);
}
@Override
public void onFlingEndGesture(int scrollOffsetY, int scrollExtentY) {
onScrollEnded(scrollOffsetY, scrollExtentY);
}
@Override
public void onDestroyed() {
resetScrollingState();
}
private int getMaxOffsetPx() {
return mContent.getContentView().getHeight() + mBottomSheetController.getTopShadowHeight();
}
private void resetScrollingState() {
mScrolling = false;
mLastScrollOffsetY = 0;
cancelAnimation();
mBottomSheetController.removeObserver(mStateChangeTracker);
}
private void cancelAnimation() {
if (mAnimator == null) return;
mAnimator.cancel();
mAnimator = null;
}
/** Animate the sheet towards {@code goalOffsetPx} without changing its state. */
private void animateTowards(int goalOffsetPx) {
Callback<Integer> offsetController = mContent.getOffsetController();
if (offsetController == null) return;
ValueAnimator animator =
ValueAnimator.ofInt(mBottomSheetController.getCurrentOffset(), goalOffsetPx);
animator.setDuration(BASE_ANIMATION_DURATION_MS);
animator.setInterpolator(new DecelerateInterpolator(1.0f));
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animator) {
if (mAnimator != animator) return;
offsetController.onResult((Integer) animator.getAnimatedValue());
}
});
mAnimator = animator;
mAnimator.start();
}
/** Stop scrolling if the sheet leaves the FULL state during scrolling. */
private class StateChangeTracker extends EmptyBottomSheetObserver {
@Override
public void onSheetStateChanged(@SheetState int newState) {
if (newState != SheetState.FULL) {
resetScrollingState();
}
}
}
}