chromium/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/base/DynamicSpacingRecyclerViewItemDecoration.java

// Copyright 2023 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.omnibox.suggestions.base;


import androidx.annotation.Px;
import androidx.annotation.VisibleForTesting;

import org.chromium.build.annotations.MockedInTests;

/**
 * RecyclerView ItemDecoration that dynamically calculates preferred element spacing based on
 *
 * <p>- container size, - initial (lead-in) space, - item size, - minimum item space
 *
 * <p>ensuring that the last item is exactly 50% exposed.
 *
 * <p>Note: currently dynamic spacing is activated in portrait mode only.
 */
@MockedInTests
public class DynamicSpacingRecyclerViewItemDecoration extends SpacingRecyclerViewItemDecoration {
    private final @Px int mMinElementSpace;
    private final @Px int mItemWidth;
    private final @Px int mMinItemExposure;
    private final @Px int mMaxItemExposure;
    private @Px int mContainerWidth;
    private boolean mIsPortraitOrientation;

    /**
     * @param leadInSpace the space before the first item in the carousel
     * @param minElementSpace the minimum spacing between two adjacent elements; the space may be
     *     greater than this
     * @param itemWidth the width of a single element
     * @param minItemExposureFraction the minimum exposure of the last visible element; range: [0.f;
     *     1.f]; value of 0.3 means at least 30% of the last element will be exposed
     * @param maxItemExposureFraction the maximum exposure of the last visible element; range: [0.f;
     *     1.f]; value of 0.7 means at most 70% of the last element will be exposed
     */
    public DynamicSpacingRecyclerViewItemDecoration(
            @Px int leadInSpace,
            @Px int minElementSpace,
            @Px int itemWidth,
            float minItemExposureFraction,
            float maxItemExposureFraction) {
        super(leadInSpace, minElementSpace);
        mMinElementSpace = minElementSpace;
        mItemWidth = itemWidth;

        assert minItemExposureFraction >= 0 && minItemExposureFraction <= 1.f;
        assert maxItemExposureFraction >= 0 && maxItemExposureFraction <= 1.f;
        assert minItemExposureFraction <= maxItemExposureFraction;
        mMinItemExposure = (int) (itemWidth * minItemExposureFraction);
        mMaxItemExposure = (int) (itemWidth * maxItemExposureFraction);
        mContainerWidth = 0;
    }

    public DynamicSpacingRecyclerViewItemDecoration(
            @Px int leadInSpace, @Px int minElementSpace, @Px int itemWidth) {
        this(leadInSpace, minElementSpace, itemWidth, 0.5f, 0.5f);
    }

    @Override
    public boolean notifyViewSizeChanged(
            boolean isPortraitOrientation, int newWidth, int newHeight) {
        if (newWidth == mContainerWidth && isPortraitOrientation == mIsPortraitOrientation) {
            return false;
        }

        mContainerWidth = newWidth;
        mIsPortraitOrientation = isPortraitOrientation;
        return setElementSpace(computeElementSpacingPx());
    }

    /**
     * Calculate the margin between tiles based on screen size.
     *
     * @return the requested item spacing, expressed in Pixels
     */
    @VisibleForTesting
    /* package */ int computeElementSpacingPx() {
        if (mIsPortraitOrientation) {
            // Compute item spacing, guaranteeing exactly 50% exposure of one item
            // given the carousel width, item width, initial spacing, and base item spacing.
            // Resulting item spacing must be no smaller than base item spacing.
            //
            // Given a carousel entry:
            //   |__XXXX...XXXX...XXXX...XX|
            // where:
            // - '|' marks boundaries of the carousel,
            // - '_' is the initial spacing,
            // - 'X' is a carousel element, and
            // - '.' is the item space
            // computes the width of item space ('.').
            int adjustedCarouselWidth = mContainerWidth - getLeadInSpace();
            int itemAndSpaceWidth = mItemWidth + mMinElementSpace;
            int numberOfFullyVisibleItems = adjustedCarouselWidth / itemAndSpaceWidth;

            // Calculate fractional item exposure, and adjust it so that the last view is partially
            // exposed within caller-supplied bounds.
            int remainingAreaSize =
                    (adjustedCarouselWidth - numberOfFullyVisibleItems * itemAndSpaceWidth);
            int lastVisibleItemExposure = remainingAreaSize;

            if (lastVisibleItemExposure > mMaxItemExposure) {
                lastVisibleItemExposure = mMaxItemExposure;
            } else if (lastVisibleItemExposure < mMinItemExposure) {
                lastVisibleItemExposure = mMaxItemExposure;
                numberOfFullyVisibleItems--;
            }

            // If tiles are too large (i.e. larger than the screen width), just return default
            // padding. There's nothing we can do.
            if (numberOfFullyVisibleItems <= 0) {
                return mMinElementSpace;
            }

            int totalPaddingAreaSize =
                    adjustedCarouselWidth
                            - (numberOfFullyVisibleItems * mItemWidth)
                            - lastVisibleItemExposure;
            int itemSpacing = totalPaddingAreaSize / numberOfFullyVisibleItems;
            return itemSpacing;
        } else {
            return mMinElementSpace;
        }
    }
}