chromium/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/bottom/ScrollingBottomViewResourceFrameLayout.java

// Copyright 2018 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.toolbar.bottom;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.os.Looper;
import android.util.AttributeSet;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.chrome.browser.toolbar.ConstraintsChecker;
import org.chromium.chrome.browser.toolbar.R;
import org.chromium.chrome.browser.toolbar.ToolbarCaptureType;
import org.chromium.chrome.browser.toolbar.ToolbarFeatures;
import org.chromium.chrome.browser.ui.edge_to_edge.EdgeToEdgeUtils;
import org.chromium.components.browser_ui.widget.ViewResourceFrameLayout;
import org.chromium.ui.resources.dynamics.ViewResourceAdapter;

/**
 * A {@link ViewResourceFrameLayout} that specifically handles redraw of the top shadow of the view
 * it represents.
 */
public class ScrollingBottomViewResourceFrameLayout extends ViewResourceFrameLayout {
    /** A cached rect to avoid extra allocations. */
    private final Rect mCachedRect = new Rect();

    /** The height of the shadow sitting above the bottom view in px. */
    private final int mTopShadowHeightPx;

    /** Snapshot tokens used to be more restrictive about when to allow captures. */
    private @Nullable Object mCurrentSnapshotToken;

    private @Nullable Object mLastCaptureSnapshotToken;

    private @Nullable ConstraintsChecker mConstraintsChecker;

    private boolean mLayoutChanged;

    public ScrollingBottomViewResourceFrameLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        mTopShadowHeightPx = getResources().getDimensionPixelOffset(R.dimen.toolbar_shadow_height);
    }

    @Override
    protected ViewResourceAdapter createResourceAdapter() {
        return new ViewResourceAdapter(this) {
            @Override
            public boolean isDirty() {
                if (ToolbarFeatures.shouldSuppressCaptures()) {
                    // Dirty rect tracking will claim changes more often than token differences due
                    // to model changes. It is also cheaper to simply check a boolean, so do it
                    // first.
                    if (!super.isDirty()) {
                        return false;
                    }

                    // Navigating to and from edge-to-edge tabs causes changes to the layout, and
                    // should trigger a new snapshot. This isn't ideal, though.
                    // TODO (crbug.com/331692414) Remove once the edge-to-edge adjustment to the
                    // bottom controls height / padding is refactored.
                    if (EdgeToEdgeUtils.isLegacyWebsiteOptInEnabled() && mLayoutChanged) {
                        return true;
                    }

                    if (mConstraintsChecker != null && mConstraintsChecker.areControlsLocked()) {
                        mConstraintsChecker.scheduleRequestResourceOnUnlock();
                        return false;
                    }

                    return mCurrentSnapshotToken != null
                            && !mCurrentSnapshotToken.equals(mLastCaptureSnapshotToken);
                } else {
                    return super.isDirty();
                }
            }

            @Override
            public void onCaptureStart(Canvas canvas, Rect dirtyRect) {
                RecordHistogram.recordEnumeratedHistogram(
                        "Android.Toolbar.BitmapCapture",
                        ToolbarCaptureType.BOTTOM,
                        ToolbarCaptureType.NUM_ENTRIES);

                mCachedRect.set(dirtyRect);
                if (mCachedRect.intersect(0, 0, getWidth(), mTopShadowHeightPx)) {
                    canvas.save();

                    // Clip the canvas to only the section of the dirty rect that contains the top
                    // shadow of the view.
                    canvas.clipRect(mCachedRect);

                    // Clear the shadow so redrawing does not make it progressively darker.
                    canvas.drawColor(0, PorterDuff.Mode.CLEAR);

                    canvas.restore();
                }

                super.onCaptureStart(canvas, dirtyRect);
                mLayoutChanged = false;
                mLastCaptureSnapshotToken = mCurrentSnapshotToken;
            }
        };
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        mLayoutChanged = changed;
    }

    /**
     * @return The height of the view's top shadow in px.
     */
    public int getTopShadowHeight() {
        return mTopShadowHeightPx;
    }

    /**
     * Should be invoked any time a model change occurs that that materially impacts the way the
     * view should be drawn such that a new capture is warranted. Should not be affected by
     * animations.
     * @param token Can be used to compare with object equality against previous model states.
     */
    public void onModelTokenChange(@NonNull Object token) {
        mCurrentSnapshotToken = token;
    }

    /**
     * @param constraintsSupplier Used to access current constraints of the browser controls.
     */
    public void setConstraintsSupplier(ObservableSupplier<Integer> constraintsSupplier) {
        assert mConstraintsChecker == null;
        mConstraintsChecker =
                new ConstraintsChecker(
                        getResourceAdapter(), constraintsSupplier, Looper.getMainLooper());
    }
}