void handleUpdateProperties(ReactStylesDiffMap styles) { if (!mountsToView()) { // Make sure we mount this FlatShadowNode to a View if any of these properties are present. if (styles.hasKey(PROP_OPACITY) || styles.hasKey(PROP_RENDER_TO_HARDWARE_TEXTURE) || styles.hasKey(PROP_TEST_ID) || styles.hasKey(PROP_ACCESSIBILITY_LABEL) || styles.hasKey(PROP_ACCESSIBILITY_COMPONENT_TYPE) || styles.hasKey(PROP_ACCESSIBILITY_LIVE_REGION) || styles.hasKey(PROP_TRANSFORM) || styles.hasKey(PROP_IMPORTANT_FOR_ACCESSIBILITY) || styles.hasKey(PROP_REMOVE_CLIPPED_SUBVIEWS)) { forceMountToView(); } } }
@Override protected void handleCreateView( ReactShadowNode cssNode, int rootViewTag, @Nullable ReactStylesDiffMap styles) { if (cssNode instanceof FlatShadowNode) { FlatShadowNode node = (FlatShadowNode) cssNode; if (styles != null) { node.handleUpdateProperties(styles); } if (node.mountsToView()) { mStateBuilder.enqueueCreateOrUpdateView(node, styles); } } else { super.handleCreateView(cssNode, rootViewTag, styles); } }
@Override protected void handleUpdateView( ReactShadowNode cssNode, String className, ReactStylesDiffMap styles) { if (cssNode instanceof FlatShadowNode) { FlatShadowNode node = (FlatShadowNode) cssNode; node.handleUpdateProperties(styles); if (node.mountsToView()) { mStateBuilder.enqueueCreateOrUpdateView(node, styles); } } else { super.handleUpdateView(cssNode, className, styles); } }
/** * Create a backing view for a node, or update the backing view if it has already been created. * * @param node The node to create the backing view for. * @param styles Styles for the view. */ /* package */ void enqueueCreateOrUpdateView( FlatShadowNode node, @Nullable ReactStylesDiffMap styles) { if (node.isBackingViewCreated()) { // If the View is already created, make sure to propagate the new styles. mOperationsQueue.enqueueUpdateProperties( node.getReactTag(), node.getViewClass(), styles); } else { mOperationsQueue.enqueueCreateView( node.getThemedContext(), node.getReactTag(), node.getViewClass(), styles); node.signalBackingViewIsCreated(); } }
public final void updateView(UIImplementation uiImplementation) { if (mConnectedViewTag == -1) { throw new IllegalStateException("Node has not been attached to a view"); } JavaOnlyMap propsMap = new JavaOnlyMap(); for (Map.Entry<String, Integer> entry : mPropMapping.entrySet()) { @Nullable AnimatedNode node = mNativeAnimatedNodesManager.getNodeById(entry.getValue()); if (node == null) { throw new IllegalArgumentException("Mapped property node does not exists"); } else if (node instanceof StyleAnimatedNode) { ((StyleAnimatedNode) node).collectViewUpdates(propsMap); } else if (node instanceof ValueAnimatedNode) { propsMap.putDouble(entry.getKey(), ((ValueAnimatedNode) node).getValue()); } else { throw new IllegalArgumentException("Unsupported type of node used in property node " + node.getClass()); } } // TODO: Reuse propsMap and stylesDiffMap objects - note that in subsequent animation steps // for a given node most of the time we will be creating the same set of props (just with // different values). We can take advantage on that and optimize the way we allocate property // maps (we also know that updating view props doesn't retain a reference to the styles object). uiImplementation.synchronouslyUpdateViewOnUIThread( mConnectedViewTag, new ReactStylesDiffMap(propsMap)); }
@Test public void testNativeAnimatedEventDoUpdate() { int viewTag = 1000; createSimpleAnimatedViewWithOpacity(viewTag, 0d); mNativeAnimatedNodesManager.addAnimatedEventToView(viewTag, "topScroll", JavaOnlyMap.of( "animatedValueTag", 1, "nativeEventPath", JavaOnlyArray.of("contentOffset", "y"))); mNativeAnimatedNodesManager.onEventDispatch(createScrollEvent(viewTag, 10)); ArgumentCaptor<ReactStylesDiffMap> stylesCaptor = ArgumentCaptor.forClass(ReactStylesDiffMap.class); reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(viewTag), stylesCaptor.capture()); assertThat(stylesCaptor.getValue().getDouble("opacity", Double.NaN)).isEqualTo(10); }
@Test public void testNativeAnimatedEventDoNotUpdate() { int viewTag = 1000; createSimpleAnimatedViewWithOpacity(viewTag, 0d); mNativeAnimatedNodesManager.addAnimatedEventToView(viewTag, "otherEvent", JavaOnlyMap.of( "animatedValueTag", 1, "nativeEventPath", JavaOnlyArray.of("contentOffset", "y"))); mNativeAnimatedNodesManager.addAnimatedEventToView(999, "topScroll", JavaOnlyMap.of( "animatedValueTag", 1, "nativeEventPath", JavaOnlyArray.of("contentOffset", "y"))); mNativeAnimatedNodesManager.onEventDispatch(createScrollEvent(viewTag, 10)); ArgumentCaptor<ReactStylesDiffMap> stylesCaptor = ArgumentCaptor.forClass(ReactStylesDiffMap.class); reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(viewTag), stylesCaptor.capture()); assertThat(stylesCaptor.getValue().getDouble("opacity", Double.NaN)).isEqualTo(0); }
public final void updateView(UIImplementation uiImplementation) { if (mConnectedViewTag == -1) { throw new IllegalStateException("Node has not been attached to a view"); } JavaOnlyMap propsMap = new JavaOnlyMap(); for (Map.Entry<String, Integer> entry : mPropMapping.entrySet()) { @Nullable AnimatedNode node = mNativeAnimatedNodesManager.getNodeById(entry.getValue()); if (node == null) { throw new IllegalArgumentException("Mapped property node does not exists"); } else if (node instanceof StyleAnimatedNode) { ((StyleAnimatedNode) node).collectViewUpdates(propsMap); } else if (node instanceof ValueAnimatedNode) { propsMap.putDouble(entry.getKey(), ((ValueAnimatedNode) node).mValue); } else { throw new IllegalArgumentException("Unsupported type of node used in property node " + node.getClass()); } } // TODO: Reuse propsMap and stylesDiffMap objects - note that in subsequent animation steps // for a given node most of the time we will be creating the same set of props (just with // different values). We can take advantage on that and optimize the way we allocate property // maps (we also know that updating view props doesn't retain a reference to the styles object). uiImplementation.synchronouslyUpdateViewOnUIThread( mConnectedViewTag, new ReactStylesDiffMap(propsMap)); }
@Override /* package */ void handleUpdateProperties(ReactStylesDiffMap styles) { mRemoveClippedSubviews = mRemoveClippedSubviews || (styles.hasKey(PROP_REMOVE_CLIPPED_SUBVIEWS) && styles.getBoolean(PROP_REMOVE_CLIPPED_SUBVIEWS, false)); if (mRemoveClippedSubviews) { mHorizontal = mHorizontal || (styles.hasKey(PROP_HORIZONTAL) && styles.getBoolean(PROP_HORIZONTAL, false)); } super.handleUpdateProperties(styles); }
@Test public void testFramesAnimation() { createSimpleAnimatedViewWithOpacity(1000, 0d); JavaOnlyArray frames = JavaOnlyArray.of(0d, 0.2d, 0.4d, 0.6d, 0.8d, 1d); Callback animationCallback = mock(Callback.class); mNativeAnimatedNodesManager.startAnimatingNode( 1, 1, JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 1d), animationCallback); ArgumentCaptor<ReactStylesDiffMap> stylesCaptor = ArgumentCaptor.forClass(ReactStylesDiffMap.class); reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture()); assertThat(stylesCaptor.getValue().getDouble("opacity", Double.NaN)).isEqualTo(0); for (int i = 0; i < frames.size(); i++) { reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); verify(mUIImplementationMock) .synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture()); assertThat(stylesCaptor.getValue().getDouble("opacity", Double.NaN)) .isEqualTo(frames.getDouble(i)); } reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); verifyNoMoreInteractions(mUIImplementationMock); }
@Test public void testFramesAnimationLoopsFiveTimes() { createSimpleAnimatedViewWithOpacity(1000, 0d); JavaOnlyArray frames = JavaOnlyArray.of(0d, 0.2d, 0.4d, 0.6d, 0.8d, 1d); Callback animationCallback = mock(Callback.class); mNativeAnimatedNodesManager.startAnimatingNode( 1, 1, JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 1d, "iterations", 5), animationCallback); ArgumentCaptor<ReactStylesDiffMap> stylesCaptor = ArgumentCaptor.forClass(ReactStylesDiffMap.class); reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture()); assertThat(stylesCaptor.getValue().getDouble("opacity", Double.NaN)).isEqualTo(0); for (int iteration = 0; iteration < 5; iteration++) { for (int i = 0; i < frames.size(); i++) { reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); verify(mUIImplementationMock) .synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture()); assertThat(stylesCaptor.getValue().getDouble("opacity", Double.NaN)) .isEqualTo(frames.getDouble(i)); } } reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); verifyNoMoreInteractions(mUIImplementationMock); }
/** * Verifies that {@link NativeAnimatedNodesManager#runUpdates} updates the view correctly in case * when one of the addition input nodes has started animating while the other one has not. * * We expect that the output of the addition node will take the starting value of the second input * node even though the node hasn't been connected to an active animation driver. */ @Test public void testViewReceiveUpdatesIfOneOfAnimationHasntStarted() { createAnimatedGraphWithAdditionNode(50, 100d, 1000d); // Start animating only the first addition input node Callback animationCallback = mock(Callback.class); JavaOnlyArray frames = JavaOnlyArray.of(0d, 1d); mNativeAnimatedNodesManager.startAnimatingNode( 1, 1, JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 101d), animationCallback); ArgumentCaptor<ReactStylesDiffMap> stylesCaptor = ArgumentCaptor.forClass(ReactStylesDiffMap.class); reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture()); assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(1100d); reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); verify(mUIImplementationMock) .synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture()); assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(1100d); reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); verify(mUIImplementationMock) .synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture()); assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(1101d); reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); verifyNoMoreInteractions(mUIImplementationMock); }
@Test public void testNativeAnimatedEventCustomMapping() { int viewTag = 1000; PowerMockito.when(mUIManagerMock.getConstants()).thenAnswer(new Answer<Object>() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { return MapBuilder.of("customDirectEventTypes", MapBuilder.of( "topScroll", MapBuilder.of("registrationName", "onScroll") )); } }); mNativeAnimatedNodesManager = new NativeAnimatedNodesManager(mUIManagerMock); createSimpleAnimatedViewWithOpacity(viewTag, 0d); mNativeAnimatedNodesManager.addAnimatedEventToView(viewTag, "onScroll", JavaOnlyMap.of( "animatedValueTag", 1, "nativeEventPath", JavaOnlyArray.of("contentOffset", "y"))); mNativeAnimatedNodesManager.onEventDispatch(createScrollEvent(viewTag, 10)); ArgumentCaptor<ReactStylesDiffMap> stylesCaptor = ArgumentCaptor.forClass(ReactStylesDiffMap.class); reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(viewTag), stylesCaptor.capture()); assertThat(stylesCaptor.getValue().getDouble("opacity", Double.NaN)).isEqualTo(10); }
@Override /* package*/ void handleUpdateProperties(ReactStylesDiffMap styles) { if (mReactShadowNode != null) { mReactShadowNode.updateProperties(styles); } }
public ReactStylesDiffMap buildStyles(Object... keysAndValues) { return new ReactStylesDiffMap(JavaOnlyMap.of(keysAndValues)); }
@Test public void testSpringAnimation() { createSimpleAnimatedViewWithOpacity(1000, 0d); Callback animationCallback = mock(Callback.class); mNativeAnimatedNodesManager.startAnimatingNode( 1, 1, JavaOnlyMap.of( "type", "spring", "friction", 7d, "tension", 40.0d, "initialVelocity", 0d, "toValue", 1d, "restSpeedThreshold", 0.001d, "restDisplacementThreshold", 0.001d, "overshootClamping", false), animationCallback); ArgumentCaptor<ReactStylesDiffMap> stylesCaptor = ArgumentCaptor.forClass(ReactStylesDiffMap.class); reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture()); assertThat(stylesCaptor.getValue().getDouble("opacity", Double.NaN)).isEqualTo(0); double previousValue = 0d; boolean wasGreaterThanOne = false; /* run 3 secs of animation */ for (int i = 0; i < 3 * 60; i++) { reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); verify(mUIImplementationMock, atMost(1)) .synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture()); double currentValue = stylesCaptor.getValue().getDouble("opacity", Double.NaN); if (currentValue > 1d) { wasGreaterThanOne = true; } // verify that animation step is relatively small assertThat(Math.abs(currentValue - previousValue)).isLessThan(0.1d); previousValue = currentValue; } // verify that we've reach the final value at the end of animation assertThat(previousValue).isEqualTo(1d); // verify that value has reached some maximum value that is greater than the final value (bounce) assertThat(wasGreaterThanOne); reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); verifyNoMoreInteractions(mUIImplementationMock); }
@Test public void testSpringAnimationLoopsFiveTimes() { createSimpleAnimatedViewWithOpacity(1000, 0d); Callback animationCallback = mock(Callback.class); mNativeAnimatedNodesManager.startAnimatingNode( 1, 1, JavaOnlyMap.of( "type", "spring", "friction", 7d, "tension", 40.0d, "initialVelocity", 0d, "toValue", 1d, "restSpeedThreshold", 0.001d, "restDisplacementThreshold", 0.001d, "overshootClamping", false, "iterations", 5), animationCallback); ArgumentCaptor<ReactStylesDiffMap> stylesCaptor = ArgumentCaptor.forClass(ReactStylesDiffMap.class); reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture()); assertThat(stylesCaptor.getValue().getDouble("opacity", Double.NaN)).isEqualTo(0); double previousValue = 0d; boolean wasGreaterThanOne = false; boolean didComeToRest = false; int numberOfResets = 0; /* run 3 secs of animation, five times */ for (int i = 0; i < 3 * 60 * 5; i++) { reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); verify(mUIImplementationMock, atMost(1)) .synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture()); double currentValue = stylesCaptor.getValue().getDouble("opacity", Double.NaN); if (currentValue > 1d) { wasGreaterThanOne = true; } // Test to see if it reset after coming to rest if (didComeToRest && currentValue == 0d && Math.abs(Math.abs(currentValue - previousValue) - 1d) < 0.001d) { numberOfResets++; } // verify that an animation step is relatively small, unless it has come to rest and reset if (!didComeToRest) assertThat(Math.abs(currentValue - previousValue)).isLessThan(0.1d); // record that the animation did come to rest when it rests on toValue didComeToRest = Math.abs(currentValue - 1d) < 0.001d && Math.abs(currentValue - previousValue) < 0.001d; previousValue = currentValue; } // verify that we've reach the final value at the end of animation assertThat(previousValue).isEqualTo(1d); // verify that value has reached some maximum value that is greater than the final value (bounce) assertThat(wasGreaterThanOne); // verify that value reset 4 times after finishing a full animation assertThat(numberOfResets).isEqualTo(4); reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); verifyNoMoreInteractions(mUIImplementationMock); }
@Test public void testDecayAnimation() { createSimpleAnimatedViewWithOpacity(1000, 0d); Callback animationCallback = mock(Callback.class); mNativeAnimatedNodesManager.startAnimatingNode( 1, 1, JavaOnlyMap.of( "type", "decay", "velocity", 0.5d, "deceleration", 0.998d), animationCallback); ArgumentCaptor<ReactStylesDiffMap> stylesCaptor = ArgumentCaptor.forClass(ReactStylesDiffMap.class); reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); verify(mUIImplementationMock, atMost(1)) .synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture()); double previousValue = stylesCaptor.getValue().getDouble("opacity", Double.NaN); double previousDiff = Double.POSITIVE_INFINITY; /* run 3 secs of animation */ for (int i = 0; i < 3 * 60; i++) { reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); verify(mUIImplementationMock, atMost(1)) .synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture()); double currentValue = stylesCaptor.getValue().getDouble("opacity", Double.NaN); double currentDiff = currentValue - previousValue; // verify monotonicity // greater *or equal* because the animation stops during these 3 seconds assertThat(currentValue).as("on frame " + i).isGreaterThanOrEqualTo(previousValue); // verify decay if (i > 3) { // i > 3 because that's how long it takes to settle previousDiff if (i % 3 != 0) { // i % 3 != 0 because every 3 frames we go a tiny // bit faster, because frame length is 16.(6)ms assertThat(currentDiff).as("on frame " + i).isLessThanOrEqualTo(previousDiff); } else { assertThat(currentDiff).as("on frame " + i).isGreaterThanOrEqualTo(previousDiff); } } previousValue = currentValue; previousDiff = currentDiff; } // should be done in 3s reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); verifyNoMoreInteractions(mUIImplementationMock); }