chromium/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/GradientParagraphLoadingView.java

// Copyright 2024 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.browser_ui.widget;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Outline;
import android.graphics.Path;
import android.graphics.Path.Direction;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewOutlineProvider;

import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.core.content.res.ResourcesCompat;

/**
 * ImageView with a scrolling gradient background and an outline with multiple horizontal lines,
 * meant to indicate a paragraph of text that's loading in an indeterminate state. For use in
 * Android API R+.
 *
 * <p>Example:
 *
 * <pre>{@code
 * <GradientParagraphLoadingView
 *     android:layout_width="match_parent"
 *     android:layout_height="55dp"
 *     app:numberOfLines="4"
 *     app:lineHeight="10dp"
 *     app:lineSpacing="5dp"
 *     app:lastLineWidthFraction="25%"/>
 * }</pre>
 *
 * This would draw 4 lines of 10dp height each with 5dp of space between each other, with the last
 * line having a width of 25% of this view. In Android APIs previous to R it will ignore
 * app:numberOfLines and show a single line.
 */
public class GradientParagraphLoadingView extends AppCompatImageView {

    private final int mLineCount;
    private final float mLineHeight;
    private final float mLineSpacing;
    private final float mLastLineWidthFraction;

    public GradientParagraphLoadingView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);

        TypedArray a =
                context.obtainStyledAttributes(attrs, R.styleable.GradientParagraphLoadingView);

        // Number of lines to draw on the outline.
        mLineCount =
                a.getInt(
                        R.styleable.GradientParagraphLoadingView_numberOfLines,
                        getResources().getInteger(R.integer.loading_text_line_count));
        // Height of each line drawn on the outline.
        mLineHeight =
                a.getDimension(
                        R.styleable.GradientParagraphLoadingView_lineHeight,
                        getResources().getDimension(R.dimen.loading_text_line_height));
        // Amount of space between lines.
        mLineSpacing =
                a.getDimension(
                        R.styleable.GradientParagraphLoadingView_lineSpacing,
                        getResources().getDimension(R.dimen.loading_text_line_spacing));
        // Width of the last line as a fraction of the width of this view. All previous lines are as
        // wide as this view.
        mLastLineWidthFraction =
                a.getFraction(
                        R.styleable.GradientParagraphLoadingView_lastLineWidthFraction,
                        1,
                        1,
                        getResources()
                                .getFraction(
                                        R.fraction.loading_text_last_line_width_fraction, 1, 1));

        assert mLineCount > 0 : "Line count must be a positive value";

        a.recycle();
        initialize(context);
    }

    private void initialize(Context context) {

        // TODO(salg): Handle theming on incognito mode.
        AnimatedVectorDrawable animatedGradientDrawable =
                (AnimatedVectorDrawable)
                        ResourcesCompat.getDrawable(
                                context.getResources(),
                                R.drawable.gradient_paragraph_loading_animation,
                                context.getTheme());
        setScaleType(ScaleType.CENTER_CROP);
        setImageDrawable(animatedGradientDrawable);

        setOutlineProvider(
                new ViewOutlineProvider() {
                    @Override
                    public void getOutline(View view, Outline outline) {
                        int viewWidth = view.getWidth();
                        if (viewWidth == 0 || view.getHeight() == 0) return;

                        float cornerRadius = mLineHeight / 2;

                        // Custom outline shapes aren't supported on Android API < R. Fallback to a
                        // single line.
                        if (VERSION.SDK_INT < VERSION_CODES.R) {
                            Rect fallbackRoundRect = new Rect(0, 0, viewWidth, (int) mLineHeight);
                            outline.setRoundRect(fallbackRoundRect, cornerRadius);
                            return;
                        }

                        // Draw all lines into a Path to use as an outline.
                        Path clipPath = new Path();
                        RectF textLineRect = new RectF(0, 0, viewWidth, mLineHeight);
                        for (int line = 0; line < mLineCount; line++) {
                            textLineRect.offsetTo(0, line * (mLineHeight + mLineSpacing));
                            if (line == mLineCount - 1) {
                                // On the last line change its width to a fraction.
                                textLineRect.right = (float) (viewWidth * mLastLineWidthFraction);
                            }
                            clipPath.addRoundRect(
                                    textLineRect, cornerRadius, cornerRadius, Direction.CW);
                        }

                        outline.setPath(clipPath);
                    }
                });
        setClipToOutline(true);

        animatedGradientDrawable.start();
    }
}