chromium/chrome/browser/feed/android/java/src/org/chromium/chrome/browser/feed/HeaderIphScrollListener.java

// Copyright 2020 The Chromium Authors
// 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.feed;

import org.chromium.base.TraceEvent;
import org.chromium.chrome.browser.feed.ScrollListener.ScrollState;
import org.chromium.components.feature_engagement.FeatureConstants;
import org.chromium.components.feature_engagement.Tracker;
import org.chromium.components.feature_engagement.TriggerState;

/**
 * Creates a ScrollListener that triggers the menu IPH. The listener removes itself from the
 * list of observers when the IPH is determined to be already triggered.
 *
 * Triggering the IPH is based on (1) the fraction of scroll done on the stream proportionally
 * to its height, (2) the transition fraction of the top search bar, and (3) the position of the
 * menu button in the stream.
 *
 * We want the IPH to be triggered when the section header is properly positioned in the stream
 * which has to meet the following conditions: (1) the IPH popup won't interfere with the search
 * bar at the top of the NTP, (2) the user has scrolled down a bit because they want to look at
 * the feed, and (3) the feed header with its menu button is high enough in the stream to have
 * the feed visible. The goal of conditions (2) and (3) is to show the IPH when the signals are
 * that the user wants to interact with the feed are strong.
 */
public class HeaderIphScrollListener implements ScrollListener {
    private static final float MIN_SCROLL_FRACTION = 0.1f;
    private static final float MAX_HEADER_POS_FRACTION = 0.35f;

    private final FeedBubbleDelegate mDelegate;
    private final ScrollableContainerDelegate mScrollableContainerDelegate;
    private final Runnable mShowIPHRunnable;

    private float mMinScrollFraction;
    private float mHeaderMaxPosFraction;

    HeaderIphScrollListener(
            FeedBubbleDelegate delegate,
            ScrollableContainerDelegate scrollableContainerDelegate,
            Runnable showIPHRunnable) {
        mDelegate = delegate;
        mScrollableContainerDelegate = scrollableContainerDelegate;
        mShowIPHRunnable = showIPHRunnable;

        mMinScrollFraction = MIN_SCROLL_FRACTION;
        mHeaderMaxPosFraction = MAX_HEADER_POS_FRACTION;
    }

    @Override
    public void onScrollStateChanged(@ScrollState int state) {
        if (state != ScrollState.IDLE) return;

        maybeTriggerIPH(mScrollableContainerDelegate.getVerticalScrollOffset());
    }

    @Override
    public void onScrolled(int dx, int dy) {}

    @Override
    public void onHeaderOffsetChanged(int verticalOffset) {
        if (verticalOffset == 0) return;

        // Negate the vertical offset because it is inversely proportional to the scroll offset.
        // For example, a header verical offset of -50px corresponds to a scroll offset of 50px.
        maybeTriggerIPH(-verticalOffset);
    }

    private void maybeTriggerIPH(int verticalScrollOffset) {
        try (TraceEvent e = TraceEvent.scoped("HeaderIphScrollListener.maybeTriggerIPH")) {
            // Get the feature tracker for the IPH and determine whether to show the IPH.
            final String featureForIph = FeatureConstants.FEED_HEADER_MENU_FEATURE;
            final Tracker tracker = mDelegate.getFeatureEngagementTracker();
            // Stop listening to scroll if the IPH was already displayed in the past.
            if (tracker.getTriggerState(featureForIph) == TriggerState.HAS_BEEN_DISPLAYED) {
                mScrollableContainerDelegate.removeScrollListener(this);
                return;
            }

            // Check whether the feed is expanded.
            if (!mDelegate.isFeedExpanded()) return;

            // Check whether the user is signed in.
            if (!mDelegate.isSignedIn()) return;

            // Check that enough scrolling was done proportionally to the stream height.
            if ((float) verticalScrollOffset
                    < (float) mScrollableContainerDelegate.getRootViewHeight()
                            * mMinScrollFraction) {
                return;
            }

            // Check that the feed header is well positioned in the recycler view to show the IPH.
            if (!mDelegate.isFeedHeaderPositionInContainerSuitableForIPH(mHeaderMaxPosFraction)) {
                return;
            }

            mShowIPHRunnable.run();
        }
    }
}