chromium/ui/android/java/src/org/chromium/ui/widget/LoadingView.java

// Copyright 2015 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.ui.widget;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ProgressBar;

import org.chromium.base.ResettersForTesting;
import org.chromium.ui.interpolators.Interpolators;

import java.util.ArrayList;
import java.util.List;

/** A {@link ProgressBar} that understands the hiding/showing policy defined in Material Design. */
public class LoadingView extends ProgressBar {
    private static final int LOADING_ANIMATION_DELAY_MS = 500;
    private static final int MINIMUM_ANIMATION_SHOW_TIME_MS = 500;

    /** A observer interface that will be notified when the progress bar is hidden. */
    public interface Observer {
        /**
         * Notify the listener a call to {@link #showLoadingUI()} is complete and loading view
         * is VISIBLE.
         */
        void onShowLoadingUIComplete();

        /**
         * Notify the listener a call to {@link #hideLoadingUI()} is complete and loading view is
         * GONE.
         */
        void onHideLoadingUIComplete();
    }

    private long mStartTime = -1;
    private static boolean sDisableAnimationForTest;

    private final List<Observer> mObservers = new ArrayList<>();

    private final Runnable mDelayedShow =
            new Runnable() {
                @Override
                public void run() {
                    if (!mShouldShow) return;
                    mStartTime = SystemClock.elapsedRealtime();
                    setVisibility(View.VISIBLE);
                    setAlpha(1.0f);

                    for (Observer observer : mObservers) {
                        observer.onShowLoadingUIComplete();
                    }
                }
            };

    /**
     * Tracks whether the View should be displayed when {@link #mDelayedShow} is run.  Android
     * doesn't always cancel a Runnable when requested, meaning that the View could be hidden before
     * it even has a chance to be shown.
     */
    private boolean mShouldShow;

    // Material loading design spec requires us to show progress spinner at least 500ms, so we need
    // this delayed runnable to implement that.
    private final Runnable mDelayedHide =
            new Runnable() {
                @Override
                public void run() {
                    if (sDisableAnimationForTest) {
                        onHideLoadingFinished();
                        return;
                    }

                    animate()
                            .alpha(0.0f)
                            .setInterpolator(Interpolators.FAST_OUT_SLOW_IN_INTERPOLATOR)
                            .setListener(
                                    new AnimatorListenerAdapter() {
                                        @Override
                                        public void onAnimationEnd(Animator animation) {
                                            onHideLoadingFinished();
                                        }
                                    });
                }
            };

    /** Constructor for creating the view programmatically. */
    public LoadingView(Context context) {
        super(context);
    }

    /** Constructor for inflating from XML. */
    public LoadingView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /** Shows loading UI with a delay by calling showLoadingUI(false). */
    public void showLoadingUI() {
        showLoadingUI(/* skipDelay= */ false);
    }

    /**
     * Show the loading UI. If skipDelay is set to true, the delay before the loading animation will
     * be skipped. If skipDelay is set to false, the loading animation will be shown after a delay
     * based on LOADING_ANIMATION_DELAY_MS (500ms).
     */
    public void showLoadingUI(boolean skipDelay) {
        removeCallbacks(mDelayedShow);
        removeCallbacks(mDelayedHide);
        mShouldShow = true;

        setVisibility(GONE);

        if (skipDelay) {
            mDelayedShow.run();
        } else {
            postDelayed(mDelayedShow, LOADING_ANIMATION_DELAY_MS);
        }
    }

    /**
     * Hide loading UI. If progress bar is not shown, it disappears immediately. If so, it smoothly
     * fades out.
     */
    public void hideLoadingUI() {
        removeCallbacks(mDelayedShow);
        removeCallbacks(mDelayedHide);
        mShouldShow = false;

        if (getVisibility() == VISIBLE) {
            postDelayed(
                    mDelayedHide,
                    Math.max(
                            0,
                            mStartTime
                                    + MINIMUM_ANIMATION_SHOW_TIME_MS
                                    - SystemClock.elapsedRealtime()));
        } else {
            onHideLoadingFinished();
        }
    }

    /** Remove all callbacks when this view is no longer needed. */
    public void destroy() {
        removeCallbacks(mDelayedShow);
        removeCallbacks(mDelayedHide);
        mObservers.clear();
    }

    /**
     * Add the listener that will be notified when the spinner is completely hidden with {@link
     * #hideLoadingUI()}.
     * @param listener {@link Observer} that will be notified when the spinner is
     *         completely hidden with {@link #hideLoadingUI()}.
     */
    public void addObserver(Observer listener) {
        mObservers.add(listener);
    }

    private void onHideLoadingFinished() {
        setVisibility(GONE);
        for (Observer observer : mObservers) {
            observer.onHideLoadingUIComplete();
        }
    }

    /**
     * Set disable the fading animation during {@link #hideLoadingUI()}.
     * This function is added as a work around for disable animation during unit tests.
     * @param disableAnimation Whether the fading animation should be disabled during {@link
     *         #hideLoadingUI()}.
     */
    public static void setDisableAnimationForTest(boolean disableAnimation) {
        sDisableAnimationForTest = disableAnimation;
        ResettersForTesting.register(() -> sDisableAnimationForTest = false);
    }

    /**
     * Check if the Loading View Observer is empty or not.
     * @return If the observers is empty then return true.
     */
    public boolean isObserverListEmpty() {
        return mObservers.isEmpty();
    }
}