Commit ae23a686 authored by Matt Jones's avatar Matt Jones Committed by Commit Bot

Introduce a view lookup caching FrameLayout

This patch introduces ViewLookupCachingFrameLayout with the intent of
making frequent lookups for views much quicker. The new layout adds a
method "fastFindViewById" that can be used in place of findViewById. Once
the initial lookup has occurred, the view is stored in a map by its ID
until it changes location in the hierarchy, removed, or a view with a
matching ID is added or removed. This class will help accomplish what
was typically done for ViewHolders in RecyclerView.

Bug: 982075
Change-Id: I8f4b4f304649b62dcf15c6af748f9fda02b3f8c6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1696172Reviewed-by: default avatarTed Choc <tedchoc@chromium.org>
Reviewed-by: default avatarTheresa <twellington@chromium.org>
Reviewed-by: default avatarWei-Yin Chen (陳威尹) <wychen@chromium.org>
Commit-Queue: Matthew Jones <mdjones@chromium.org>
Cr-Commit-Position: refs/heads/master@{#682335}
parent 8910940e
...@@ -329,6 +329,7 @@ android_library("ui_full_java") { ...@@ -329,6 +329,7 @@ android_library("ui_full_java") {
"java/src/org/chromium/ui/widget/TextViewWithLeading.java", "java/src/org/chromium/ui/widget/TextViewWithLeading.java",
"java/src/org/chromium/ui/widget/Toast.java", "java/src/org/chromium/ui/widget/Toast.java",
"java/src/org/chromium/ui/widget/UiWidgetFactory.java", "java/src/org/chromium/ui/widget/UiWidgetFactory.java",
"java/src/org/chromium/ui/widget/ViewLookupCachingFrameLayout.java",
"java/src/org/chromium/ui/widget/ViewRectProvider.java", "java/src/org/chromium/ui/widget/ViewRectProvider.java",
] ]
deps = [ deps = [
...@@ -391,6 +392,7 @@ junit_binary("ui_junit_tests") { ...@@ -391,6 +392,7 @@ junit_binary("ui_junit_tests") {
"junit/src/org/chromium/ui/shadows/ShadowAnimatedStateListDrawable.java", "junit/src/org/chromium/ui/shadows/ShadowAnimatedStateListDrawable.java",
"junit/src/org/chromium/ui/text/SpanApplierTest.java", "junit/src/org/chromium/ui/text/SpanApplierTest.java",
"junit/src/org/chromium/ui/widget/AnchoredPopupWindowTest.java", "junit/src/org/chromium/ui/widget/AnchoredPopupWindowTest.java",
"junit/src/org/chromium/ui/widget/ViewLookupCachingFrameLayoutTest.java",
] ]
deps = [ deps = [
":ui_java", ":ui_java",
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// 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.support.annotation.IdRes;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import org.chromium.base.BuildConfig;
import org.chromium.base.VisibleForTesting;
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 android.support.v7.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.DCHECK_IS_ON) {
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;
}
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// 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 static org.junit.Assert.assertEquals;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.chromium.base.test.BaseRobolectricTestRunner;
/** Unit tests for the {@link org.chromium.ui.widget.ViewLookupCachingFrameLayout}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ViewLookupCachingFrameLayoutTest {
private static final int VIEW1_ID = 10;
private static final int VIEW2_ID = 20;
private ViewLookupCachingFrameLayout mCachingLayout;
private View mView1;
private View mViewWithSameIdAs1;
private View mView2;
private ViewGroup mGroup;
@Before
public void setUp() {
Context context = RuntimeEnvironment.systemContext;
mCachingLayout = new ViewLookupCachingFrameLayout(context, null);
mView1 = new View(context);
mView1.setId(VIEW1_ID);
mViewWithSameIdAs1 = new View(context);
mViewWithSameIdAs1.setId(VIEW1_ID);
mView2 = new View(context);
mView2.setId(VIEW2_ID);
mGroup = new FrameLayout(context);
assertEquals("Cache should be empty.", 0, mCachingLayout.getCache().size());
}
@Test
public void testAddViewAndLookup() {
mCachingLayout.addView(mView1);
assertEquals("Cache should be empty; no lookups have occurred.", 0,
mCachingLayout.getCache().size());
assertEquals(
"Lookup found the wrong view.", mView1, mCachingLayout.fastFindViewById(VIEW1_ID));
assertEquals("The cache should contain the view.", mView1,
mCachingLayout.getCache().get(VIEW1_ID).get());
}
@Test
public void testAddNestedViewAddSameId() {
mCachingLayout.addView(mGroup);
mGroup.addView(mView1);
assertEquals("View lookup methods should agree.", mCachingLayout.findViewById(VIEW1_ID),
mCachingLayout.fastFindViewById(VIEW1_ID));
assertEquals("The cache should contain the first view.", mView1,
mCachingLayout.getCache().get(VIEW1_ID).get());
// Add the second view earlier in the hierarchy than the original.
mGroup.addView(mViewWithSameIdAs1, 0);
assertEquals("Cache should be empty.", 0, mCachingLayout.getCache().size());
assertEquals("View lookup methods should agree.", mCachingLayout.findViewById(VIEW1_ID),
mCachingLayout.fastFindViewById(VIEW1_ID));
assertEquals("The cache should contain the view that was added second.", mViewWithSameIdAs1,
mCachingLayout.getCache().get(VIEW1_ID).get());
}
@Test
public void testAddNestedViewRemove() {
mCachingLayout.addView(mGroup);
mGroup.addView(mView1);
assertEquals(
"Lookup found the wrong view.", mView1, mCachingLayout.fastFindViewById(VIEW1_ID));
assertEquals("The cache should contain the view.", mView1,
mCachingLayout.getCache().get(VIEW1_ID).get());
mGroup.removeView(mView1);
assertEquals("Cache should be empty.", 0, mCachingLayout.getCache().size());
assertEquals("The view should not longer be in the hierarchy.", null,
mCachingLayout.fastFindViewById(VIEW1_ID));
}
@Test
public void testAddItemWithSameId() {
mCachingLayout.addView(mView1);
assertEquals(
"Lookup found the wrong view.", mView1, mCachingLayout.fastFindViewById(VIEW1_ID));
assertEquals("The cache should contain the view.", mView1,
mCachingLayout.getCache().get(VIEW1_ID).get());
mCachingLayout.addView(mViewWithSameIdAs1);
assertEquals("Cache should be empty.", 0, mCachingLayout.getCache().size());
}
@Test
public void testAddNestedItemWithSameId() {
mCachingLayout.addView(mGroup);
mGroup.addView(mView1);
assertEquals(
"Lookup found the wrong view.", mView1, mCachingLayout.fastFindViewById(VIEW1_ID));
assertEquals("The cache should contain the view.", mView1,
mCachingLayout.getCache().get(VIEW1_ID).get());
mGroup.addView(mViewWithSameIdAs1);
assertEquals("Cache should be empty.", 0, mCachingLayout.getCache().size());
}
@Test
public void testAddItemWithDifferentId() {
mCachingLayout.addView(mView1);
mCachingLayout.addView(mView2);
assertEquals(
"Lookup found the wrong view.", mView1, mCachingLayout.fastFindViewById(VIEW1_ID));
assertEquals(
"Lookup found the wrong view.", mView2, mCachingLayout.fastFindViewById(VIEW2_ID));
assertEquals("The cache should contain the first view.", mView1,
mCachingLayout.getCache().get(VIEW1_ID).get());
assertEquals("The cache should contain the second view.", mView2,
mCachingLayout.getCache().get(VIEW2_ID).get());
}
@Test
public void testRemoveItem() {
mCachingLayout.addView(mView1);
assertEquals(
"Lookup found the wrong view.", mView1, mCachingLayout.fastFindViewById(VIEW1_ID));
assertEquals("The cache should contain the first view.", mView1,
mCachingLayout.getCache().get(VIEW1_ID).get());
mCachingLayout.removeView(mView1);
assertEquals("Cache should be empty.", 0, mCachingLayout.getCache().size());
assertEquals("The view should not longer be in the hierarchy.", null,
mCachingLayout.fastFindViewById(VIEW1_ID));
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment