chromium/ui/android/java/src/org/chromium/ui/widget/ViewLookupCachingFrameLayout.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.ui.widget;

import android.content.Context;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.IdRes;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.chromium.build.BuildConfig;

import java.lang.ref.WeakReference;

/**
 * An {@link OptimizedFrameLayout} that increases the speed of frequent view lookup by ID by caching
 * the result of the lookup. Adding or removing a view with the same ID as a cached version will
 * cause the cache to be invalidated for that view and cause a re-lookup the next time it is
 * queried. The goal of this view type is to be used in cases where child views are frequently
 * accessed or reused, for example as part of a {@link androidx.recyclerview.widget.RecyclerView}.
 * The logic in the {@link #fastFindViewById(int)} method would be in {@link #findViewById(int)} if
 * it weren't final on the {@link View} class.
 *
 * {@link android.view.ViewGroup.OnHierarchyChangeListener}s cannot be used on ViewGroups that are
 * children of this group since they would overwrite the listeners that are critical to this class'
 * functionality.
 *
 * Usage:
 *     Use the same way that you would use a normal {@link android.widget.FrameLayout}, but instead
 *     of using {@link #findViewById(int)} to access views, use {@link #fastFindViewById(int)}.
 */
public class ViewLookupCachingFrameLayout extends OptimizedFrameLayout {
    /** A map containing views that have had lookup performed on them for quicker access. */
    private final SparseArray<WeakReference<View>> mCachedViews = new SparseArray<>();

    /** The hierarchy listener responsible for notifying the cache that the tree has changed. */
    @VisibleForTesting
    final OnHierarchyChangeListener mListener =
            new OnHierarchyChangeListener() {
                @Override
                public void onChildViewAdded(View parent, View child) {
                    mCachedViews.remove(child.getId());
                    setHierarchyListenerOnTree(child, this);
                }

                @Override
                public void onChildViewRemoved(View parent, View child) {
                    mCachedViews.remove(child.getId());
                    setHierarchyListenerOnTree(child, null);
                }
            };

    /** Default constructor for use in XML. */
    public ViewLookupCachingFrameLayout(Context context, AttributeSet atts) {
        super(context, atts);
        setOnHierarchyChangeListener(mListener);
    }

    @Override
    public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
        assert listener == mListener : "Hierarchy change listeners cannot be set for this group!";
        super.setOnHierarchyChangeListener(listener);
    }

    /**
     * Set the hierarchy listener that invalidates relevant parts of the cache when subtrees change.
     * @param view The root of the tree to attach listeners to.
     * @param listener The listener to attach (null to unset).
     */
    private void setHierarchyListenerOnTree(View view, OnHierarchyChangeListener listener) {
        if (!(view instanceof ViewGroup)) return;

        ViewGroup group = (ViewGroup) view;
        group.setOnHierarchyChangeListener(listener);
        for (int i = 0; i < group.getChildCount(); i++) {
            setHierarchyListenerOnTree(group.getChildAt(i), listener);
        }
    }

    /**
     * Does the same thing as {@link #findViewById(int)} but caches the result if not null.
     * Subsequent lookups are cheaper as a result. Adding or removing a child view invalidates
     * the cache for the ID of the view removed and causes a re-lookup.
     * @param id The ID of the view to lookup.
     * @return The view if it exists.
     */
    @Nullable
    public View fastFindViewById(@IdRes int id) {
        WeakReference<View> ref = mCachedViews.get(id);
        View view = null;
        if (ref != null) view = ref.get();
        if (view == null) view = findViewById(id);
        if (BuildConfig.ENABLE_ASSERTS) {
            assert view == findViewById(id) : "View caching logic is broken!";
            assert ref == null || ref.get() != null
                    : "Cache held reference to garbage collected view!";
        }

        if (view != null) mCachedViews.put(id, new WeakReference<>(view));

        return view;
    }

    @VisibleForTesting
    SparseArray<WeakReference<View>> getCache() {
        return mCachedViews;
    }
}