Commit de245f04 authored by Stephane Zermatten's avatar Stephane Zermatten Committed by Chromium LUCI CQ

[Autofill Assistant] Add scroll-to-hide to JITT.

With this change when scroll-to-hide is enabled in the JITT UI,
scrolling down on the browser page hides the bottom sheet and scrolling
up reveals it again.

Change-Id: I92f9753b09d25443293e5a1b4cb23bc8c3cfb3d3
Bug: b/171792266
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2599864
Commit-Queue: Stephane Zermatten <szermatt@chromium.org>
Reviewed-by: default avatarClemens Arbesser <arbesser@google.com>
Cr-Commit-Position: refs/heads/master@{#842475}
parent e559586b
......@@ -115,6 +115,7 @@ android_library("java") {
"java/src/org/chromium/chrome/browser/autofill_assistant/DialogOnboardingCoordinator.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/FeedbackContext.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/LayoutUtils.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/ScrollToHideGestureListener.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/SizeListenableLinearLayout.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/carousel/AssistantActionsCarouselCoordinator.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/carousel/AssistantActionsDecoration.java",
......
......@@ -11,9 +11,11 @@ import android.widget.ScrollView;
import androidx.annotation.Nullable;
import org.chromium.base.Callback;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.autofill_assistant.R;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetContent;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
/**
* The {@link BottomSheetContent} for the Autofill Assistant. It supports notifying the
......@@ -27,6 +29,9 @@ public class AssistantBottomSheetContent implements BottomSheetContent {
private ScrollView mContentScrollableView;
private Supplier<AssistantBottomBarDelegate> mBottomBarDelegateSupplier;
private boolean mPeekModeDisabled;
private BottomSheetController mController;
@Nullable
private Callback<Integer> mOffsetController;
public AssistantBottomSheetContent(
Context context, Supplier<AssistantBottomBarDelegate> supplier) {
......@@ -151,4 +156,18 @@ public class AssistantBottomSheetContent implements BottomSheetContent {
return bottomBarDelegate.onBackButtonPressed();
}
@Override
public boolean contentControlsOffset() {
return true;
}
@Override
public void setOffsetController(Callback<Integer> offsetController) {
mOffsetController = offsetController;
}
public Callback<Integer> getOffsetController() {
return mOffsetController;
}
}
// 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();
}
}
}
}
......@@ -20,6 +20,7 @@ import org.chromium.chrome.browser.autofill_assistant.AssistantBottomSheetConten
import org.chromium.chrome.browser.autofill_assistant.AssistantRootViewContainer;
import org.chromium.chrome.browser.autofill_assistant.BottomSheetUtils;
import org.chromium.chrome.browser.autofill_assistant.LayoutUtils;
import org.chromium.chrome.browser.autofill_assistant.ScrollToHideGestureListener;
import org.chromium.chrome.browser.autofill_assistant.carousel.AssistantChip;
import org.chromium.chrome.browser.autofill_assistant.carousel.AssistantChipViewHolder;
import org.chromium.chrome.browser.autofill_assistant.generic_ui.AssistantDimension;
......@@ -30,6 +31,7 @@ import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController.StateChangeReason;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetObserver;
import org.chromium.components.browser_ui.bottomsheet.EmptyBottomSheetObserver;
import org.chromium.content_public.browser.GestureListenerManager;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.ApplicationViewportInsetSupplier;
......@@ -60,6 +62,7 @@ public class AssistantTriggerScript {
private AssistantHeaderCoordinator mHeaderCoordinator;
private AssistantHeaderModel mHeaderModel;
private ScrollToHideGestureListener mGestureListener;
private LinearLayout mChipsContainer;
private final int mInnerChipSpacing;
/** Height of the bottom sheet's shadow, used to compute the viewport resize offset. */
......@@ -152,6 +155,7 @@ public class AssistantTriggerScript {
}
public void destroy() {
disableScrollToHide();
mBottomSheetController.removeObserver(mBottomSheetObserver);
if (mHeaderCoordinator != null) {
mHeaderCoordinator.destroy();
......@@ -250,7 +254,7 @@ public class AssistantTriggerScript {
addChipsToContainer(mChipsContainer, mRightAlignedChips);
}
public boolean show(boolean resizeVisualViewport) {
public boolean show(boolean resizeVisualViewport, boolean scrollToHide) {
if (mHeaderModel == null || mHeaderCoordinator == null) {
assert false : "createHeaderAndGetModel() must be called before show()";
return false;
......@@ -262,10 +266,14 @@ public class AssistantTriggerScript {
mBottomSheetController.addObserver(mBottomSheetObserver);
BottomSheetUtils.showContentAndMaybeExpand(mBottomSheetController, mContent,
/* shouldExpand = */ true, /* animate = */ mAnimateBottomSheet);
if (scrollToHide) enableScrollToHide();
return true;
}
public void hide() {
disableScrollToHide();
mBottomSheetController.removeObserver(mBottomSheetObserver);
mBottomSheetController.hideContent(mContent, /* animate = */ mAnimateBottomSheet);
mResizeVisualViewport = false;
......@@ -297,4 +305,18 @@ public class AssistantTriggerScript {
mInsetSupplier.set(resizing);
}
private void disableScrollToHide() {
if (mGestureListener == null) return;
GestureListenerManager.fromWebContents(mWebContents).removeListener(mGestureListener);
mGestureListener = null;
}
private void enableScrollToHide() {
if (mGestureListener != null) return;
mGestureListener = new ScrollToHideGestureListener(mBottomSheetController, mContent);
GestureListenerManager.fromWebContents(mWebContents).addListener(mGestureListener);
}
}
......@@ -143,7 +143,7 @@ public class AssistantTriggerScriptBridge {
private boolean showTriggerScript(String[] cancelPopupMenuItems, int[] cancelPopupMenuActions,
List<AssistantChip> leftAlignedChips, int[] leftAlignedChipsActions,
List<AssistantChip> rightAlignedChips, int[] rightAlignedChipsActions,
boolean resizeVisualViewport) {
boolean resizeVisualViewport, boolean scrollToHide) {
// Trigger scripts currently do not support switching activities (such as CCT->tab).
// TODO(b/171776026): Re-inject dependencies on activity change to support CCT->tab.
if (TabUtils.getActivity(TabUtils.fromWebContents(mWebContents)) != mContext) {
......@@ -154,7 +154,7 @@ public class AssistantTriggerScriptBridge {
mTriggerScript.setCancelPopupMenu(cancelPopupMenuItems, cancelPopupMenuActions);
mTriggerScript.setLeftAlignedChips(leftAlignedChips, leftAlignedChipsActions);
mTriggerScript.setRightAlignedChips(rightAlignedChips, rightAlignedChipsActions);
boolean shown = mTriggerScript.show(resizeVisualViewport);
boolean shown = mTriggerScript.show(resizeVisualViewport, scrollToHide);
// A trigger script was displayed, users are no longer considered first-time users.
if (shown) {
......
......@@ -15,6 +15,8 @@ import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThan;
import static org.chromium.base.test.util.CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL;
import static org.chromium.chrome.browser.autofill_assistant.AutofillAssistantUiTestUtil.tapElement;
......@@ -35,6 +37,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.DisableIf;
import org.chromium.chrome.autofill_assistant.R;
......@@ -63,7 +66,12 @@ import org.chromium.chrome.browser.signin.services.UnifiedConsentServiceBridge;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.util.browser.Features;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.content_public.browser.GestureListenerManager;
import org.chromium.content_public.browser.GestureStateListener;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.content_public.browser.test.util.TouchCommon;
import org.chromium.net.test.EmbeddedTestServer;
import java.util.ArrayList;
......@@ -572,4 +580,80 @@ public class AutofillAssistantTriggerScriptIntegrationTest {
Espresso.pressBack();
waitUntilViewMatchesCondition(withText("Hello world"), isCompletelyDisplayed());
}
@Test
@MediumTest
@Features.EnableFeatures({ChromeFeatureList.AUTOFILL_ASSISTANT_DISABLE_ONBOARDING_FLOW,
ChromeFeatureList.AUTOFILL_ASSISTANT_PROACTIVE_HELP})
public void
testScrollToHide() throws Exception {
GetTriggerScriptsResponseProto triggerScripts =
GetTriggerScriptsResponseProto.newBuilder()
.addTriggerScripts(
TriggerScriptProto
.newBuilder()
/* no trigger condition */
.setUserInterface(createDefaultUI("Trigger script",
/* bubbleMessage = */ "",
/* withProgressBar = */ false)
.setScrollToHide(true)))
.build();
setupTriggerScripts(triggerScripts);
AutofillAssistantPreferencesUtil.setInitialPreferences(false);
startAutofillAssistantOnTab(TEST_PAGE_A);
waitUntilViewMatchesCondition(withText("Trigger script"), isCompletelyDisplayed());
BottomSheetController bottomSheetController =
AutofillAssistantUiTestUtil.getBottomSheetController(mTestRule.getActivity());
CallbackHelper waitForScroll = new CallbackHelper();
WebContents webContents = mTestRule.getWebContents();
TestThreadUtils.runOnUiThreadBlocking(() -> {
GestureListenerManager.fromWebContents(webContents)
.addListener(new GestureStateListener() {
@Override
public void onScrollEnded(int scrollOffsetY, int scrollExtentY) {
waitForScroll.notifyCalled();
}
});
});
int webContentX = TestThreadUtils.runOnUiThreadBlocking(
() -> webContents.getViewAndroidDelegate().getContainerView().getWidth() / 2);
int webContentY = TestThreadUtils.runOnUiThreadBlocking(
() -> webContents.getViewAndroidDelegate().getContainerView().getHeight() / 3);
int offsetBeforeScroll = TestThreadUtils.runOnUiThreadBlocking(
() -> bottomSheetController.getCurrentOffset());
assertThat(offsetBeforeScroll, greaterThan(0));
// Scroll more than the bottom sheet height, to be sure it's going to be completely hidden
// or shown due to the scroll.
int scrollDistance = (int) (bottomSheetController.getCurrentOffset() * 1.5f);
TouchCommon.performDrag(mTestRule.getActivity(), webContentX, webContentX, webContentY,
webContentY - scrollDistance,
/* stepCount*/ 10, /* duration in ms */ 250);
waitForScroll.waitForCallback("scroll down expected", /* currentCallCount= */ 0);
// After scroll down, the bottom sheet is completely hidden.
int offsetAfterScrollDown = TestThreadUtils.runOnUiThreadBlocking(
() -> bottomSheetController.getCurrentOffset());
Assert.assertEquals(0, offsetAfterScrollDown);
TouchCommon.performDrag(mTestRule.getActivity(), webContentX, webContentX, webContentY,
webContentY + scrollDistance, /* stepCount*/ 10, /* duration in ms */ 250);
waitForScroll.waitForCallback("scroll up expected", /* currentCallCount= */ 1);
// Wait until the bottom sheet is fully back on the screen again before capturing one last
// offset.
waitUntilViewMatchesCondition(withText("Trigger script"), isCompletelyDisplayed());
int offsetAfterScrollUp = TestThreadUtils.runOnUiThreadBlocking(
() -> bottomSheetController.getCurrentOffset());
// After scroll up, the bottom sheet is visible again.
Assert.assertEquals(offsetBeforeScroll, offsetAfterScrollUp);
}
}
......@@ -223,7 +223,7 @@ void TriggerScriptBridgeAndroid::OnTriggerScriptShown(
ToJavaIntArray(env, cancel_popup_actions), jleft_aligned_chips,
ToJavaIntArray(env, left_aligned_chip_actions), jright_aligned_chips,
ToJavaIntArray(env, right_aligned_chip_actions),
proto.resize_visual_viewport());
proto.resize_visual_viewport(), proto.scroll_to_hide());
trigger_script_coordinator_->OnTriggerScriptShown(success);
}
......
......@@ -596,6 +596,13 @@ message TriggerScriptUIProto {
// Whether the visual viewport should be resized to allow scrolling to the
// bottom of the page while the trigger script is being displayed.
optional bool resize_visual_viewport = 8;
// Whether the bottom sheet should temporarily disappear when scrolling down
// the website, to move out of the way.
//
// Avoid setting both resize_visual_viewport and scroll_to_hide to true, as
// the resulting behavior is confusing: the bottom sheet can pop back up after
// a scroll down instead of staying hidden in some situations.
optional bool scroll_to_hide = 9;
}
// An action could be performed.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment