chromium/components/paint_preview/player/android/java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameScaleController.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.components.paintpreview.player.frame;

import android.graphics.Matrix;
import android.util.Size;

import androidx.annotation.Nullable;

import org.chromium.base.Callback;
import org.chromium.base.supplier.Supplier;

/** Handles scaling of the top level frame for the paint preview player. */
public class PlayerFrameScaleController {
    private static final float MAX_SCALE_FACTOR = 5f;

    private float mUncommittedScaleFactor;

    /** References to shared state. */
    private final PlayerFrameViewport mViewport;

    private final Size mContentSize;
    private final Matrix mBitmapScaleMatrix;

    /** Interface for calling shared methods on the mediator. */
    private final PlayerFrameMediatorDelegate mMediatorDelegate;

    private Supplier<Boolean> mIsAccessibilityEnabled;
    private final Callback<Boolean> mOnScaleListener;
    private boolean mAcceptUserInput;

    PlayerFrameScaleController(
            Matrix bitmapScaleMatrix,
            PlayerFrameMediatorDelegate mediatorDelegate,
            @Nullable Supplier<Boolean> isAccessibilityEnabled,
            @Nullable Callback<Boolean> onScaleListener) {
        mUncommittedScaleFactor = 0f;
        mViewport = mediatorDelegate.getViewport();
        mContentSize = mediatorDelegate.getContentSize();
        mBitmapScaleMatrix = bitmapScaleMatrix;
        mMediatorDelegate = mediatorDelegate;
        mIsAccessibilityEnabled = isAccessibilityEnabled;
        mOnScaleListener = onScaleListener;
        mAcceptUserInput = true;
    }

    /**
     * How scale for the paint preview player works.
     *
     * There are two reference frames:
     * - The currently loaded bitmaps, which changes as scaling happens.
     * - The viewport, which is static until scaling is finished.
     *
     * During {@link #scaleBy()} the gesture is still ongoing.
     *
     * On each scale gesture the |scaleFactor| is applied to |mUncommittedScaleFactor| which
     * accumulates the scale starting from the currently committed scale factor. This is
     * committed when {@link #scaleFinished()} event occurs. This is for the viewport reference
     * frame. |mViewport| also accumulates the transforms to track the translation behavior.
     *
     * |mBitmapScaleMatrix| tracks scaling from the perspective of the bitmaps. This is used to
     * transform the canvas the bitmaps are painted on such that scaled images can be shown
     * mid-gesture.
     *
     * Each subframe is updated with a new rect based on the interim scale factor and when the
     * matrix is set in {@link #setBitmapScaleMatrix()} the subframe matricies are recursively
     * updated.
     *
     * On {@link #scaleFinished()} the gesture is now considered finished.
     *
     * The final translation is applied to the viewport. The transform for the bitmaps (that is
     * |mBitmapScaleMatrix|) is cancelled.
     *
     * During {@link #updateVisuals()} new bitmaps are requested for the main frame and subframes
     * to improve quality.
     */
    boolean scaleBy(float scaleFactor, float focalPointX, float focalPointY) {
        if (!mAcceptUserInput) return false;

        if (mIsAccessibilityEnabled != null && mIsAccessibilityEnabled.get()) return false;

        // This is filtered to only apply to the top level view upstream.
        if (mUncommittedScaleFactor == 0f) {
            mUncommittedScaleFactor = mViewport.getScale();
            mMediatorDelegate.onStartScaling();
        }
        // Don't scale outside of the acceptable range. The value is still accumulated such that the
        // continuous gesture feels smooth.
        final float initialScaleFactor = mMediatorDelegate.getMinScaleFactor();
        final float lastUncommittedScaleFactor = mUncommittedScaleFactor;
        mUncommittedScaleFactor *= scaleFactor;
        // Compute a corrected and bounded scale factor when close to the max/min scale.
        if (mUncommittedScaleFactor < initialScaleFactor
                && lastUncommittedScaleFactor > initialScaleFactor) {
            scaleFactor = initialScaleFactor / lastUncommittedScaleFactor;
        } else if (mUncommittedScaleFactor > MAX_SCALE_FACTOR
                && lastUncommittedScaleFactor < MAX_SCALE_FACTOR) {
            scaleFactor = MAX_SCALE_FACTOR / lastUncommittedScaleFactor;
        } else if (mUncommittedScaleFactor > initialScaleFactor
                && lastUncommittedScaleFactor < initialScaleFactor) {
            scaleFactor = mUncommittedScaleFactor / initialScaleFactor;
        } else if (mUncommittedScaleFactor < MAX_SCALE_FACTOR
                && lastUncommittedScaleFactor > MAX_SCALE_FACTOR) {
            scaleFactor = mUncommittedScaleFactor / MAX_SCALE_FACTOR;
        } else if (mUncommittedScaleFactor < initialScaleFactor
                || lastUncommittedScaleFactor > MAX_SCALE_FACTOR) {
            return true;
        }
        final float correctedAggregateScaleFactor = lastUncommittedScaleFactor * scaleFactor;

        // TODO(crbug.com/40133900): trigger a fetch of new bitmaps periodically when zooming out.

        mViewport.scale(scaleFactor, focalPointX, focalPointY);
        mBitmapScaleMatrix.postScale(scaleFactor, scaleFactor, focalPointX, focalPointY);

        float[] bitmapScaleMatrixValues = new float[9];
        mBitmapScaleMatrix.getValues(bitmapScaleMatrixValues);

        // It is possible the scale pushed the viewport outside the content bounds. These new values
        // are forced to be within bounds.
        final float uncorrectedX = mViewport.getTransX();
        final float uncorrectedY = mViewport.getTransY();
        final float correctedX =
                Math.max(
                        0f,
                        Math.min(
                                uncorrectedX,
                                mContentSize.getWidth() * correctedAggregateScaleFactor
                                        - mViewport.getWidth()));
        final float correctedY =
                Math.max(
                        0f,
                        Math.min(
                                uncorrectedY,
                                mContentSize.getHeight() * correctedAggregateScaleFactor
                                        - mViewport.getHeight()));

        if (uncorrectedX != correctedX || uncorrectedY != correctedY) {
            // This is the delta required to force the viewport to be inside the bounds of the
            // content.
            final float deltaX = uncorrectedX - correctedX;
            final float deltaY = uncorrectedY - correctedY;

            // Directly used the forced bounds of the viewport reference frame for the viewport
            // scale matrix.
            mViewport.setTrans(correctedX, correctedY);

            // For the bitmap matrix we only want the delta as its position will be different as the
            // coordinates are bitmap relative.
            bitmapScaleMatrixValues[Matrix.MTRANS_X] += deltaX;
            bitmapScaleMatrixValues[Matrix.MTRANS_Y] += deltaY;
            mBitmapScaleMatrix.setValues(bitmapScaleMatrixValues);
        }
        mMediatorDelegate.updateSubframes(mViewport.asRect(), mViewport.getScale());
        mMediatorDelegate.setBitmapScaleMatrix(mBitmapScaleMatrix, correctedAggregateScaleFactor);
        if (mOnScaleListener != null) mOnScaleListener.onResult(false);
        return true;
    }

    /**
     * Called when scaling is finished to finalize the scaling.
     * @param scaleFactor The final scale event's scale factor.
     * @param focalPointX The final scale event's focal point in the x-axis.
     * @param focalPointY The final scale event's focal point in the y-axis.
     * @return Whether the scale event was consumed.
     */
    boolean scaleFinished(float scaleFactor, float focalPointX, float focalPointY) {
        // All correction/scaling happens in scaleBy() here we just update the mediator.
        mMediatorDelegate.updateScaleFactorOfAllSubframes(mViewport.getScale());
        mMediatorDelegate.updateVisuals(true);
        mMediatorDelegate.forceRedrawVisibleSubframes();
        mUncommittedScaleFactor = 0f;
        if (mOnScaleListener != null) mOnScaleListener.onResult(true);
        return true;
    }

    /** Enables/disables processing input events for scaling. */
    public void setAcceptUserInput(boolean acceptUserInput) {
        mAcceptUserInput = acceptUserInput;
    }
}