chromium/chrome/browser/hub/android/java/src/org/chromium/chrome/browser/hub/ShrinkExpandAnimator.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.hub;

import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.util.Size;
import android.widget.ImageView;

import androidx.annotation.NonNull;

import org.chromium.build.annotations.UsedByReflection;

/** Animator for scaling a {@link ShrinkExpandImageView} from one rect to another. */
// TODO(crbug.com/40286625): Move to hub/internal/ once TabSwitcherLayout no longer depends on this.
public class ShrinkExpandAnimator {
    /**
     * Tag for the {@link ObjectAnimator#ofObject()} {@code propertyName} param. Using this triggers
     * invocation of {@link #setRect(Rect)} during animation steps. An {@link RectEvaluator} is used
     * to interpolate between {@link Rect}s based on animation steps.
     */
    public static final String RECT = "rect";

    private final ShrinkExpandImageView mView;
    private final Rect mInitialRect;
    private final Rect mFinalRect;
    private final Matrix mImageMatrix;
    private Size mThumbnailSize;

    /**
     * Create an animator that scales and translates a view from one rect to another.
     *
     * @param view the ShrinkExpandImageView to apply the translation and scaling to. It should have
     *     a default scale of 1.0f and translation of 0.0f and be the size of {@code initialRect}.
     * @param initialRect the initial rect that view encompasses in global coordinates.
     * @param finalRect the final rect that view will encompass in global coordinates.
     */
    public ShrinkExpandAnimator(
            @NonNull ShrinkExpandImageView view,
            @NonNull Rect initialRect,
            @NonNull Rect finalRect) {
        mView = view;
        assert mView.getScaleX() == 1.0f;
        assert mView.getScaleY() == 1.0f;
        assert mView.getTranslationX() == 0.0f;
        assert mView.getTranslationY() == 0.0f;

        // Copy these rects to ensure they aren't re-used.
        mInitialRect = new Rect(initialRect);
        mFinalRect = new Rect(finalRect);
        mImageMatrix = new Matrix();
    }

    /**
     * Set the size of the thumbnail for top offset computation. Only necessary when expanding.
     * @param thumbnailSize The size of the thumbnail in the tab grid.
     */
    public void setThumbnailSizeForOffset(Size thumbnailSize) {
        mThumbnailSize = thumbnailSize;
    }

    /**
     * Adjusts {@link mView} to be the size of {@code rect}. Called by an {@link ObjectAnimator}
     * for each frame using a {@link RectEvaluator} to interpolate between {@link mInitialRect} and
     * {@link mFinalRect}.
     * @param rect The rect to scale the view to for the current frame.
     */
    @UsedByReflection("Used in ObjectAnimator")
    public void setRect(@NonNull Rect rect) {
        final float scaleX = (float) rect.width() / mInitialRect.width();
        final float scaleY = (float) rect.height() / mInitialRect.height();

        // Use view properties to animate the change without needing to re-layout the view. This
        // makes the animation efficient and smooth compared to manually resizing.
        mView.setScaleX(scaleX);
        mView.setScaleY(scaleY);
        mView.setTranslationX(
                rect.left
                        - Math.round(
                                mInitialRect.left + (1.0 - scaleX) * mInitialRect.width() / 2.0));
        mView.setTranslationY(
                rect.top
                        - Math.round(
                                mInitialRect.top + (1.0 - scaleY) * mInitialRect.height() / 2.0));

        // If there is no image we don't need to do anything else.
        mImageMatrix.reset();
        Bitmap bitmap = mView.getBitmap();
        if (bitmap == null) return;

        // Scale image to fill the width of the screen. Normalize the scale against the scaling of
        // the view to ensure the image appears as if it is scaling with the view. Scaling the
        // view by itself will not change the image size which would lead to the wrong appearance.
        final float scale = (float) rect.width() / bitmap.getWidth();
        final float xFactor = scale / scaleX;
        final float yFactor = scale / scaleY;
        mImageMatrix.setScale(xFactor, yFactor);

        // This section handles y-offset of the image so that a view that is initially partially
        // offscreen at the top correctly "crops" the top of the image throughout the animation.
        // This is only necessary when expanding the rect.
        if (mThumbnailSize != null
                && mInitialRect.top == mFinalRect.top
                && mInitialRect.height() < mThumbnailSize.getHeight()) {
            // Y translation offset shifts in line with the scaling of the rectangle. It should
            // progress from initialYOffset -> 0 as the scaling progresses.
            //
            // Use scaleX here as it changes linearly (unlike scaleY) and will ensure the bitmap
            // maintains a fixed aspect ratio as the image should fill the width of the screen.
            //
            // initialYOffset: the initial offset of the image off the screen.
            // scaleX: the current scale value goes from 0 to finalScaleX.
            // finalScaleX: the final expected scale.
            //
            // Multiply the initialYOffset by a linear function that follows the progression from
            // initialYOffset to 0 as a function of how the scaling has progressed. This is applied
            // using preTranslate because it isn't affected by scale.
            final float finalScaleX = (float) mFinalRect.width() / mInitialRect.width();
            final int initialYOffset = mInitialRect.height() - mThumbnailSize.getHeight();
            final int yOffset =
                    (int)
                            Math.round(
                                    (float) initialYOffset
                                            * ((1.0 - scaleX) / (finalScaleX - 1.0) + 1.0));
            mImageMatrix.preTranslate(0, yOffset);
        }

        // Center the image on the x-axis of the view. This is applied postTranslate because it is
        // affected by the scaling of the view.
        final int xOffset =
                Math.round((mInitialRect.width() - (bitmap.getWidth() * xFactor)) / 2.0f);
        mImageMatrix.postTranslate(xOffset, 0);

        mView.setScaleType(ImageView.ScaleType.MATRIX);
        mView.setImageMatrix(mImageMatrix);
    }
}