/** * Copyright (c) 2017-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.litho; import android.graphics.Color; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.ColorDrawable; import android.support.v4.util.SparseArrayCompat; import com.facebook.litho.testing.TestComponent; import com.facebook.litho.testing.TestDrawableComponent; import com.facebook.litho.testing.TestSizeDependentComponent; import com.facebook.litho.testing.testrunner.ComponentsTestRunner; import com.facebook.litho.testing.util.InlineLayoutSpec; import com.facebook.yoga.YogaAlign; import com.facebook.yoga.YogaConstants; import com.facebook.yoga.YogaEdge; import com.facebook.yoga.YogaMeasureFunction; import com.facebook.yoga.YogaMeasureOutput; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.rule.PowerMockRule; import org.powermock.reflect.Whitebox; import org.robolectric.RuntimeEnvironment; import static com.facebook.yoga.YogaMeasureMode.EXACTLY; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @PrepareForTest(Component.class) @PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) @RunWith(ComponentsTestRunner.class) public class TreeDiffingTest { @Rule public PowerMockRule mPowerMockRule = new PowerMockRule(); private int mUnspecifiedSpec; private ComponentContext mContext; @Before public void setup() throws Exception { mContext = new ComponentContext(RuntimeEnvironment.application); mUnspecifiedSpec = SizeSpec.makeSizeSpec(0, SizeSpec.UNSPECIFIED); } @Test public void testDiffTreeDisabled() { final Component component = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .child(TestDrawableComponent.create(c)) .child( Column.create(c) .child(TestDrawableComponent.create(c))) .build(); } }; LayoutState layoutState = LayoutState.calculate( mContext, component, -1, SizeSpec.makeSizeSpec(350, SizeSpec.EXACTLY), SizeSpec.makeSizeSpec(200, SizeSpec.EXACTLY), false, false, null, false); // Check diff tree is null. assertNull(layoutState.getDiffTree()); } @Test public void testDiffTreeEnabled() { final Component component = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .child(TestDrawableComponent.create(c)) .child( Column.create(c) .child(TestDrawableComponent.create(c))) .build(); } }; LayoutState layoutState = LayoutState.calculate( mContext, component, -1, SizeSpec.makeSizeSpec(350, SizeSpec.EXACTLY), SizeSpec.makeSizeSpec(200, SizeSpec.EXACTLY), true, false, null, false); // Check diff tree is not null and consistent. DiffNode node = layoutState.getDiffTree(); assertNotNull(node); assertEquals(countNodes(node), 4); } private static int countNodes(DiffNode node) { int sum = 1; for (int i = 0; i < node.getChildCount(); i++) { sum += countNodes(node.getChildAt(i)); } return sum; } private InternalNode createInternalNodeForMeasurableComponent(Component component) { InternalNode node = LayoutState.createTree( component, mContext); return node; } private long measureInternalNode( InternalNode node, float widthConstranint, float heightConstraint) { final YogaMeasureFunction measureFunc = Whitebox.getInternalState( node.mYogaNode, "mMeasureFunction"); return measureFunc.measure( node.mYogaNode, widthConstranint, EXACTLY, heightConstraint, EXACTLY); } @Test public void testCachedMeasureFunction() { final Component component = TestDrawableComponent.create(mContext) .build(); InternalNode node = createInternalNodeForMeasurableComponent(component); DiffNode diffNode = new DiffNode(); diffNode.setLastHeightSpec(mUnspecifiedSpec); diffNode.setLastWidthSpec(mUnspecifiedSpec); diffNode.setLastMeasuredWidth(10); diffNode.setLastMeasuredHeight(5); diffNode.setComponent(component); node.setCachedMeasuresValid(true); node.setDiffNode(diffNode); long output = measureInternalNode( node, YogaConstants.UNDEFINED, YogaConstants.UNDEFINED); assertTrue(YogaMeasureOutput.getHeight(output) == (int) diffNode.getLastMeasuredHeight()); assertTrue(YogaMeasureOutput.getWidth(output) == (int) diffNode.getLastMeasuredWidth()); } @Test public void tesLastConstraints() { final Component component = TestDrawableComponent.create(mContext) .build(); InternalNode node = createInternalNodeForMeasurableComponent(component); DiffNode diffNode = new DiffNode(); diffNode.setLastWidthSpec(SizeSpec.makeSizeSpec(10, SizeSpec.EXACTLY)); diffNode.setLastHeightSpec(SizeSpec.makeSizeSpec(5, SizeSpec.EXACTLY)); diffNode.setLastMeasuredWidth(10f); diffNode.setLastMeasuredHeight(5f); diffNode.setComponent(component); node.setCachedMeasuresValid(true); node.setDiffNode(diffNode); long output = measureInternalNode(node, 10f, 5f); assertTrue(YogaMeasureOutput.getHeight(output) == (int) diffNode.getLastMeasuredHeight()); assertTrue(YogaMeasureOutput.getWidth(output) == (int) diffNode.getLastMeasuredWidth()); int lastWidthSpec = node.getLastWidthSpec(); int lastHeightSpec = node.getLastHeightSpec(); assertTrue(SizeSpec.getMode(lastWidthSpec) == SizeSpec.EXACTLY); assertTrue(SizeSpec.getMode(lastHeightSpec) == SizeSpec.EXACTLY); assertTrue(SizeSpec.getSize(lastWidthSpec) == 10); assertTrue(SizeSpec.getSize(lastHeightSpec) == 5); } @Test public void measureAndCreateDiffNode() { final Component component = TestDrawableComponent.create(mContext) .build(); InternalNode node = createInternalNodeForMeasurableComponent(component); long output = measureInternalNode( node, YogaConstants.UNDEFINED, YogaConstants.UNDEFINED); node.setCachedMeasuresValid(false); DiffNode diffNode = LayoutState.createDiffNode(node, null); assertTrue(YogaMeasureOutput.getHeight(output) == (int) diffNode.getLastMeasuredHeight()); assertTrue(YogaMeasureOutput.getWidth(output) == (int) diffNode.getLastMeasuredWidth()); } @Test public void testCachedMeasures() { final Component component1 = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .child(TestDrawableComponent.create(c)) .child( Column.create(c) .child(TestDrawableComponent.create(c))) .build(); } }; final Component component2 = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .child(TestDrawableComponent.create(c)) .child( Column.create(c) .child(TestDrawableComponent.create(c))) .build(); } }; LayoutState prevLayoutState = LayoutState.calculate( mContext, component1, -1, SizeSpec.makeSizeSpec(350, SizeSpec.EXACTLY), SizeSpec.makeSizeSpec(200, SizeSpec.EXACTLY), true, false, null, false); // Check diff tree is consistent. DiffNode node = prevLayoutState.getDiffTree(); InternalNode layoutTreeRoot = LayoutState.createTree( component2, mContext); LayoutState.applyDiffNodeToUnchangedNodes(layoutTreeRoot, node); checkAllComponentsHaveMeasureCache(layoutTreeRoot); } @Test public void testPartiallyCachedMeasures() { final Component component1 = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .child(TestDrawableComponent.create(c)) .child( Column.create(c) .child(TestDrawableComponent.create(c))) .build(); } }; final Component component2 = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .child(TestDrawableComponent.create(c)) .child( Column.create(c) .child(TestDrawableComponent.create(c))) .child(TestDrawableComponent.create(c)) .build(); } }; LayoutState prevLayoutState = LayoutState.calculate( mContext, component1, -1, SizeSpec.makeSizeSpec(350, SizeSpec.EXACTLY), SizeSpec.makeSizeSpec(200, SizeSpec.EXACTLY), true, false, null, false); // Check diff tree is consistent. DiffNode node = prevLayoutState.getDiffTree(); InternalNode layoutTreeRoot = LayoutState.createTree( component2, mContext); LayoutState.applyDiffNodeToUnchangedNodes(layoutTreeRoot, node); InternalNode child_1 = (InternalNode) layoutTreeRoot.getChildAt(0); assertCachedMeasurementsDefined(child_1); InternalNode child_2 = (InternalNode) layoutTreeRoot.getChildAt(1); assertCachedMeasurementsNotDefined(child_2); InternalNode child_3 = (InternalNode) child_2.getChildAt(0); assertCachedMeasurementsDefined(child_3); InternalNode child_4 = (InternalNode) layoutTreeRoot.getChildAt(2); assertCachedMeasurementsNotDefined(child_4); } @Test public void testLayoutOutputReuse() { final Component component1 = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .child(TestDrawableComponent.create(c)) .child( Column.create(c) .child(TestDrawableComponent.create(c))) .build(); } }; final Component component2 = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .child(TestDrawableComponent.create(c)) .child( Column.create(c) .child(TestDrawableComponent.create(c))) .build(); } }; LayoutState prevLayoutState = LayoutState.calculate( mContext, component1, -1, SizeSpec.makeSizeSpec(350, SizeSpec.EXACTLY), SizeSpec.makeSizeSpec(200, SizeSpec.EXACTLY), true, false, null, false); LayoutState layoutState = LayoutState.calculate( mContext, component2, -1, SizeSpec.makeSizeSpec(350, SizeSpec.EXACTLY), SizeSpec.makeSizeSpec(200, SizeSpec.EXACTLY), true, false, prevLayoutState.getDiffTree(), false); assertEquals(prevLayoutState.getMountableOutputCount(), layoutState.getMountableOutputCount()); for (int i = 0, count = prevLayoutState.getMountableOutputCount(); i < count; i++) { assertEquals( prevLayoutState.getMountableOutputAt(i).getId(), layoutState.getMountableOutputAt(i).getId()); } } @Test public void testLayoutOutputPartialReuse() { final Component component1 = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .child(TestDrawableComponent.create(c)) .child( Column.create(c) .child(TestDrawableComponent.create(c))) .build(); } }; final Component component2 = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .child(TestDrawableComponent.create(c)) .child( Column.create(c) .child(TestDrawableComponent.create(c))) .child(TestDrawableComponent.create(c)) .build(); } }; LayoutState prevLayoutState = LayoutState.calculate( mContext, component1, -1, SizeSpec.makeSizeSpec(350, SizeSpec.EXACTLY), SizeSpec.makeSizeSpec(200, SizeSpec.EXACTLY), true, false, null, false); LayoutState layoutState = LayoutState.calculate( mContext, component2, -1, SizeSpec.makeSizeSpec(350, SizeSpec.EXACTLY), SizeSpec.makeSizeSpec(200, SizeSpec.EXACTLY), true, false, prevLayoutState.getDiffTree(), false); assertNotEquals( prevLayoutState.getMountableOutputCount(), layoutState.getMountableOutputCount()); for (int i = 0, count = prevLayoutState.getMountableOutputCount(); i < count; i++) { assertEquals( prevLayoutState.getMountableOutputAt(i).getId(), layoutState.getMountableOutputAt(i).getId()); } } private void assertCachedMeasurementsNotDefined(InternalNode node) { assertFalse(node.areCachedMeasuresValid()); } private void checkAllComponentsHaveMeasureCache(InternalNode node) { if (node.getRootComponent() != null && node.getRootComponent().getLifecycle().canMeasure()) { assertCachedMeasurementsDefined(node); } int numChildren = node.getChildCount(); for (int i = 0; i < numChildren; i++) { checkAllComponentsHaveMeasureCache((InternalNode) node.getChildAt(i)); } } @Test public void testComponentHostMoveItem() { ComponentHost hostHolder = new ComponentHost(mContext); MountItem mountItem = new MountItem(); MountItem mountItem1 = new MountItem(); MountItem mountItem2 = new MountItem(); hostHolder.mount(0, mountItem, new Rect()); hostHolder.mount(1, mountItem1, new Rect()); hostHolder.mount(2, mountItem2, new Rect()); assertEquals(hostHolder.getMountItemAt(0), mountItem); assertEquals(hostHolder.getMountItemAt(1), mountItem1); assertEquals(hostHolder.getMountItemAt(2), mountItem2); hostHolder.moveItem(mountItem, 0, 2); hostHolder.moveItem(mountItem2, 2, 0); assertEquals(hostHolder.getMountItemAt(0), mountItem2); assertEquals(hostHolder.getMountItemAt(1), mountItem1); assertEquals(hostHolder.getMountItemAt(2), mountItem); } @Test public void testComponentHostMoveItemPartial() { ComponentHost hostHolder = new ComponentHost(mContext); MountItem mountItem = new MountItem(); MountItem mountItem1 = new MountItem(); MountItem mountItem2 = new MountItem(); hostHolder.mount(0, mountItem, new Rect()); hostHolder.mount(1, mountItem1, new Rect()); hostHolder.mount(2, mountItem2, new Rect()); assertEquals(hostHolder.getMountItemAt(0), mountItem); assertEquals(hostHolder.getMountItemAt(1), mountItem1); assertEquals(hostHolder.getMountItemAt(2), mountItem2); hostHolder.moveItem(mountItem2, 2, 0); assertEquals(hostHolder.getMountItemAt(0), mountItem2); assertEquals(hostHolder.getMountItemAt(1), mountItem1); assertEquals( ((SparseArrayCompat<MountItem>) Whitebox .getInternalState(hostHolder, "mScrapMountItemsArray")).size(), 1); hostHolder.unmount(0, mountItem); assertEquals( ((SparseArrayCompat<MountItem>) Whitebox .getInternalState(hostHolder, "mMountItems")).size(), 2); assertNull(Whitebox.getInternalState(hostHolder, "mScrapMountItemsArray")); } @Test public void testLayoutOutputUpdateState() { final Component firstComponent = TestDrawableComponent.create(mContext) .color(Color.BLACK) .build(); final Component secondComponent = TestDrawableComponent.create(mContext) .color(Color.BLACK) .build(); final Component thirdComponent = TestDrawableComponent.create(mContext) .color(Color.WHITE) .build(); ComponentTree componentTree = ComponentTree.create(mContext, firstComponent) .incrementalMount(false) .layoutDiffing(false) .build(); LayoutState state = componentTree.calculateLayoutState( null, mContext, firstComponent, SizeSpec.makeSizeSpec(10, SizeSpec.EXACTLY), SizeSpec.makeSizeSpec(10, SizeSpec.EXACTLY), true, false, null); assertOutputsState(state, LayoutOutput.STATE_UNKNOWN); LayoutState secondState = componentTree.calculateLayoutState( null, mContext, secondComponent, SizeSpec.makeSizeSpec(10, SizeSpec.EXACTLY), SizeSpec.makeSizeSpec(10, SizeSpec.EXACTLY), true, false, state.getDiffTree()); assertOutputsState(secondState, LayoutOutput.STATE_UPDATED); LayoutState thirdState = componentTree.calculateLayoutState( null, mContext, thirdComponent, SizeSpec.makeSizeSpec(10, SizeSpec.EXACTLY), SizeSpec.makeSizeSpec(10, SizeSpec.EXACTLY), true, false, secondState.getDiffTree()); assertOutputsState(thirdState, LayoutOutput.STATE_DIRTY); } @Test public void testLayoutOutputUpdateStateWithBackground() { final Drawable redDrawable = new ColorDrawable(Color.RED); final Drawable blackDrawable = new ColorDrawable(Color.BLACK); final Drawable transparentDrawable = new ColorDrawable(Color.TRANSPARENT); final Component component1 = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .background(redDrawable) .foreground(transparentDrawable) .child(TestDrawableComponent.create(c)) .child( Column.create(c) .child(TestDrawableComponent.create(c))) .build(); } }; final Component component2 = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .background(redDrawable) .foreground(transparentDrawable) .child(TestDrawableComponent.create(c)) .child( Column.create(c) .child(TestDrawableComponent.create(c))) .build(); } }; final Component component3 = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .background(blackDrawable) .foreground(transparentDrawable) .child(TestDrawableComponent.create(c)) .child( Column.create(c) .child(TestDrawableComponent.create(c))) .build(); } }; ComponentTree componentTree = ComponentTree.create(mContext, component1) .incrementalMount(false) .layoutDiffing(false) .build(); LayoutState state = componentTree.calculateLayoutState( null, mContext, component1, SizeSpec.makeSizeSpec(10, SizeSpec.EXACTLY), SizeSpec.makeSizeSpec(10, SizeSpec.EXACTLY), true, false, null); assertOutputsState(state, LayoutOutput.STATE_UNKNOWN); LayoutState secondState = componentTree.calculateLayoutState( null, mContext, component2, SizeSpec.makeSizeSpec(10, SizeSpec.EXACTLY), SizeSpec.makeSizeSpec(10, SizeSpec.EXACTLY), true, false, state.getDiffTree()); assertEquals(secondState.getMountableOutputCount(), 5); assertOutputsState(secondState, LayoutOutput.STATE_UPDATED); LayoutState thirdState = componentTree.calculateLayoutState( null, mContext, component3, SizeSpec.makeSizeSpec(10, SizeSpec.EXACTLY), SizeSpec.makeSizeSpec(10, SizeSpec.EXACTLY), true, false, secondState.getDiffTree()); assertEquals(thirdState.getMountableOutputCount(), 5); assertEquals(LayoutOutput.STATE_DIRTY, thirdState.getMountableOutputAt(1).getUpdateState()); assertEquals(LayoutOutput.STATE_UPDATED, thirdState.getMountableOutputAt(2).getUpdateState()); assertEquals(LayoutOutput.STATE_UPDATED, thirdState.getMountableOutputAt(3).getUpdateState()); assertEquals(LayoutOutput.STATE_UPDATED, thirdState.getMountableOutputAt(4).getUpdateState()); } // This test covers the same case with the foreground since the code path is the same! @Test public void testLayoutOutputUpdateStateWithBackgroundInWithLayout() { final Drawable redDrawable = new ColorDrawable(Color.RED); final Drawable blackDrawable = new ColorDrawable(Color.BLACK); final Component component1 = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .background(redDrawable) .foregroundRes(android.R.drawable.btn_default) .child( TestDrawableComponent.create(c) .withLayout() .background(blackDrawable)) .child( Column.create(c) .child(TestDrawableComponent.create(c))) .build(); } }; final Component component2 = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .background(redDrawable) .foregroundRes(android.R.drawable.btn_default) .child(TestDrawableComponent.create(c) .withLayout() .background(blackDrawable)) .child( Column.create(c) .child(TestDrawableComponent.create(c))) .build(); } }; final Component component3 = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .background(redDrawable) .foregroundRes(android.R.drawable.btn_default) .child(TestDrawableComponent.create(c) .withLayout() .background(redDrawable)) .child( Column.create(c) .child(TestDrawableComponent.create(c))) .build(); } }; ComponentTree componentTree = ComponentTree.create(mContext, component1) .incrementalMount(false) .layoutDiffing(false) .build(); LayoutState state = componentTree.calculateLayoutState( null, mContext, component1, SizeSpec.makeSizeSpec(10, SizeSpec.EXACTLY), SizeSpec.makeSizeSpec(10, SizeSpec.EXACTLY), true, false, null); assertEquals(LayoutOutput.STATE_UNKNOWN, state.getMountableOutputAt(2).getUpdateState()); LayoutState secondState = componentTree.calculateLayoutState( null, mContext, component2, SizeSpec.makeSizeSpec(10, SizeSpec.EXACTLY), SizeSpec.makeSizeSpec(10, SizeSpec.EXACTLY), true, false, state.getDiffTree()); assertEquals(LayoutOutput.STATE_UPDATED, secondState.getMountableOutputAt(2).getUpdateState()); LayoutState thirdState = componentTree.calculateLayoutState( null, mContext, component3, SizeSpec.makeSizeSpec(10, SizeSpec.EXACTLY), SizeSpec.makeSizeSpec(10, SizeSpec.EXACTLY), true, false, secondState.getDiffTree()); assertEquals(LayoutOutput.STATE_DIRTY, thirdState.getMountableOutputAt(2).getUpdateState()); } @Test public void testLayoutOutputUpdateStateIdClash() { final Component component1 = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .child( Column.create(c) .wrapInView() .child(TestDrawableComponent.create(c))) .child( Column.create(c) .wrapInView() .child(TestDrawableComponent.create(c))) .build(); } }; final Component component2 = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .child( Column.create(c) .wrapInView() .child(TestDrawableComponent.create(c)) .child(TestDrawableComponent.create(c))) .child( Column.create(c) .wrapInView() .child(TestDrawableComponent.create(c))) .build(); } }; ComponentTree componentTree = ComponentTree.create(mContext, component1) .incrementalMount(false) .layoutDiffing(false) .build(); LayoutState state = componentTree.calculateLayoutState( null, mContext, component1, SizeSpec.makeSizeSpec(10, SizeSpec.EXACTLY), SizeSpec.makeSizeSpec(10, SizeSpec.EXACTLY), true, false, null); assertOutputsState(state, LayoutOutput.STATE_UNKNOWN); LayoutState secondState = componentTree.calculateLayoutState( null, mContext, component2, SizeSpec.makeSizeSpec(10, SizeSpec.EXACTLY), SizeSpec.makeSizeSpec(10, SizeSpec.EXACTLY), true, false, state.getDiffTree()); assertEquals(secondState.getMountableOutputCount(), 6); assertEquals(secondState.getMountableOutputAt(0).getUpdateState(), LayoutOutput.STATE_DIRTY); assertEquals(secondState.getMountableOutputAt(1).getUpdateState(), LayoutOutput.STATE_UNKNOWN); assertEquals(secondState.getMountableOutputAt(2).getUpdateState(), LayoutOutput.STATE_UPDATED); assertEquals(secondState.getMountableOutputAt(3).getUpdateState(), LayoutOutput.STATE_UNKNOWN); assertEquals(secondState.getMountableOutputAt(4).getUpdateState(), LayoutOutput.STATE_UNKNOWN); assertEquals(secondState.getMountableOutputAt(5).getUpdateState(), LayoutOutput.STATE_UNKNOWN); } @Test public void testDiffTreeUsedIfRootMeasureSpecsAreDifferentButChildHasSame() { final TestComponent component = TestDrawableComponent.create(mContext) .color(Color.BLACK) .build(); final Component layoutComponent = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .alignItems(YogaAlign.FLEX_START) .child(Layout.create(c, component).heightPx(50)) .build(); } }; LayoutState firstLayoutState = LayoutState.calculate( mContext, layoutComponent, 0, SizeSpec.makeSizeSpec(100, SizeSpec.EXACTLY), SizeSpec.makeSizeSpec(100, SizeSpec.EXACTLY), true, false, null, false); assertTrue(component.wasMeasureCalled()); final TestComponent secondComponent = TestDrawableComponent.create(mContext) .color(Color.BLACK) .build(); final Component secondLayoutComponent = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .alignItems(YogaAlign.FLEX_START) .child(Layout.create(c, secondComponent).heightPx(50)) .build(); } }; LayoutState.calculate( mContext, secondLayoutComponent, 0, SizeSpec.makeSizeSpec(100, SizeSpec.EXACTLY), SizeSpec.makeSizeSpec(90, SizeSpec.EXACTLY), true, false, firstLayoutState.getDiffTree(), false); assertFalse(secondComponent.wasMeasureCalled()); } @Test public void testDiffTreeUsedIfMeasureSpecsAreSame() { final TestComponent component = TestDrawableComponent.create(mContext) .color(Color.BLACK) .build(); final Component layoutComponent = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .child(component) .build(); } }; LayoutState firstLayoutState = LayoutState.calculate( mContext, layoutComponent, 0, SizeSpec.makeSizeSpec(100, SizeSpec.EXACTLY), SizeSpec.makeSizeSpec(100, SizeSpec.EXACTLY), true, false, null, false); assertTrue(component.wasMeasureCalled()); final TestComponent secondComponent = TestDrawableComponent.create(mContext) .color(Color.BLACK) .build(); final Component secondLayoutComponent = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .child(secondComponent) .build(); } }; LayoutState.calculate( mContext, secondLayoutComponent, 0, SizeSpec.makeSizeSpec(100, SizeSpec.EXACTLY), SizeSpec.makeSizeSpec(100, SizeSpec.EXACTLY), true, false, firstLayoutState.getDiffTree(), false); assertFalse(secondComponent.wasMeasureCalled()); } @Test public void testCachedMeasuresForNestedTreeComponentDelegateWithUndefinedSize() { final Component component1 = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .paddingPx(YogaEdge.ALL, 2) .child( TestSizeDependentComponent.create(c) .setDelegate(true) .withLayout() .marginPx(YogaEdge.ALL, 11)) .build(); } }; final Component component2 = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .paddingPx(YogaEdge.ALL, 2) .child( TestSizeDependentComponent.create(c) .setDelegate(true) .withLayout() .marginPx(YogaEdge.ALL, 11)) .build(); } }; LayoutState prevLayoutState = LayoutState.calculate( mContext, component1, -1, SizeSpec.makeSizeSpec(350, SizeSpec.EXACTLY), SizeSpec.makeSizeSpec(200, SizeSpec.EXACTLY), true, false, null, false); LayoutState layoutState = LayoutState.calculate( mContext, component2, -1, SizeSpec.makeSizeSpec(350, SizeSpec.EXACTLY), SizeSpec.makeSizeSpec(200, SizeSpec.EXACTLY), true, false, prevLayoutState.getDiffTree(), false); // The nested root measure() was called in the first layout calculation. TestComponent prevNestedRoot = (TestComponent) prevLayoutState.getMountableOutputAt(2).getComponent(); assertTrue(prevNestedRoot.wasMeasureCalled()); TestComponent nestedRoot = (TestComponent) layoutState.getMountableOutputAt(2).getComponent(); assertFalse(nestedRoot.wasMeasureCalled()); } @Test public void testCachedMeasuresForNestedTreeComponentWithUndefinedSize() { final Component component1 = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .paddingPx(YogaEdge.ALL, 2) .child( TestDrawableComponent.create(c, false, true, true, false, false)) .child( TestSizeDependentComponent.create(c) .setDelegate(false) .withLayout() .flexShrink(0) .marginPx(YogaEdge.ALL, 11)) .build(); } }; final Component component2 = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .paddingPx(YogaEdge.ALL, 2) .child( TestDrawableComponent.create(c, false, true, true, false, false)) .child( TestSizeDependentComponent.create(c) .setDelegate(false) .withLayout() .flexShrink(0) .marginPx(YogaEdge.ALL, 11)) .build(); } }; LayoutState prevLayoutState = LayoutState.calculate( mContext, component1, -1, SizeSpec.makeSizeSpec(350, SizeSpec.EXACTLY), SizeSpec.makeSizeSpec(200, SizeSpec.EXACTLY), true, false, null, false); LayoutState layoutState = LayoutState.calculate( mContext, component2, -1, SizeSpec.makeSizeSpec(350, SizeSpec.EXACTLY), SizeSpec.makeSizeSpec(200, SizeSpec.EXACTLY), true, false, prevLayoutState.getDiffTree(), false); // The nested root measure() was called in the first layout calculation. TestComponent prevMainTreeLeaf = (TestComponent) prevLayoutState.getMountableOutputAt(1).getComponent(); assertTrue(prevMainTreeLeaf.wasMeasureCalled()); TestComponent prevNestedLeaf1 = (TestComponent) prevLayoutState.getMountableOutputAt(3).getComponent(); assertTrue(prevNestedLeaf1.wasMeasureCalled()); TestComponent prevNestedLeaf2 = (TestComponent) prevLayoutState.getMountableOutputAt(4).getComponent(); assertTrue(prevNestedLeaf2.wasMeasureCalled()); TestComponent mainTreeLeaf = (TestComponent) layoutState.getMountableOutputAt(1).getComponent(); assertFalse(mainTreeLeaf.wasMeasureCalled()); TestComponent nestedLeaf1 = (TestComponent) layoutState.getMountableOutputAt(3).getComponent(); assertFalse(nestedLeaf1.wasMeasureCalled()); TestComponent nestedLeaf2 = (TestComponent) layoutState.getMountableOutputAt(4).getComponent(); assertFalse(nestedLeaf2.wasMeasureCalled()); } @Test public void testCachedMeasuresForCachedLayoutSpecWithMeasure() { final ComponentContext c = new ComponentContext(RuntimeEnvironment.application); final int widthSpecContainer = SizeSpec.makeSizeSpec(300, SizeSpec.EXACTLY); final int heightSpec = SizeSpec.makeSizeSpec(40, SizeSpec.AT_MOST); final int horizontalPadding = 20; final int widthMeasuredComponent = SizeSpec.makeSizeSpec( SizeSpec.getSize(widthSpecContainer) - horizontalPadding - horizontalPadding, SizeSpec.EXACTLY); final Component<?> sizeDependentComponentSpy1 = PowerMockito.spy( TestSizeDependentComponent.create(c) .setFixSizes(false) .setDelegate(false) .build()); Size sizeOutput1 = new Size(); sizeDependentComponentSpy1.measure( c, widthMeasuredComponent, heightSpec, sizeOutput1); // Now embed the measured component in another container and calculate a layout. final Component rootContainer1 = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .paddingPx(YogaEdge.HORIZONTAL, horizontalPadding) .child(sizeDependentComponentSpy1) .build(); } }; final Component<?> sizeDependentComponentSpy2 = PowerMockito.spy( TestSizeDependentComponent.create(c) .setFixSizes(false) .setDelegate(false) .build()); Size sizeOutput2 = new Size(); sizeDependentComponentSpy1.measure( c, widthMeasuredComponent, heightSpec, sizeOutput2); // Now embed the measured component in another container and calculate a layout. final Component rootContainer2 = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .paddingPx(YogaEdge.HORIZONTAL, horizontalPadding) .child(sizeDependentComponentSpy2) .build(); } }; LayoutState prevLayoutState = LayoutState.calculate( mContext, rootContainer1, -1, widthSpecContainer, heightSpec, true, false, null, false); // Make sure we reused the cached layout and it wasn't released. verify(sizeDependentComponentSpy1, never()).releaseCachedLayout(); verify(sizeDependentComponentSpy1, times(1)).clearCachedLayout(); LayoutState layoutState = LayoutState.calculate( mContext, rootContainer2, -1, widthSpecContainer, heightSpec, true, false, prevLayoutState.getDiffTree(), false); // Make sure we reused the cached layout and it wasn't released. verify(sizeDependentComponentSpy2, never()).releaseCachedLayout(); verify(sizeDependentComponentSpy2, never()).clearCachedLayout(); // The nested root measure() was called in the first layout calculation. TestComponent prevNestedLeaf1 = (TestComponent) prevLayoutState.getMountableOutputAt(2).getComponent(); assertTrue(prevNestedLeaf1.wasMeasureCalled()); TestComponent prevNestedLeaf2 = (TestComponent) prevLayoutState.getMountableOutputAt(3).getComponent(); assertTrue(prevNestedLeaf2.wasMeasureCalled()); TestComponent nestedLeaf1 = (TestComponent) layoutState.getMountableOutputAt(2).getComponent(); assertFalse(nestedLeaf1.wasMeasureCalled()); TestComponent nestedLeaf2 = (TestComponent) layoutState.getMountableOutputAt(3).getComponent(); assertFalse(nestedLeaf2.wasMeasureCalled()); } private static void assertOutputsState( LayoutState layoutState, @LayoutOutput.UpdateState int state) { assertEquals(layoutState.getMountableOutputAt(0).getUpdateState(), LayoutOutput.STATE_DIRTY); for (int i = 1; i < layoutState.getMountableOutputCount(); i++) { LayoutOutput output = layoutState.getMountableOutputAt(i); assertEquals(output.getUpdateState(), state); } } private static void assertCachedMeasurementsDefined(InternalNode node) { float diffHeight = node.getDiffNode() == null ? -1 : node.getDiffNode().getLastMeasuredHeight(); float diffWidth = node.getDiffNode() == null ? -1 : node.getDiffNode().getLastMeasuredWidth(); assertTrue(diffHeight != -1); assertTrue(diffWidth != -1); assertTrue(node.areCachedMeasuresValid()); } }