diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/BUCK b/ReactAndroid/src/main/java/com/facebook/react/animated/BUCK new file mode 100644 index 000000000..73444282c --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/BUCK @@ -0,0 +1,24 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'animated', + srcs = glob([ + '*.java', + ]), + deps = [ + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/uimanager:uimanager'), + + react_native_target('java/com/facebook/react/uimanager/annotations:annotations'), + react_native_dep('third-party/java/infer-annotations:infer-annotations'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + react_native_dep('third-party/android/support/v4:lib-support-v4'), + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':animated', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java index bdb833f27..6778d739d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java @@ -9,7 +9,7 @@ package com.facebook.react.animated; -import android.support.annotation.Nullable; +import javax.annotation.Nullable; import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.Callback; @@ -20,7 +20,6 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.uimanager.GuardedChoreographerFrameCallback; -import com.facebook.react.uimanager.NativeViewHierarchyManager; import com.facebook.react.uimanager.ReactChoreographer; import com.facebook.react.uimanager.UIImplementation; import com.facebook.react.uimanager.UIManagerModule; diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java index 185716b36..9288fd6b1 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java @@ -17,13 +17,14 @@ import com.facebook.react.bridge.JSApplicationIllegalArgumentException; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.bridge.WritableMap; -import com.facebook.react.uimanager.NativeViewHierarchyManager; import com.facebook.react.uimanager.UIImplementation; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Queue; +import javax.annotation.Nullable; + /** * This is the main class that coordinates how native animated JS implementation drives UI changes. * @@ -49,12 +50,12 @@ import java.util.Queue; mUIImplementation = uiImplementation; } - /*package*/ AnimatedNode getNodeById(int id) { + /*package*/ @Nullable AnimatedNode getNodeById(int id) { return mAnimatedNodes.get(id); } public boolean hasActiveAnimations() { - return !mActiveAnimations.isEmpty(); + return !mActiveAnimations.isEmpty() || !mUpdatedNodes.isEmpty(); } public void createAnimatedNode(int tag, ReadableMap config) { @@ -315,6 +316,8 @@ import java.util.Queue; + activeNodesCount + " but toposort visited only " + updatedNodesCount); } + // Clean mUpdatedNodes queue + mUpdatedNodes.clear(); // Cleanup finished animations. Iterate over the array of animations and override ones that has // finished, then resize `mActiveAnimations`. diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.java b/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.java index c3d9667ce..d0c9f09e0 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.java @@ -20,6 +20,8 @@ import com.facebook.react.uimanager.UIManagerModule; import java.util.HashMap; import java.util.Map; +import javax.annotation.Nullable; + /** * Animated node that represents view properties. There is a special handling logic implemented for * the nodes of this type in {@link NativeAnimatedNodesManager} that is responsible for extracting @@ -50,7 +52,7 @@ import java.util.Map; } JavaOnlyMap propsMap = new JavaOnlyMap(); for (Map.Entry entry : mPropMapping.entrySet()) { - AnimatedNode node = mNativeAnimatedNodesManager.getNodeById(entry.getValue()); + @Nullable AnimatedNode node = mNativeAnimatedNodesManager.getNodeById(entry.getValue()); if (node == null) { throw new IllegalArgumentException("Mapped property node does not exists"); } else if (node instanceof StyleAnimatedNode) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/StyleAnimatedNode.java b/ReactAndroid/src/main/java/com/facebook/react/animated/StyleAnimatedNode.java index 7841fd215..de7c1b054 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/StyleAnimatedNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/StyleAnimatedNode.java @@ -16,6 +16,8 @@ import com.facebook.react.bridge.ReadableMapKeySetIterator; import java.util.HashMap; import java.util.Map; +import javax.annotation.Nullable; + /** * Native counterpart of style animated node (see AnimatedStyle class in AnimatedImplementation.js) */ @@ -38,7 +40,7 @@ import java.util.Map; public void collectViewUpdates(JavaOnlyMap propsMap) { for (Map.Entry entry : mPropMapping.entrySet()) { - AnimatedNode node = mNativeAnimatedNodesManager.getNodeById(entry.getValue()); + @Nullable AnimatedNode node = mNativeAnimatedNodesManager.getNodeById(entry.getValue()); if (node == null) { throw new IllegalArgumentException("Mapped style node does not exists"); } else if (node instanceof ValueAnimatedNode) { diff --git a/ReactAndroid/src/test/java/com/facebook/react/animated/BUCK b/ReactAndroid/src/test/java/com/facebook/react/animated/BUCK new file mode 100644 index 000000000..b6f4ea8d0 --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/animated/BUCK @@ -0,0 +1,29 @@ +include_defs('//ReactAndroid/DEFS') + +robolectric3_test( + name = 'animated', + # Please change the contact to the oncall of your team + contacts = ['oncall+fbandroid_sheriff@xmail.facebook.com'], + srcs = glob(['**/*.java']), + deps = [ + react_native_target('java/com/facebook/react/animated:animated'), + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/uimanager:uimanager'), + react_native_target('java/com/facebook/react:react'), + + react_native_tests_target('java/com/facebook/react/bridge:testhelpers'), + react_native_dep('libraries/fbcore/src/test/java/com/facebook/powermock:powermock'), + react_native_dep('third-party/java/fest:fest'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + react_native_dep('third-party/java/junit:junit'), + react_native_dep('third-party/java/mockito:mockito'), + react_native_dep('third-party/java/robolectric3/robolectric:robolectric'), + ], + visibility = [ + 'PUBLIC' + ], +) + +project_config( + test_target = ':animated', +) diff --git a/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java b/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java new file mode 100644 index 000000000..7e510c1ee --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java @@ -0,0 +1,168 @@ +/** + * Copyright (c) 2015-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.react.animated; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.JavaOnlyArray; +import com.facebook.react.bridge.JavaOnlyMap; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.uimanager.ReactStylesDiffMap; +import com.facebook.react.uimanager.UIImplementation; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +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.robolectric.RobolectricTestRunner; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * Tests the animated nodes graph traversal algorithm from {@link NativeAnimatedNodesManager}. + */ +@PrepareForTest({Arguments.class}) +@RunWith(RobolectricTestRunner.class) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) +public class NativeAnimatedNodeTraversalTest { + + private static long FRAME_LEN_NANOS = 1000000000L / 60L; + private static long INITIAL_FRAME_TIME_NANOS = 14599233201256L; /* random */ + + @Rule + public PowerMockRule rule = new PowerMockRule(); + + private long mFrameTimeNanos; + private UIImplementation mUIImplementationMock; + private NativeAnimatedNodesManager mNativeAnimatedNodesManager; + + private long nextFrameTime() { + return mFrameTimeNanos += FRAME_LEN_NANOS; + } + + @Before + public void setUp() { + PowerMockito.mockStatic(Arguments.class); + PowerMockito.when(Arguments.createArray()).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return new JavaOnlyArray(); + } + }); + PowerMockito.when(Arguments.createMap()).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return new JavaOnlyMap(); + } + }); + + mFrameTimeNanos = INITIAL_FRAME_TIME_NANOS; + mUIImplementationMock = mock(UIImplementation.class); + mNativeAnimatedNodesManager = new NativeAnimatedNodesManager(mUIImplementationMock); + } + + /** + * Generates a simple animated nodes graph and attaches the props node to a given {@param viewTag} + * Parameter {@param opacity} is used as a initial value for the "opacity" attribute. + * + * Nodes are connected as follows (nodes IDs in parens): + * ValueNode(1) -> StyleNode(2) -> PropNode(3) + */ + private void createSimpleAnimatedViewWithOpacity(int viewTag, double opacity) { + mNativeAnimatedNodesManager.createAnimatedNode( + 1, + JavaOnlyMap.of("type", "value", "value", opacity)); + mNativeAnimatedNodesManager.createAnimatedNode( + 2, + JavaOnlyMap.of("type", "style", "style", JavaOnlyMap.of("opacity", 1))); + mNativeAnimatedNodesManager.createAnimatedNode( + 3, + JavaOnlyMap.of("type", "props", "props", JavaOnlyMap.of("style", 2))); + mNativeAnimatedNodesManager.connectAnimatedNodes(1, 2); + mNativeAnimatedNodesManager.connectAnimatedNodes(2, 3); + mNativeAnimatedNodesManager.connectAnimatedNodeToView(3, viewTag); + } + + @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, + JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 1d), + animationCallback); + + ArgumentCaptor 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 testAnimationCallbackFinish() { + createSimpleAnimatedViewWithOpacity(1000, 0d); + + JavaOnlyArray frames = JavaOnlyArray.of(0d, 1d); + Callback animationCallback = mock(Callback.class); + mNativeAnimatedNodesManager.startAnimatingNode( + 1, + JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 1d), + animationCallback); + + ArgumentCaptor callbackResponseCaptor = ArgumentCaptor.forClass(ReadableMap.class); + + reset(animationCallback); + mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); + mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); + verifyNoMoreInteractions(animationCallback); + + reset(animationCallback); + mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); + verify(animationCallback).invoke(callbackResponseCaptor.capture()); + + assertThat(callbackResponseCaptor.getValue().hasKey("finished")).isTrue(); + assertThat(callbackResponseCaptor.getValue().getBoolean("finished")).isTrue(); + + reset(animationCallback); + mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); + verifyNoMoreInteractions(animationCallback); + } +}