chromium/components/browser_ui/util/android/java/src/org/chromium/components/browser_ui/util/FirstDrawDetector.java

// Copyright 2019 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.util;

import android.view.Choreographer;
import android.view.View;
import android.view.ViewTreeObserver;

import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;

import java.lang.ref.WeakReference;

/** A utility for observing when a view gets drawn for the first time. */
public class FirstDrawDetector {
    WeakReference<View> mView;
    Runnable mCallback;
    private boolean mHasRunBefore;

    private FirstDrawDetector(View view, Runnable callback) {
        mView = new WeakReference<>(view);
        mCallback = callback;
    }

    /**
     * Waits for a view to be drawn on the screen for the first time.
     * @param view View whose drawing to observe.
     * @param callback Callback to trigger on first draw. Will be called on the UI thread.
     */
    public static void waitForFirstDraw(View view, Runnable callback) {
        new FirstDrawDetector(view, callback).startWaiting(/* strict= */ false);
    }

    /**
     * Waits for a view to be drawn on the screen for the first time. Unlike |#waitForFirstDraw()|,
     * which can trigger the callback in |#onPreDraw()|, this method waits for |#onDraw()|. This is
     * useful when the caller knows that the draw may be delayed because some
     * {@link OnPreDrawListener}s return false.
     * @param view View whose drawing to observe.
     * @param callback Callback to trigger on first draw. Will be called on the UI thread.
     */
    public static void waitForFirstDrawStrict(View view, Runnable callback) {
        new FirstDrawDetector(view, callback).startWaiting(/* strict= */ true);
    }

    /**
     * Starts waiting for a draw to trigger the callback.
     * @param strict Whether to wait for an |#onDraw| strictly. See |#waitForFirstDrawStrict()|.
     */
    private void startWaiting(boolean strict) {
        ViewTreeObserver.OnDrawListener firstDrawListener =
                new ViewTreeObserver.OnDrawListener() {
                    @Override
                    public void onDraw() {
                        if (mHasRunBefore) return;
                        mHasRunBefore = true;
                        // This callback will be run in the normal case (e.g., screen is on).
                        onFirstDraw();
                        // The draw listener can't be removed from within the callback, so remove it
                        // asynchronously.
                        PostTask.postTask(
                                TaskTraits.UI_BEST_EFFORT,
                                () -> {
                                    if (mView.get() == null) return;
                                    mView.get().getViewTreeObserver().removeOnDrawListener(this);
                                });
                    }
                };
        mView.get().getViewTreeObserver().addOnDrawListener(firstDrawListener);
        if (strict) return;
        // We use a draw listener to detect when a view is first drawn. However, if the view
        // doesn't get drawn for some reason (e.g. the screen is off), our listener will never
        // get called. To work around this, we also schedule a callback for the next frame from
        // a pre-draw listener (which will always get called). Whichever callback runs first
        // will declare the view to have been drawn.
        //
        // Note that we cannot just use a pre-draw listener here, because it does not guarantee
        // that the view has actually been drawn.
        ViewTreeObserver.OnPreDrawListener firstPreDrawListener =
                new ViewTreeObserver.OnPreDrawListener() {
                    @Override
                    public boolean onPreDraw() {
                        // The pre-draw listener will run both when the screen is on or off, but the
                        // view might not have been drawn yet at this point. Trigger the first paint
                        // at the next frame.
                        Choreographer.getInstance()
                                .postFrameCallback(
                                        (long frameTimeNanos) -> {
                                            onFirstDraw();
                                        });
                        if (mView.get() != null) {
                            mView.get().getViewTreeObserver().removeOnPreDrawListener(this);
                        }
                        return true;
                    }
                };
        mView.get().getViewTreeObserver().addOnPreDrawListener(firstPreDrawListener);
    }

    private void onFirstDraw() {
        if (mCallback != null) {
            mCallback.run();
            mCallback = null;
        }
    }
}