This commit is contained in:
Yamozha
2021-04-02 02:24:13 +03:00
parent c23950b545
commit 7256d79e2c
31493 changed files with 3036630 additions and 0 deletions

15
node_modules/react-native-reanimated/android/README.md generated vendored Normal file
View File

@ -0,0 +1,15 @@
README
======
If you want to publish the lib as a maven dependency, follow these steps before publishing a new version to npm:
1. Be sure to have the Android [SDK](https://developer.android.com/studio/index.html) and [NDK](https://developer.android.com/ndk/guides/index.html) installed
2. Be sure to have a `local.properties` file in this folder that points to the Android SDK and NDK
```
ndk.dir=/Users/{username}/Library/Android/sdk/ndk-bundle
sdk.dir=/Users/{username}/Library/Android/sdk
```
3. Delete the `maven` folder
4. Run `sudo ./gradlew installArchives`
5. Verify that latest set of generated files is in the maven folder with the correct version number

View File

@ -0,0 +1,133 @@
def safeExtGet(prop, fallback) {
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
buildscript {
if (project == rootProject) {
// The Android Gradle plugin is only required when opening the android folder stand-alone.
// This avoids unnecessary downloads and potential conflicts when the library is included as a
// module dependency in an application project.
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.2'
}
}
}
apply plugin: 'com.android.library'
apply plugin: 'maven'
android {
compileSdkVersion safeExtGet('compileSdkVersion', 28)
buildToolsVersion safeExtGet('buildToolsVersion', '28.0.3')
defaultConfig {
minSdkVersion safeExtGet('minSdkVersion', 16)
targetSdkVersion safeExtGet('targetSdkVersion', 28)
versionCode 1
versionName "1.0"
}
lintOptions {
abortOnError false
}
}
repositories {
mavenLocal()
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url "$rootDir/../node_modules/react-native/android"
}
maven {
// Android JSC is installed from npm
url "$rootDir/../node_modules/jsc-android/dist"
}
google()
jcenter()
}
dependencies {
//noinspection GradleDynamicVersion
implementation 'com.facebook.react:react-native:+' // From node_modules
implementation "androidx.transition:transition:1.1.0"
}
def configureReactNativePom(def pom) {
def packageJson = new groovy.json.JsonSlurper().parseText(file('../package.json').text)
pom.project {
name packageJson.title
artifactId packageJson.name
version = packageJson.version
group = "com.swmansion.reanimated"
description packageJson.description
url packageJson.repository.baseUrl
licenses {
license {
name packageJson.license
url packageJson.repository.baseUrl + '/blob/master/' + packageJson.licenseFilename
distribution 'repo'
}
}
developers {
developer {
id packageJson.author.username
name packageJson.author.name
}
}
}
}
afterEvaluate { project ->
task androidJavadoc(type: Javadoc) {
source = android.sourceSets.main.java.srcDirs
classpath += files(android.bootClasspath)
classpath += files(project.getConfigurations().getByName('compile').asList())
include '**/*.java'
}
task androidJavadocJar(type: Jar, dependsOn: androidJavadoc) {
classifier = 'javadoc'
from androidJavadoc.destinationDir
}
task androidSourcesJar(type: Jar) {
classifier = 'sources'
from android.sourceSets.main.java.srcDirs
include '**/*.java'
}
android.libraryVariants.all { variant ->
def compileTask
if (variant.hasProperty('javaCompileProvider')){
compileTask = variant.javaCompileProvider.get()
}else{
compileTask = variant.javaCompile
}
def name = variant.name.capitalize()
task "jar${name}"(type: Jar, dependsOn: compileTask) {
from compileTask.destinationDir
}
}
artifacts {
archives androidSourcesJar
archives androidJavadocJar
}
task installArchives(type: Upload) {
configuration = configurations.archives
repositories.mavenDeployer {
// Deploy to react-native-event-bridge/maven, ready to publish to npm
repository url: "file://${projectDir}/../android/maven"
configureReactNativePom pom
}
}
}

View File

@ -0,0 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.swmansion.reanimated">
</manifest>

View File

@ -0,0 +1,14 @@
package com.facebook.react.uimanager;
/**
* This class provides a way to workaround limited visibility of UIViewOperationQueue#getUIViewOperationQueue.
* We rely on accessing that method to check if operation queue is empty or not. This in turn indicates if
* we are in a middle of processing batch of operations from JS. In such a case we can rely on the enqueued update
* operations to be flushed onto the shadow view hierarchy. Otherwise we want to trigger "dispatchViewUpdates" and
* enforce flush immediately.
*/
public class UIManagerReanimatedHelper {
public static boolean isOperationQueueEmpty(UIImplementation uiImplementation) {
return uiImplementation.getUIViewOperationQueue().isEmpty();
}
}

View File

@ -0,0 +1,28 @@
package com.swmansion.reanimated;
import com.facebook.react.bridge.JSApplicationCausedNativeException;
import com.facebook.react.bridge.NoSuchKeyException;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableType;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class MapUtils {
public static int getInt(ReadableMap map, @Nonnull String name, String errorMsg) {
try {
return map.getInt(name);
} catch (NoSuchKeyException e) {
throw new JSApplicationCausedNativeException(errorMsg);
}
}
@Nullable
public static String getString(ReadableMap map, @Nonnull String name, String errorMsg) {
try {
return map.getString(name);
} catch (NoSuchKeyException e) {
throw new JSApplicationCausedNativeException(errorMsg);
}
}
}

View File

@ -0,0 +1,414 @@
package com.swmansion.reanimated;
import android.util.SparseArray;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.GuardedRunnable;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.modules.core.ReactChoreographer;
import com.facebook.react.uimanager.GuardedFrameCallback;
import com.facebook.react.uimanager.ReactShadowNode;
import com.facebook.react.uimanager.UIImplementation;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.UIManagerReanimatedHelper;
import com.facebook.react.uimanager.events.Event;
import com.facebook.react.uimanager.events.EventDispatcherListener;
import com.swmansion.reanimated.nodes.AlwaysNode;
import com.swmansion.reanimated.nodes.BezierNode;
import com.swmansion.reanimated.nodes.BlockNode;
import com.swmansion.reanimated.nodes.ClockNode;
import com.swmansion.reanimated.nodes.ClockOpNode;
import com.swmansion.reanimated.nodes.ConcatNode;
import com.swmansion.reanimated.nodes.CondNode;
import com.swmansion.reanimated.nodes.DebugNode;
import com.swmansion.reanimated.nodes.EventNode;
import com.swmansion.reanimated.nodes.JSCallNode;
import com.swmansion.reanimated.nodes.Node;
import com.swmansion.reanimated.nodes.NoopNode;
import com.swmansion.reanimated.nodes.OperatorNode;
import com.swmansion.reanimated.nodes.PropsNode;
import com.swmansion.reanimated.nodes.SetNode;
import com.swmansion.reanimated.nodes.StyleNode;
import com.swmansion.reanimated.nodes.TransformNode;
import com.swmansion.reanimated.nodes.ValueNode;
import com.swmansion.reanimated.nodes.ParamNode;
import com.swmansion.reanimated.nodes.FunctionNode;
import com.swmansion.reanimated.nodes.CallFuncNode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
public class NodesManager implements EventDispatcherListener {
private static final Double ZERO = Double.valueOf(0);
public interface OnAnimationFrame {
void onAnimationFrame();
}
private final SparseArray<Node> mAnimatedNodes = new SparseArray<>();
private final Map<String, EventNode> mEventMapping = new HashMap<>();
private final UIImplementation mUIImplementation;
private final DeviceEventManagerModule.RCTDeviceEventEmitter mEventEmitter;
private final ReactChoreographer mReactChoreographer;
private final GuardedFrameCallback mChoreographerCallback;
private final UIManagerModule.CustomEventNamesResolver mCustomEventNamesResolver;
private final AtomicBoolean mCallbackPosted = new AtomicBoolean();
private final NoopNode mNoopNode;
private final ReactContext mContext;
private final UIManagerModule mUIManager;
private List<OnAnimationFrame> mFrameCallbacks = new ArrayList<>();
private ConcurrentLinkedQueue<Event> mEventQueue = new ConcurrentLinkedQueue<>();
private boolean mWantRunUpdates;
public double currentFrameTimeMs;
public final UpdateContext updateContext;
public Set<String> uiProps = Collections.emptySet();
public Set<String> nativeProps = Collections.emptySet();
private final class NativeUpdateOperation {
public int mViewTag;
public WritableMap mNativeProps;
public NativeUpdateOperation(int viewTag, WritableMap nativeProps) {
mViewTag = viewTag;
mNativeProps = nativeProps;
}
}
private Queue<NativeUpdateOperation> mOperationsInBatch = new LinkedList<>();
public NodesManager(ReactContext context) {
mContext = context;
mUIManager = context.getNativeModule(UIManagerModule.class);
updateContext = new UpdateContext();
mUIImplementation = mUIManager.getUIImplementation();
mCustomEventNamesResolver = mUIManager.getDirectEventNamesResolver();
mEventEmitter = context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
mReactChoreographer = ReactChoreographer.getInstance();
mChoreographerCallback = new GuardedFrameCallback(context) {
@Override
protected void doFrameGuarded(long frameTimeNanos) {
onAnimationFrame(frameTimeNanos);
}
};
mNoopNode = new NoopNode(this);
// We register as event listener at the end, because we pass `this` and we haven't finished contructing an object yet.
// This lead to a crash described in https://github.com/software-mansion/react-native-reanimated/issues/604 which was caused by Nodes Manager being constructed on UI thread and registering for events.
// Events are handled in the native modules thread in the `onEventDispatch()` method.
// This method indirectly uses `mChoreographerCallback` which was created after event registration, creating race condition
mUIManager.getEventDispatcher().addListener(this);
}
public void onHostPause() {
if (mCallbackPosted.get()) {
stopUpdatingOnAnimationFrame();
mCallbackPosted.set(true);
}
}
public void onHostResume() {
if (mCallbackPosted.getAndSet(false)) {
startUpdatingOnAnimationFrame();
}
}
private void startUpdatingOnAnimationFrame() {
if (!mCallbackPosted.getAndSet(true)) {
mReactChoreographer.postFrameCallback(
ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE,
mChoreographerCallback);
}
}
private void stopUpdatingOnAnimationFrame() {
if (mCallbackPosted.getAndSet(false)) {
mReactChoreographer.removeFrameCallback(
ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE,
mChoreographerCallback);
}
}
private void onAnimationFrame(long frameTimeNanos) {
currentFrameTimeMs = frameTimeNanos / 1000000.;
while (!mEventQueue.isEmpty()) {
handleEvent(mEventQueue.poll());
}
if (!mFrameCallbacks.isEmpty()) {
List<OnAnimationFrame> frameCallbacks = mFrameCallbacks;
mFrameCallbacks = new ArrayList<>(frameCallbacks.size());
for (int i = 0, size = frameCallbacks.size(); i < size; i++) {
frameCallbacks.get(i).onAnimationFrame();
}
}
if (mWantRunUpdates) {
Node.runUpdates(updateContext);
}
if (!mOperationsInBatch.isEmpty()) {
final Queue<NativeUpdateOperation> copiedOperationsQueue = mOperationsInBatch;
mOperationsInBatch = new LinkedList<>();
mContext.runOnNativeModulesQueueThread(
// FIXME replace `mContext` with `mContext.getExceptionHandler()` after RN 0.59 support is dropped
new GuardedRunnable(mContext) {
@Override
public void runGuarded() {
boolean shouldDispatchUpdates = UIManagerReanimatedHelper.isOperationQueueEmpty(mUIImplementation);
while (!copiedOperationsQueue.isEmpty()) {
NativeUpdateOperation op = copiedOperationsQueue.remove();
ReactShadowNode shadowNode = mUIImplementation.resolveShadowNode(op.mViewTag);
if (shadowNode != null) {
mUIManager.updateView(op.mViewTag, shadowNode.getViewClass(), op.mNativeProps);
}
}
if (shouldDispatchUpdates) {
mUIImplementation.dispatchViewUpdates(-1); // no associated batchId
}
}
});
}
mCallbackPosted.set(false);
mWantRunUpdates = false;
if (!mFrameCallbacks.isEmpty() || !mEventQueue.isEmpty()) {
// enqueue next frame
startUpdatingOnAnimationFrame();
}
}
/**
* Null-safe way of getting node's value. If node is not present we return 0. This also matches
* iOS behavior when the app won't just crash.
*/
public Object getNodeValue(int nodeID) {
Node node = mAnimatedNodes.get(nodeID);
if (node != null) {
return node.value();
}
return ZERO;
}
/**
* Null-safe way of getting node reference. This method always returns non-null instance. If the
* node is not present we try to return a "no-op" node that allows for "set" calls and always
* returns 0 as a value.
*/
public <T extends Node> T findNodeById(int id, Class<T> type) {
Node node = mAnimatedNodes.get(id);
if (node == null) {
if (type == Node.class || type == ValueNode.class) {
return (T) mNoopNode;
}
throw new IllegalArgumentException("Requested node with id " + id + " of type " + type +
" cannot be found");
}
if (type.isInstance(node)) {
return (T) node;
}
throw new IllegalArgumentException("Node with id " + id + " is of incompatible type " +
node.getClass() + ", requested type was " + type);
}
public void createNode(int nodeID, ReadableMap config) {
if (mAnimatedNodes.get(nodeID) != null) {
throw new JSApplicationIllegalArgumentException("Animated node with ID " + nodeID +
" already exists");
}
String type = config.getString("type");
final Node node;
if ("props".equals(type)) {
node = new PropsNode(nodeID, config, this, mUIImplementation);
} else if ("style".equals(type)) {
node = new StyleNode(nodeID, config, this);
} else if ("transform".equals(type)) {
node = new TransformNode(nodeID, config, this);
} else if ("value".equals(type)) {
node = new ValueNode(nodeID, config, this);
} else if ("block".equals(type)) {
node = new BlockNode(nodeID, config, this);
} else if ("cond".equals(type)) {
node = new CondNode(nodeID, config, this);
} else if ("op".equals(type)) {
node = new OperatorNode(nodeID, config, this);
} else if ("set".equals(type)) {
node = new SetNode(nodeID, config, this);
} else if ("debug".equals(type)) {
node = new DebugNode(nodeID, config, this);
} else if ("clock".equals(type)) {
node = new ClockNode(nodeID, config, this);
} else if ("clockStart".equals(type)) {
node = new ClockOpNode.ClockStartNode(nodeID, config, this);
} else if ("clockStop".equals(type)) {
node = new ClockOpNode.ClockStopNode(nodeID, config, this);
} else if ("clockTest".equals(type)) {
node = new ClockOpNode.ClockTestNode(nodeID, config, this);
} else if ("call".equals(type)) {
node = new JSCallNode(nodeID, config, this);
} else if ("bezier".equals(type)) {
node = new BezierNode(nodeID, config, this);
} else if ("event".equals(type)) {
node = new EventNode(nodeID, config, this);
} else if ("always".equals(type)) {
node = new AlwaysNode(nodeID, config, this);
} else if ("concat".equals(type)) {
node = new ConcatNode(nodeID, config, this);
} else if ("param".equals(type)) {
node = new ParamNode(nodeID, config, this);
} else if ("func".equals(type)) {
node = new FunctionNode(nodeID, config, this);
} else if ("callfunc".equals(type)) {
node = new CallFuncNode(nodeID, config, this);
} else {
throw new JSApplicationIllegalArgumentException("Unsupported node type: " + type);
}
mAnimatedNodes.put(nodeID, node);
}
public void dropNode(int tag) {
mAnimatedNodes.remove(tag);
}
public void connectNodes(int parentID, int childID) {
Node parentNode = mAnimatedNodes.get(parentID);
Node childNode = mAnimatedNodes.get(childID);
if (childNode == null) {
throw new JSApplicationIllegalArgumentException("Animated node with ID " + childID +
" does not exists");
}
parentNode.addChild(childNode);
}
public void disconnectNodes(int parentID, int childID) {
Node parentNode = mAnimatedNodes.get(parentID);
Node childNode = mAnimatedNodes.get(childID);
if (childNode == null) {
throw new JSApplicationIllegalArgumentException("Animated node with ID " + childID +
" does not exists");
}
parentNode.removeChild(childNode);
}
public void connectNodeToView(int nodeID, int viewTag) {
Node node = mAnimatedNodes.get(nodeID);
if (node == null) {
throw new JSApplicationIllegalArgumentException("Animated node with ID " + nodeID +
" does not exists");
}
if (!(node instanceof PropsNode)) {
throw new JSApplicationIllegalArgumentException("Animated node connected to view should be" +
"of type " + PropsNode.class.getName());
}
((PropsNode) node).connectToView(viewTag);
}
public void disconnectNodeFromView(int nodeID, int viewTag) {
Node node = mAnimatedNodes.get(nodeID);
if (node == null) {
throw new JSApplicationIllegalArgumentException("Animated node with ID " + nodeID +
" does not exists");
}
if (!(node instanceof PropsNode)) {
throw new JSApplicationIllegalArgumentException("Animated node connected to view should be" +
"of type " + PropsNode.class.getName());
}
((PropsNode) node).disconnectFromView(viewTag);
}
public void enqueueUpdateViewOnNativeThread(int viewTag, WritableMap nativeProps) {
mOperationsInBatch.add(new NativeUpdateOperation(viewTag, nativeProps));
}
public void attachEvent(int viewTag, String eventName, int eventNodeID) {
String key = viewTag + eventName;
EventNode node = (EventNode) mAnimatedNodes.get(eventNodeID);
if (node == null) {
throw new JSApplicationIllegalArgumentException("Event node " + eventNodeID + " does not exists");
}
if (mEventMapping.containsKey(key)) {
throw new JSApplicationIllegalArgumentException("Event handler already set for the given view and event type");
}
mEventMapping.put(key, node);
}
public void detachEvent(int viewTag, String eventName, int eventNodeID) {
String key = viewTag + eventName;
mEventMapping.remove(key);
}
public void configureProps(Set<String> nativePropsSet, Set<String> uiPropsSet) {
nativeProps = nativePropsSet;
uiProps = uiPropsSet;
}
public void getValue(int nodeID, Callback callback) {
callback.invoke(mAnimatedNodes.get(nodeID).value());
}
public void postRunUpdatesAfterAnimation() {
mWantRunUpdates = true;
startUpdatingOnAnimationFrame();
}
public void postOnAnimation(OnAnimationFrame onAnimationFrame) {
mFrameCallbacks.add(onAnimationFrame);
startUpdatingOnAnimationFrame();
}
@Override
public void onEventDispatch(Event event) {
// Events can be dispatched from any thread so we have to make sure handleEvent is run from the
// UI thread.
if (UiThreadUtil.isOnUiThread()) {
handleEvent(event);
} else {
mEventQueue.offer(event);
startUpdatingOnAnimationFrame();
}
}
private void handleEvent(Event event) {
if (!mEventMapping.isEmpty()) {
// If the event has a different name in native convert it to it's JS name.
String eventName = mCustomEventNamesResolver.resolveCustomEventName(event.getEventName());
int viewTag = event.getViewTag();
String key = viewTag + eventName;
EventNode node = mEventMapping.get(key);
if (node != null) {
event.dispatch(node);
}
}
}
public void sendEvent(String name, WritableMap body) {
mEventEmitter.emit(name, body);
}
public void setValue(int nodeID, Double newValue) {
Node node = mAnimatedNodes.get(nodeID);
if (node != null) {
((ValueNode) node).setValue(newValue);
}
}
}

View File

@ -0,0 +1,226 @@
package com.swmansion.reanimated;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.NativeViewHierarchyManager;
import com.facebook.react.uimanager.UIBlock;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.UIManagerModuleListener;
import com.swmansion.reanimated.transitions.TransitionModule;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import javax.annotation.Nullable;
@ReactModule(name = ReanimatedModule.NAME)
public class ReanimatedModule extends ReactContextBaseJavaModule implements
LifecycleEventListener, UIManagerModuleListener {
public static final String NAME = "ReanimatedModule";
private interface UIThreadOperation {
void execute(NodesManager nodesManager);
}
private ArrayList<UIThreadOperation> mOperations = new ArrayList<>();
private @Nullable NodesManager mNodesManager;
private @Nullable TransitionModule mTransitionManager;
public ReanimatedModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public void initialize() {
ReactApplicationContext reactCtx = getReactApplicationContext();
UIManagerModule uiManager = reactCtx.getNativeModule(UIManagerModule.class);
reactCtx.addLifecycleEventListener(this);
uiManager.addUIManagerListener(this);
mTransitionManager = new TransitionModule(uiManager);
}
@Override
public void onHostPause() {
if (mNodesManager != null) {
mNodesManager.onHostPause();
}
}
@Override
public void onHostResume() {
if (mNodesManager != null) {
mNodesManager.onHostResume();
}
}
@Override
public void onHostDestroy() {
// do nothing
}
@Override
public void willDispatchViewUpdates(final UIManagerModule uiManager) {
if (mOperations.isEmpty()) {
return;
}
final ArrayList<UIThreadOperation> operations = mOperations;
mOperations = new ArrayList<>();
uiManager.addUIBlock(new UIBlock() {
@Override
public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
NodesManager nodesManager = getNodesManager();
for (UIThreadOperation operation : operations) {
operation.execute(nodesManager);
}
}
});
}
@Override
public String getName() {
return NAME;
}
private NodesManager getNodesManager() {
if (mNodesManager == null) {
mNodesManager = new NodesManager(getReactApplicationContext());
}
return mNodesManager;
}
@ReactMethod
public void animateNextTransition(int tag, ReadableMap config) {
mTransitionManager.animateNextTransition(tag, config);
}
@ReactMethod
public void createNode(final int tag, final ReadableMap config) {
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NodesManager nodesManager) {
nodesManager.createNode(tag, config);
}
});
}
@ReactMethod
public void dropNode(final int tag) {
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NodesManager nodesManager) {
nodesManager.dropNode(tag);
}
});
}
@ReactMethod
public void connectNodes(final int parentID, final int childID) {
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NodesManager nodesManager) {
nodesManager.connectNodes(parentID, childID);
}
});
}
@ReactMethod
public void disconnectNodes(final int parentID, final int childID) {
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NodesManager nodesManager) {
nodesManager.disconnectNodes(parentID, childID);
}
});
}
@ReactMethod
public void connectNodeToView(final int nodeID, final int viewTag) {
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NodesManager nodesManager) {
nodesManager.connectNodeToView(nodeID, viewTag);
}
});
}
@ReactMethod
public void disconnectNodeFromView(final int nodeID, final int viewTag) {
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NodesManager nodesManager) {
nodesManager.disconnectNodeFromView(nodeID, viewTag);
}
});
}
@ReactMethod
public void attachEvent(final int viewTag, final String eventName, final int eventNodeID) {
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NodesManager nodesManager) {
nodesManager.attachEvent(viewTag, eventName, eventNodeID);
}
});
}
@ReactMethod
public void detachEvent(final int viewTag, final String eventName, final int eventNodeID) {
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NodesManager nodesManager) {
nodesManager.detachEvent(viewTag, eventName, eventNodeID);
}
});
}
@ReactMethod
public void configureProps(ReadableArray nativePropsArray, ReadableArray uiPropsArray) {
int size = nativePropsArray.size();
final Set<String> nativeProps = new HashSet<>(size);
for (int i = 0; i < size; i++) {
nativeProps.add(nativePropsArray.getString(i));
}
size = uiPropsArray.size();
final Set<String> uiProps = new HashSet<>(size);
for (int i = 0; i < size; i++) {
uiProps.add(uiPropsArray.getString(i));
}
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NodesManager nodesManager) {
nodesManager.configureProps(nativeProps, uiProps);
}
});
}
@ReactMethod
public void getValue(final int nodeID, final Callback callback) {
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NodesManager nodesManager) {
nodesManager.getValue(nodeID, callback);
}
});
}
@ReactMethod
public void setValue(final int nodeID, final Double newValue) {
mOperations.add(new UIThreadOperation() {
@Override
public void execute(NodesManager nodesManager) {
nodesManager.setValue(nodeID, newValue);
}
});
}
}

View File

@ -0,0 +1,21 @@
package com.swmansion.reanimated;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.Arrays;
import java.util.List;
public class ReanimatedPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(new ReanimatedModule(reactContext));
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Arrays.asList();
}
}

View File

@ -0,0 +1,13 @@
package com.swmansion.reanimated;
import com.swmansion.reanimated.nodes.Node;
import java.util.ArrayList;
public class UpdateContext {
public long updateLoopID = 0;
public String callID = "";
public final ArrayList<Node> updatedNodes = new ArrayList<>();
}

View File

@ -0,0 +1,31 @@
package com.swmansion.reanimated;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import java.util.HashMap;
import java.util.Map;
public class Utils {
public static Map<String, Integer> processMapping(ReadableMap style) {
ReadableMapKeySetIterator iter = style.keySetIterator();
HashMap<String, Integer> mapping = new HashMap<>();
while (iter.hasNextKey()) {
String propKey = iter.nextKey();
int nodeIndex = style.getInt(propKey);
mapping.put(propKey, nodeIndex);
}
return mapping;
}
public static int[] processIntArray(ReadableArray ary) {
int size = ary.size();
int[] res = new int[size];
for (int i = 0; i < size; i++) {
res[i] = ary.getInt(i);
}
return res;
}
}

View File

@ -0,0 +1,25 @@
package com.swmansion.reanimated.nodes;
import com.facebook.react.bridge.ReadableMap;
import com.swmansion.reanimated.MapUtils;
import com.swmansion.reanimated.NodesManager;
public class AlwaysNode extends Node implements FinalNode {
public AlwaysNode(int nodeID, ReadableMap config, NodesManager nodesManager) {
super(nodeID, config, nodesManager);
mNodeToBeEvaluated = MapUtils.getInt(config, "what", "Reanimated: Argument passed to always node is either of wrong type or is missing.");
}
private int mNodeToBeEvaluated;
@Override
public void update() {
this.value();
}
@Override
protected Double evaluate() {
mNodesManager.findNodeById(mNodeToBeEvaluated, Node.class).value();
return ZERO;
}
}

View File

@ -0,0 +1,84 @@
package com.swmansion.reanimated.nodes;
import android.graphics.PointF;
import com.facebook.react.bridge.ReadableMap;
import com.swmansion.reanimated.MapUtils;
import com.swmansion.reanimated.NodesManager;
public class BezierNode extends Node {
private static class CubicBezierInterpolator {
protected PointF start;
protected PointF end;
protected PointF a = new PointF();
protected PointF b = new PointF();
protected PointF c = new PointF();
public CubicBezierInterpolator(PointF start, PointF end) {
this.start = start;
this.end = end;
}
public CubicBezierInterpolator(float startX, float startY, float endX, float endY) {
this(new PointF(startX, startY), new PointF(endX, endY));
}
public float getInterpolation(float time) {
return getBezierCoordinateY(getXForTime(time));
}
protected float getBezierCoordinateY(float time) {
c.y = 3 * start.y;
b.y = 3 * (end.y - start.y) - c.y;
a.y = 1 - c.y - b.y;
return time * (c.y + time * (b.y + time * a.y));
}
protected float getXForTime(float time) {
float x = time;
float z;
for (int i = 1; i < 14; i++) {
z = getBezierCoordinateX(x) - time;
if (Math.abs(z) < 1e-3) {
break;
}
x -= z / getXDerivate(x);
}
return x;
}
private float getXDerivate(float t) {
return c.x + t * (2 * b.x + 3 * a.x * t);
}
private float getBezierCoordinateX(float time) {
c.x = 3 * start.x;
b.x = 3 * (end.x - start.x) - c.x;
a.x = 1 - c.x - b.x;
return time * (c.x + time * (b.x + time * a.x));
}
}
private final int mInputID;
private final CubicBezierInterpolator mInterpolator;
public BezierNode(int nodeID, ReadableMap config, NodesManager nodesManager) {
super(nodeID, config, nodesManager);
mInputID = MapUtils.getInt(config, "input", "Reanimated: Argument passed to bezier node is either of wrong type or is missing.");
float startX = (float) config.getDouble("mX1");
float startY = (float) config.getDouble("mY1");
float endX = (float) config.getDouble("mX2");
float endY = (float) config.getDouble("mY2");
mInterpolator = new CubicBezierInterpolator(startX, startY, endX, endY);
}
@Override
protected Double evaluate() {
Double in = (Double) mNodesManager.getNodeValue(mInputID);
return Double.valueOf(mInterpolator.getInterpolation(in.floatValue()));
}
}

View File

@ -0,0 +1,24 @@
package com.swmansion.reanimated.nodes;
import com.facebook.react.bridge.ReadableMap;
import com.swmansion.reanimated.NodesManager;
import com.swmansion.reanimated.Utils;
public class BlockNode extends Node {
private final int[] mBlock;
public BlockNode(int nodeID, ReadableMap config, NodesManager nodesManager) {
super(nodeID, config, nodesManager);
mBlock = Utils.processIntArray(config.getArray("block"));
}
@Override
protected Object evaluate() {
Object res = null;
for (int i = 0; i < mBlock.length; i++) {
res = mNodesManager.findNodeById(mBlock[i], Node.class).value();
}
return res;
}
}

View File

@ -0,0 +1,48 @@
package com.swmansion.reanimated.nodes;
import com.facebook.react.bridge.ReadableMap;
import com.swmansion.reanimated.NodesManager;
import com.swmansion.reanimated.Utils;
public class CallFuncNode extends Node {
private String mPreviousCallID;
private final int mWhatNodeID;
private final int[] mArgs;
private final int[] mParams;
public CallFuncNode(int nodeID, ReadableMap config, NodesManager nodesManager) {
super(nodeID, config, nodesManager);
mWhatNodeID = config.getInt("what");
mParams = Utils.processIntArray(config.getArray("params"));
mArgs = Utils.processIntArray(config.getArray("args"));
}
private void beginContext() {
mPreviousCallID = mNodesManager.updateContext.callID;
mNodesManager.updateContext.callID = mNodesManager.updateContext.callID + '/' + String.valueOf(mNodeID);
for (int i = 0; i < mParams.length; i++) {
int paramId = mParams[i];
ParamNode paramNode = mNodesManager.findNodeById(paramId, ParamNode.class);
paramNode.beginContext(mArgs[i], mPreviousCallID);
}
}
private void endContext() {
for (int i = 0; i < mParams.length; i++) {
int paramId = mParams[i];
ParamNode paramNode = mNodesManager.findNodeById(paramId, ParamNode.class);
paramNode.endContext();
}
mNodesManager.updateContext.callID = mPreviousCallID;
}
@Override
protected Object evaluate() {
beginContext();
Node whatNode = mNodesManager.findNodeById(mWhatNodeID, Node.class);
Object retVal = whatNode.value();
endContext();
return retVal;
}
}

View File

@ -0,0 +1,38 @@
package com.swmansion.reanimated.nodes;
import com.facebook.react.bridge.ReadableMap;
import com.swmansion.reanimated.NodesManager;
public class ClockNode extends Node implements NodesManager.OnAnimationFrame {
public boolean isRunning;
public ClockNode(int nodeID, ReadableMap config, NodesManager nodesManager) {
super(nodeID, config, nodesManager);
}
public void start() {
if (isRunning) {
return;
}
isRunning = true;
mNodesManager.postOnAnimation(this);
}
public void stop() {
isRunning = false;
}
@Override
protected Double evaluate() {
return mNodesManager.currentFrameTimeMs;
}
@Override
public void onAnimationFrame() {
if (isRunning) {
markUpdated();
mNodesManager.postOnAnimation(this);
}
}
}

View File

@ -0,0 +1,69 @@
package com.swmansion.reanimated.nodes;
import com.facebook.react.bridge.ReadableMap;
import com.swmansion.reanimated.MapUtils;
import com.swmansion.reanimated.NodesManager;
public abstract class ClockOpNode extends Node {
public static class ClockStartNode extends ClockOpNode {
public ClockStartNode(int nodeID, ReadableMap config, NodesManager nodesManager) {
super(nodeID, config, nodesManager);
}
@Override
protected Double eval(Node clock) {
if (clock instanceof ParamNode) {
((ParamNode) clock).start();
} else {
((ClockNode) clock).start();
}
return ZERO;
}
}
public static class ClockStopNode extends ClockOpNode {
public ClockStopNode(int nodeID, ReadableMap config, NodesManager nodesManager) {
super(nodeID, config, nodesManager);
}
@Override
protected Double eval(Node clock) {
if (clock instanceof ParamNode) {
((ParamNode) clock).stop();
} else {
((ClockNode) clock).stop();
}
return ZERO;
}
}
public static class ClockTestNode extends ClockOpNode {
public ClockTestNode(int nodeID, ReadableMap config, NodesManager nodesManager) {
super(nodeID, config, nodesManager);
}
@Override
protected Double eval(Node clock) {
if (clock instanceof ParamNode) {
return ((ParamNode) clock).isRunning() ? 1. : 0.;
}
return ((ClockNode) clock).isRunning ? 1. : 0.;
}
}
private int clockID;
public ClockOpNode(int nodeID, ReadableMap config, NodesManager nodesManager) {
super(nodeID, config, nodesManager);
clockID = MapUtils.getInt(config, "clock", "Reanimated: Argument passed to clock node is either of wrong type or is missing.");
}
@Override
protected Double evaluate() {
Node clock = mNodesManager.findNodeById(clockID, Node.class);
return eval(clock);
}
protected abstract Double eval(Node clock);
}

View File

@ -0,0 +1,36 @@
package com.swmansion.reanimated.nodes;
import java.text.NumberFormat;
import java.util.Locale;
import com.facebook.react.bridge.ReadableMap;
import com.swmansion.reanimated.NodesManager;
import com.swmansion.reanimated.Utils;
public class ConcatNode extends Node {
private final int[] mInputIDs;
private final static NumberFormat sFormatter = NumberFormat.getInstance(Locale.ENGLISH);
static {
sFormatter.setMinimumFractionDigits(0);
sFormatter.setGroupingUsed(false);
}
public ConcatNode(int nodeID, ReadableMap config, NodesManager nodesManager) {
super(nodeID, config, nodesManager);
mInputIDs = Utils.processIntArray(config.getArray("input"));
}
@Override
protected String evaluate() {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < mInputIDs.length; i++) {
Node inputNodes = mNodesManager.findNodeById(mInputIDs[i], Node.class);
Object value = inputNodes.value();
if (value instanceof Double) {
value = sFormatter.format((Double) value);
}
builder.append(value);
}
return builder.toString();
}
}

View File

@ -0,0 +1,29 @@
package com.swmansion.reanimated.nodes;
import com.facebook.react.bridge.ReadableMap;
import com.swmansion.reanimated.MapUtils;
import com.swmansion.reanimated.NodesManager;
public class CondNode extends Node {
private final int mCondID, mIfBlockID, mElseBlockID;
public CondNode(int nodeID, ReadableMap config, NodesManager nodesManager) {
super(nodeID, config, nodesManager);
mCondID = MapUtils.getInt(config, "cond", "Reanimated: First argument passed to cond node is either of wrong type or is missing.");
mIfBlockID = MapUtils.getInt(config, "ifBlock", "Reanimated: Second argument passed to cond node is either of wrong type or is missing.");
mElseBlockID = config.hasKey("elseBlock")
? MapUtils.getInt(config, "elseBlock", "Reanimated: Second argument passed to cond node is either of wrong type or is missing.")
: -1;
}
@Override
protected Object evaluate() {
Object cond = mNodesManager.getNodeValue(mCondID);
if (cond instanceof Number && ((Number) cond).doubleValue() != 0.0) {
// This is not a good way to compare doubles but in this case it is what we want
return mIfBlockID != -1 ? mNodesManager.getNodeValue(mIfBlockID) : ZERO;
}
return mElseBlockID != -1 ? mNodesManager.getNodeValue(mElseBlockID) : ZERO;
}
}

View File

@ -0,0 +1,26 @@
package com.swmansion.reanimated.nodes;
import android.util.Log;
import com.facebook.react.bridge.ReadableMap;
import com.swmansion.reanimated.MapUtils;
import com.swmansion.reanimated.NodesManager;
public class DebugNode extends Node {
private final String mMessage;
private final int mValueID;
public DebugNode(int nodeID, ReadableMap config, NodesManager nodesManager) {
super(nodeID, config, nodesManager);
mMessage = MapUtils.getString(config, "message", "Reanimated: First argument passed to debug node is either of wrong type or is missing.");
mValueID = MapUtils.getInt(config, "value", "Reanimated: Second argument passed to debug node is either of wrong type or is missing.");
}
@Override
protected Object evaluate() {
Object value = mNodesManager.findNodeById(mValueID, Node.class).value();
Log.d("REANIMATED", String.format("%s %s", mMessage, value));
return value;
}
}

View File

@ -0,0 +1,84 @@
package com.swmansion.reanimated.nodes;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.swmansion.reanimated.NodesManager;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
public class EventNode extends Node implements RCTEventEmitter {
private static class EventMap {
private final int nodeID;
private final String[] path;
public EventMap(ReadableArray eventPath) {
int size = eventPath.size();
path = new String[size - 1];
for (int i = 0; i < size - 1; i++) {
path[i] = eventPath.getString(i);
}
nodeID = eventPath.getInt(size - 1);
}
public Double lookupValue(ReadableMap event) {
ReadableMap map = event;
for (int i = 0; map != null && i < path.length - 1; i++) {
String key = path[i];
map = map.hasKey(key) ? map.getMap(key) : null;
}
if (map != null) {
String key = path[path.length - 1];
return map.hasKey(key) ? map.getDouble(key) : null;
}
return null;
}
}
private static List<EventMap> processMapping(ReadableArray mapping) {
int size = mapping.size();
List<EventMap> res = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
res.add(new EventMap(mapping.getArray(i)));
}
return res;
}
private final List<EventMap> mMapping;
public EventNode(int nodeID, ReadableMap config, NodesManager nodesManager) {
super(nodeID, config, nodesManager);
mMapping = processMapping(config.getArray("argMapping"));
}
@Override
public void receiveEvent(int targetTag, String eventName, @Nullable WritableMap event) {
if (event == null) {
throw new IllegalArgumentException("Animated events must have event data.");
}
for (int i = 0; i < mMapping.size(); i++) {
EventMap eventMap = mMapping.get(i);
Double value = eventMap.lookupValue(event);
if (value != null) {
mNodesManager.findNodeById(eventMap.nodeID, ValueNode.class).setValue(value);
}
}
}
@Override
public void receiveTouches(String eventName, WritableArray touches, WritableArray changedIndices) {
throw new RuntimeException("receiveTouches is not support by animated events");
}
@Override
protected Double evaluate() {
return ZERO;
}
}

View File

@ -0,0 +1,5 @@
package com.swmansion.reanimated.nodes;
public interface FinalNode {
void update();
}

View File

@ -0,0 +1,20 @@
package com.swmansion.reanimated.nodes;
import com.facebook.react.bridge.ReadableMap;
import com.swmansion.reanimated.NodesManager;
public class FunctionNode extends Node {
private final int mWhatNodeID;
public FunctionNode(int nodeID, ReadableMap config, NodesManager nodesManager) {
super(nodeID, config, nodesManager);
mWhatNodeID = config.getInt("what");
}
@Override
protected Object evaluate() {
Node what = mNodesManager.findNodeById(mWhatNodeID, Node.class);
return what.value();
}
}

View File

@ -0,0 +1,41 @@
package com.swmansion.reanimated.nodes;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.swmansion.reanimated.NodesManager;
import com.swmansion.reanimated.Utils;
public class JSCallNode extends Node {
private final int[] mInputIDs;
public JSCallNode(int nodeID, ReadableMap config, NodesManager nodesManager) {
super(nodeID, config, nodesManager);
mInputIDs = Utils.processIntArray(config.getArray("input"));
}
@Override
protected Double evaluate() {
WritableArray args = Arguments.createArray();
for (int i = 0; i < mInputIDs.length; i++) {
Node node = mNodesManager.findNodeById(mInputIDs[i], Node.class);
if (node.value() == null) {
args.pushNull();
} else {
Object value = node.value();
if (value instanceof String) {
args.pushString((String) value);
} else {
args.pushDouble(node.doubleValue());
}
}
}
WritableMap eventData = Arguments.createMap();
eventData.putInt("id", mNodeID);
eventData.putArray("args", args);
mNodesManager.sendEvent("onReanimatedCall", eventData);
return ZERO;
}
}

View File

@ -0,0 +1,137 @@
package com.swmansion.reanimated.nodes;
import android.util.SparseArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.UiThreadUtil;
import com.swmansion.reanimated.NodesManager;
import com.swmansion.reanimated.UpdateContext;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import javax.annotation.Nullable;
public abstract class Node {
public static final Double ZERO = Double.valueOf(0);
public static final Double ONE = Double.valueOf(1);
protected final int mNodeID;
protected final NodesManager mNodesManager;
protected final UpdateContext mUpdateContext;
private final Map<String, Long> mLastLoopID = new HashMap<>();
private final Map<String, Object> mMemoizedValue = new HashMap<>();
private @Nullable List<Node> mChildren; /* lazy-initialized when a child is added */
public Node(int nodeID, @Nullable ReadableMap config, NodesManager nodesManager) {
mLastLoopID.put("", -1L);
mNodeID = nodeID;
mNodesManager = nodesManager;
mUpdateContext = nodesManager.updateContext;
}
protected abstract @Nullable Object evaluate();
public final @Nullable Object value() {
if (!mLastLoopID.containsKey(mUpdateContext.callID) || mLastLoopID.get(mUpdateContext.callID) < mUpdateContext.updateLoopID) {
mLastLoopID.put(mUpdateContext.callID, mUpdateContext.updateLoopID);
Object result = evaluate();
mMemoizedValue.put(mUpdateContext.callID, result);
return result;
}
return mMemoizedValue.get(mUpdateContext.callID);
}
/**
* This method will never return null. If value is null or of a different type we try to cast and
* return 0 if we fail to properly cast the value. This is to match iOS behavior where the node
* would not throw even if the value was not set.
*/
public final Double doubleValue() {
Object value = value();
if (value == null) {
return ZERO;
} else if (value instanceof Double) {
return (Double) value;
} else if (value instanceof Number) {
return Double.valueOf(((Number) value).doubleValue());
} else if (value instanceof Boolean) {
return ((Boolean) value).booleanValue() ? ONE : ZERO;
}
throw new IllegalStateException("Value of node " + this + " cannot be cast to a number");
}
public void addChild(Node child) {
if (mChildren == null) {
mChildren = new ArrayList<>();
}
mChildren.add(child);
child.dangerouslyRescheduleEvaluate();
}
public void removeChild(Node child) {
if (mChildren != null) {
mChildren.remove(child);
}
}
protected void markUpdated() {
UiThreadUtil.assertOnUiThread();
mUpdateContext.updatedNodes.add(this);
mNodesManager.postRunUpdatesAfterAnimation();
}
protected final void dangerouslyRescheduleEvaluate() {
mLastLoopID.put(mUpdateContext.callID, -1L);
markUpdated();
}
protected final void forceUpdateMemoizedValue(Object value) {
mMemoizedValue.put(mUpdateContext.callID, value);
markUpdated();
}
private static void findAndUpdateNodes(Node node, Set<Node> visitedNodes, Stack<FinalNode> finalNodes) {
if (visitedNodes.contains(node)) {
return;
} else {
visitedNodes.add(node);
}
List<Node> children = node.mChildren;
if (children != null) {
for (Node child : children) {
findAndUpdateNodes(child, visitedNodes, finalNodes);
}
}
if (node instanceof FinalNode) {
finalNodes.push((FinalNode) node);
}
}
public static void runUpdates(UpdateContext updateContext) {
UiThreadUtil.assertOnUiThread();
ArrayList<Node> updatedNodes = updateContext.updatedNodes;
Set<Node> visitedNodes = new HashSet<>();
Stack<FinalNode> finalNodes = new Stack<>();
for (int i = 0; i < updatedNodes.size(); i++) {
findAndUpdateNodes(updatedNodes.get(i), visitedNodes, finalNodes);
if (i == updatedNodes.size() - 1) {
while (!finalNodes.isEmpty()) {
finalNodes.pop().update();
}
}
}
updatedNodes.clear();
updateContext.updateLoopID++;
}
}

View File

@ -0,0 +1,37 @@
package com.swmansion.reanimated.nodes;
import com.swmansion.reanimated.NodesManager;
/**
* This node is used by {@link NodesManager} to return in place of a missing node that might have
* been requested. This way we avoid a top of null-checks and we make nodes manager compatible with
* the iOS code which does not crash for missing nodes. In most of the cases it is desirable to
* handle missing nodes gracefully as it usually means the node hasn't been hooked under animated
* view prop but is referenced in the graph (e.g. to support some edge case).
*/
public class NoopNode extends ValueNode {
public NoopNode(NodesManager nodesManager) {
super(-2, null, nodesManager);
}
@Override
public void setValue(Object value) {
// no-op
}
@Override
public void addChild(Node child) {
// no-op
}
@Override
public void removeChild(Node child) {
// no-op
}
@Override
protected void markUpdated() {
// no-op
}
}

View File

@ -0,0 +1,346 @@
package com.swmansion.reanimated.nodes;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReadableMap;
import com.swmansion.reanimated.NodesManager;
import com.swmansion.reanimated.Utils;
public class OperatorNode extends Node {
private static boolean truthy(Object value) {
return value != null && !value.equals(0.);
}
private interface Operator {
double evaluate(Node[] input);
}
private static abstract class ReduceOperator implements Operator {
@Override
public double evaluate(Node[] input) {
double acc = input[0].doubleValue();
for (int i = 1; i < input.length; i++) {
acc = reduce(acc, input[i].doubleValue());
}
return acc;
}
public abstract double reduce(Double x, Double y);
}
private static abstract class SingleOperator implements Operator {
@Override
public double evaluate(Node[] input) {
return eval((Double) input[0].value());
}
public abstract double eval(Double x);
}
private static abstract class CompOperator implements Operator {
@Override
public double evaluate(Node[] input) {
return eval((Double) input[0].value(), (Double) input[1].value()) ? 1. : 0.;
}
public abstract boolean eval(Double x, Double y);
}
// arithmetic
private static final Operator ADD = new ReduceOperator() {
@Override
public double reduce(Double x, Double y) {
return x + y;
}
};
private static final Operator SUB = new ReduceOperator() {
@Override
public double reduce(Double x, Double y) {
return x - y;
}
};
private static final Operator MULTIPLY= new ReduceOperator() {
@Override
public double reduce(Double x, Double y) {
return x * y;
}
};
private static final Operator DIVIDE = new ReduceOperator() {
@Override
public double reduce(Double x, Double y) {
return x / y;
}
};
private static final Operator POW = new ReduceOperator() {
@Override
public double reduce(Double x, Double y) {
return Math.pow(x, y);
}
};
private static final Operator MODULO = new ReduceOperator() {
@Override
public double reduce(Double x, Double y) {
return ((x % y) + y) % y;
}
};
private static final Operator SQRT = new SingleOperator() {
@Override
public double eval(Double x) {
return Math.sqrt(x);
}
};
private static final Operator LOG = new SingleOperator() {
@Override
public double eval(Double x) {
return Math.log(x);
}
};
private static final Operator SIN = new SingleOperator() {
@Override
public double eval(Double x) {
return Math.sin(x);
}
};
private static final Operator COS = new SingleOperator() {
@Override
public double eval(Double x) {
return Math.cos(x);
}
};
private static final Operator TAN = new SingleOperator() {
@Override
public double eval(Double x) {
return Math.tan(x);
}
};
private static final Operator ACOS = new SingleOperator() {
@Override
public double eval(Double x) {
return Math.acos(x);
}
};
private static final Operator ASIN = new SingleOperator() {
@Override
public double eval(Double x) {
return Math.asin(x);
}
};
private static final Operator ATAN = new SingleOperator() {
@Override
public double eval(Double x) {
return Math.atan(x);
}
};
private static final Operator EXP = new SingleOperator() {
@Override
public double eval(Double x) {
return Math.exp(x);
}
};
private static final Operator ROUND = new SingleOperator() {
@Override
public double eval(Double x) {
return Math.round(x);
}
};
private static final Operator ABS = new SingleOperator() {
@Override
public double eval(Double x) {
return Math.abs(x);
}
};
private static final Operator FLOOR = new SingleOperator() {
@Override
public double eval(Double x) {
return Math.floor(x);
}
};
private static final Operator CEIL = new SingleOperator() {
@Override
public double eval(Double x) {
return Math.ceil(x);
}
};
private static final Operator MIN = new ReduceOperator() {
@Override
public double reduce(Double x, Double y) {
return Math.min(x, y);
}
};
private static final Operator MAX = new ReduceOperator() {
@Override
public double reduce(Double x, Double y) {
return Math.max(x, y);
}
};
// logical
private static final Operator AND = new Operator() {
@Override
public double evaluate(Node[] input) {
boolean res = truthy(input[0].value());
for (int i = 1; i < input.length && res; i++) {
res = res && truthy(input[i].value());
}
return res ? 1. : 0.;
}
};
private static final Operator OR = new Operator() {
@Override
public double evaluate(Node[] input) {
boolean res = truthy(input[0].value());
for (int i = 1; i < input.length && !res; i++) {
res = res || truthy(input[i].value());
}
return res ? 1. : 0.;
}
};
private static final Operator NOT = new Operator() {
@Override
public double evaluate(Node[] input) {
return truthy(input[0].value()) ? 0. : 1.;
}
};
private static final Operator DEFINED = new Operator() {
@Override
public double evaluate(Node[] input) {
Object res = input[0].value();
return (res != null && !(res instanceof Double && ((Double) res).isNaN())) ? 1. : 0.;
}
};
// comparison
private static final Operator LESS_THAN = new CompOperator() {
@Override
public boolean eval(Double x, Double y) {
if (x == null || y == null) {
return false;
}
return x < y;
}
};
private static final Operator EQ = new CompOperator() {
@Override
public boolean eval(Double x, Double y) {
if (x == null || y == null) {
return x == y;
}
return x.doubleValue() == y.doubleValue();
}
};
private static final Operator GREATER_THAN = new CompOperator() {
@Override
public boolean eval(Double x, Double y) {
if (x == null || y == null) {
return false;
}
return x > y;
}
};
private static final Operator LESS_OR_EQ = new CompOperator() {
@Override
public boolean eval(Double x, Double y) {
return x <= y;
}
};
private static final Operator GREATER_OR_EQ = new CompOperator() {
@Override
public boolean eval(Double x, Double y) {
return x >= y;
}
};
private static final Operator NEQ = new CompOperator() {
@Override
public boolean eval(Double x, Double y) {
if (x == null || y == null) {
return x == y;
}
return x.doubleValue() != y.doubleValue();
}
};
private final int[] mInputIDs;
private final Node[] mInputNodes;
private final Operator mOperator;
public OperatorNode(int nodeID, ReadableMap config, NodesManager nodesManager) {
super(nodeID, config, nodesManager);
mInputIDs = Utils.processIntArray(config.getArray("input"));
mInputNodes = new Node[mInputIDs.length];
String op = config.getString("op");
if ("add".equals(op)) {
mOperator = ADD;
} else if ("sub".equals(op)) {
mOperator = SUB;
} else if ("multiply".equals(op)) {
mOperator = MULTIPLY;
} else if ("divide".equals(op)) {
mOperator = DIVIDE;
} else if ("pow".equals(op)) {
mOperator = POW;
} else if ("modulo".equals(op)) {
mOperator = MODULO;
} else if ("sqrt".equals(op)) {
mOperator = SQRT;
} else if ("log".equals(op)) {
mOperator = LOG;
} else if ("sin".equals(op)) {
mOperator = SIN;
} else if ("cos".equals(op)) {
mOperator = COS;
} else if ("tan".equals(op)) {
mOperator = TAN;
} else if ("acos".equals(op)) {
mOperator = ACOS;
} else if ("asin".equals(op)) {
mOperator = ASIN;
} else if ("atan".equals(op)) {
mOperator = ATAN;
} else if ("exp".equals(op)) {
mOperator = EXP;
} else if ("round".equals(op)) {
mOperator = ROUND;
} else if ("and".equals(op)) {
mOperator = AND;
} else if ("or".equals(op)) {
mOperator = OR;
} else if ("not".equals(op)) {
mOperator = NOT;
} else if ("defined".equals(op)) {
mOperator = DEFINED;
} else if ("lessThan".equals(op)) {
mOperator = LESS_THAN;
} else if ("eq".equals(op)) {
mOperator = EQ;
} else if ("greaterThan".equals(op)) {
mOperator = GREATER_THAN;
} else if ("lessOrEq".equals(op)) {
mOperator = LESS_OR_EQ;
} else if ("greaterOrEq".equals(op)) {
mOperator = GREATER_OR_EQ;
} else if ("neq".equals(op)) {
mOperator = NEQ;
} else if ("abs".equals(op)) {
mOperator = ABS;
}else if ("floor".equals(op)) {
mOperator = FLOOR;
}else if ("ceil".equals(op)) {
mOperator = CEIL;
}else if ("max".equals(op)) {
mOperator = MAX;
}else if ("min".equals(op)) {
mOperator = MIN;
} else {
throw new JSApplicationIllegalArgumentException("Unrecognized operator " + op);
}
}
@Override
protected Object evaluate() {
for (int i = 0; i < mInputIDs.length; i++) {
mInputNodes[i] = mNodesManager.findNodeById(mInputIDs[i], Node.class);
}
return mOperator.evaluate(mInputNodes);
}
}

View File

@ -0,0 +1,76 @@
package com.swmansion.reanimated.nodes;
import com.facebook.react.bridge.ReadableMap;
import com.swmansion.reanimated.NodesManager;
import java.util.Stack;
public class ParamNode extends ValueNode {
private final Stack<Integer> mArgsStack;
private String mPrevCallID;
public ParamNode(int nodeID, ReadableMap config, NodesManager nodesManager) {
super(nodeID, config, nodesManager);
mArgsStack = new Stack<>();
}
@Override
public void setValue(Object value) {
Node node = mNodesManager.findNodeById(mArgsStack.peek(), Node.class);
String callID = mUpdateContext.callID;
mUpdateContext.callID = mPrevCallID;
((ValueNode) node).setValue(value);
mUpdateContext.callID = callID;
forceUpdateMemoizedValue(value);
}
public void beginContext(Integer ref, String prevCallID) {
mPrevCallID = prevCallID;
mArgsStack.push(ref);
}
public void endContext() {
mArgsStack.pop();
}
@Override
protected Object evaluate() {
String callID = mUpdateContext.callID;
mUpdateContext.callID = mPrevCallID;
Node node = mNodesManager.findNodeById(mArgsStack.peek(), Node.class);
Object val = node.value();
mUpdateContext.callID = callID;
return val;
}
public void start() {
Node node = mNodesManager.findNodeById(mArgsStack.peek(), Node.class);
if (node instanceof ParamNode) {
((ParamNode) node).start();
} else {
((ClockNode) node).start();
}
}
public void stop() {
Node node = mNodesManager.findNodeById(mArgsStack.peek(), Node.class);
if (node instanceof ParamNode) {
((ParamNode) node).stop();
} else {
((ClockNode) node).stop();
}
}
public boolean isRunning() {
Node node = mNodesManager.findNodeById(mArgsStack.peek(), Node.class);
if (node instanceof ParamNode) {
return ((ParamNode) node).isRunning();
}
return ((ClockNode) node).isRunning;
}
}

View File

@ -0,0 +1,157 @@
package com.swmansion.reanimated.nodes;
import android.view.View;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.JavaOnlyMap;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import com.facebook.react.bridge.ReadableType;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.ReactStylesDiffMap;
import com.facebook.react.uimanager.UIImplementation;
import com.swmansion.reanimated.NodesManager;
import com.swmansion.reanimated.Utils;
import java.util.Map;
public class PropsNode extends Node implements FinalNode {
private final Map<String, Integer> mMapping;
private final UIImplementation mUIImplementation;
private int mConnectedViewTag = View.NO_ID;
private final JavaOnlyMap mPropMap;
private final ReactStylesDiffMap mDiffMap;
private static void addProp(WritableMap propMap, String key, Object value) {
if (value == null) {
propMap.putNull(key);
} else if (value instanceof Double) {
propMap.putDouble(key, (Double) value);
} else if (value instanceof Integer) {
propMap.putInt(key, (Integer) value);
} else if (value instanceof Number) {
propMap.putDouble(key, ((Number) value).doubleValue());
} else if (value instanceof Boolean) {
propMap.putBoolean(key, (Boolean) value);
} else if (value instanceof String) {
propMap.putString(key, (String) value);
} else if (value instanceof WritableArray) {
propMap.putArray(key, (WritableArray)value);
} else if (value instanceof WritableMap) {
propMap.putMap(key, (WritableMap)value);
} else {
throw new IllegalStateException("Unknown type of animated value");
}
}
public PropsNode(
int nodeID,
ReadableMap config,
NodesManager nodesManager,
UIImplementation uiImplementation) {
super(nodeID, config, nodesManager);
mMapping = Utils.processMapping(config.getMap("props"));
mUIImplementation = uiImplementation;
mPropMap = new JavaOnlyMap();
mDiffMap = new ReactStylesDiffMap(mPropMap);
}
public void connectToView(int viewTag) {
mConnectedViewTag = viewTag;
dangerouslyRescheduleEvaluate();
}
public void disconnectFromView(int viewTag) {
mConnectedViewTag = View.NO_ID;
}
@Override
protected Double evaluate() {
boolean hasUIProps = false;
boolean hasNativeProps = false;
boolean hasJSProps = false;
WritableMap jsProps = Arguments.createMap();
final WritableMap nativeProps = Arguments.createMap();
for (Map.Entry<String, Integer> entry : mMapping.entrySet()) {
Node node = mNodesManager.findNodeById(entry.getValue(), Node.class);
if (node instanceof StyleNode) {
WritableMap style = (WritableMap) node.value();
ReadableMapKeySetIterator iter = style.keySetIterator();
while (iter.hasNextKey()) {
String key = iter.nextKey();
WritableMap dest;
if (mNodesManager.uiProps.contains(key)) {
hasUIProps = true;
dest = mPropMap;
} else if (mNodesManager.nativeProps.contains(key)){
hasNativeProps = true;
dest = nativeProps;
} else {
hasJSProps = true;
dest = jsProps;
}
ReadableType type = style.getType(key);
switch (type) {
case Number:
dest.putDouble(key, style.getDouble(key));
break;
case String:
dest.putString(key, style.getString(key));
break;
case Array:
dest.putArray(key, (WritableArray) style.getArray(key));
break;
default:
throw new IllegalArgumentException("Unexpected type " + type);
}
}
} else {
String key = entry.getKey();
Object value = node.value();
if (mNodesManager.uiProps.contains(key)) {
hasUIProps = true;
addProp(mPropMap, key, value);
} else {
hasNativeProps = true;
addProp(nativeProps, key, value);
}
}
}
if (mConnectedViewTag != View.NO_ID) {
if (hasUIProps) {
mUIImplementation.synchronouslyUpdateViewOnUIThread(
mConnectedViewTag,
mDiffMap);
}
if (hasNativeProps) {
mNodesManager.enqueueUpdateViewOnNativeThread(mConnectedViewTag, nativeProps);
}
if (hasJSProps) {
WritableMap evt = Arguments.createMap();
evt.putInt("viewTag", mConnectedViewTag);
evt.putMap("props", jsProps);
mNodesManager.sendEvent("onReanimatedPropsChange", evt);
}
}
return ZERO;
}
@Override
public void update() {
// Since we are updating nodes after detaching them from views there is a time where it's
// possible that the view was disconnected and still receive an update, this is normal and
// we can simply skip that update.
if (mConnectedViewTag == View.NO_ID) {
return;
}
// call value for side effect (diff map update via changes made to prop map)
value();
}
}

View File

@ -0,0 +1,27 @@
package com.swmansion.reanimated.nodes;
import com.facebook.react.bridge.JSApplicationCausedNativeException;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.NoSuchKeyException;
import com.facebook.react.bridge.ReadableMap;
import com.swmansion.reanimated.MapUtils;
import com.swmansion.reanimated.NodesManager;
public class SetNode extends Node {
private int mWhatNodeID, mValueNodeID;
public SetNode(int nodeID, ReadableMap config, NodesManager nodesManager) {
super(nodeID, config, nodesManager);
mWhatNodeID = MapUtils.getInt(config, "what", "Reanimated: First argument passed to set node is either of wrong type or is missing.");
mValueNodeID = MapUtils.getInt(config, "value", "Reanimated: Second argument passed to set node is either of wrong type or is missing.");
}
@Override
protected Object evaluate() {
Object newValue = mNodesManager.getNodeValue(mValueNodeID);
ValueNode what = mNodesManager.findNodeById(mWhatNodeID, ValueNode.class);
what.setValue(newValue);
return newValue;
}
}

View File

@ -0,0 +1,43 @@
package com.swmansion.reanimated.nodes;
import com.facebook.react.bridge.JavaOnlyMap;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.swmansion.reanimated.NodesManager;
import com.swmansion.reanimated.Utils;
import java.util.Map;
import javax.annotation.Nullable;
public class StyleNode extends Node {
private final Map<String, Integer> mMapping;
public StyleNode(int nodeID, ReadableMap config, NodesManager nodesManager) {
super(nodeID, config, nodesManager);
mMapping = Utils.processMapping(config.getMap("style"));
}
@Override
protected WritableMap evaluate() {
JavaOnlyMap propMap = new JavaOnlyMap();
for (Map.Entry<String, Integer> entry : mMapping.entrySet()) {
Node node = mNodesManager.findNodeById(entry.getValue(), Node.class);
if (node instanceof TransformNode) {
propMap.putArray(entry.getKey(), (WritableArray) node.value());
} else {
Object val = node.value();
if (val instanceof Double) {
propMap.putDouble(entry.getKey(), (Double) val);
} else if (val instanceof String) {
propMap.putString(entry.getKey(), (String) val);
} else {
throw new IllegalStateException("Wrong style form");
}
}
}
return propMap;
}
}

View File

@ -0,0 +1,85 @@
package com.swmansion.reanimated.nodes;
import com.facebook.react.bridge.JavaOnlyArray;
import com.facebook.react.bridge.JavaOnlyMap;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableType;
import com.facebook.react.bridge.WritableArray;
import com.swmansion.reanimated.NodesManager;
import java.util.ArrayList;
import java.util.List;
public class TransformNode extends Node {
private static abstract class TransformConfig {
public String propertyName;
public abstract Object getValue(NodesManager nodesManager);
}
private static class AnimatedTransformConfig extends TransformConfig {
public int nodeID;
@Override
public Object getValue(NodesManager nodesManager) {
return nodesManager.getNodeValue(nodeID);
}
}
private static class StaticTransformConfig extends TransformConfig {
public Object value;
@Override
public Object getValue(NodesManager nodesManager) {
return value;
}
}
private static List<TransformConfig> processTransforms(ReadableArray transforms) {
List<TransformConfig> configs = new ArrayList<>(transforms.size());
for (int i = 0; i < transforms.size(); i++) {
ReadableMap transformConfigMap = transforms.getMap(i);
String property = transformConfigMap.getString("property");
if (transformConfigMap.hasKey("nodeID")) {
AnimatedTransformConfig transformConfig = new AnimatedTransformConfig();
transformConfig.propertyName = property;
transformConfig.nodeID = transformConfigMap.getInt("nodeID");
configs.add(transformConfig);
} else {
StaticTransformConfig transformConfig = new StaticTransformConfig();
transformConfig.propertyName = property;
ReadableType type = transformConfigMap.getType("value");
if(type == ReadableType.String) {
transformConfig.value = transformConfigMap.getString("value");
} else if(type == ReadableType.Array) {
transformConfig.value = transformConfigMap.getArray("value");
} else {
transformConfig.value = transformConfigMap.getDouble("value");
}
configs.add(transformConfig);
}
}
return configs;
}
private List<TransformConfig> mTransforms;
public TransformNode(int nodeID, ReadableMap config, NodesManager nodesManager) {
super(nodeID, config, nodesManager);
mTransforms = processTransforms(config.getArray("transform"));
}
@Override
protected WritableArray evaluate() {
List<JavaOnlyMap> transforms = new ArrayList<>(mTransforms.size());
for (TransformConfig transformConfig : mTransforms) {
transforms.add(
JavaOnlyMap.of(transformConfig.propertyName, transformConfig.getValue(mNodesManager)));
}
return JavaOnlyArray.from(transforms);
}
}

View File

@ -0,0 +1,40 @@
package com.swmansion.reanimated.nodes;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableType;
import com.swmansion.reanimated.NodesManager;
import javax.annotation.Nullable;
public class ValueNode extends Node {
private Object mValue;
public ValueNode(int nodeID, @Nullable ReadableMap config, NodesManager nodesManager) {
super(nodeID, config, nodesManager);
if (config == null || !config.hasKey("value")) {
mValue = null;
return;
}
ReadableType type = config.getType("value");
if (type == ReadableType.String) {
mValue = config.getString("value");
} else if (type == ReadableType.Number) {
mValue = config.getDouble("value");
} else if (type == ReadableType.Null) {
mValue = null;
} else {
throw new IllegalStateException("Not supported value type. Must be boolean, number or string");
}
}
public void setValue(Object value) {
mValue = value;
forceUpdateMemoizedValue(mValue);
}
@Override
protected Object evaluate() {
return mValue;
}
}

View File

@ -0,0 +1,82 @@
package com.swmansion.reanimated.transitions;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.TimeInterpolator;
import androidx.annotation.Nullable;
import androidx.transition.ChangeBounds;
import androidx.transition.ChangeTransform;
import androidx.transition.Transition;
import androidx.transition.TransitionPropagation;
import androidx.transition.TransitionValues;
import android.view.ViewGroup;
final class ChangeTransition extends Transition {
private final ChangeTransform mChangeTransform;
private final ChangeBounds mChangeBounds;
public ChangeTransition() {
mChangeTransform = new ChangeTransform();
mChangeBounds = new ChangeBounds();
}
@Override
public void captureStartValues(TransitionValues transitionValues) {
mChangeTransform.captureStartValues(transitionValues);
mChangeBounds.captureStartValues(transitionValues);
}
@Override
public void captureEndValues(TransitionValues transitionValues) {
mChangeTransform.captureEndValues(transitionValues);
mChangeBounds.captureEndValues(transitionValues);
}
@Override
public Transition setDuration(long duration) {
mChangeTransform.setDuration(duration);
mChangeBounds.setDuration(duration);
return super.setDuration(duration);
}
@Override
public Transition setStartDelay(long startDelay) {
mChangeTransform.setStartDelay(startDelay);
mChangeBounds.setStartDelay(startDelay);
return super.setStartDelay(startDelay);
}
@Override
public Transition setInterpolator(@Nullable TimeInterpolator interpolator) {
mChangeTransform.setInterpolator(interpolator);
mChangeBounds.setInterpolator(interpolator);
return super.setInterpolator(interpolator);
}
@Override
public void setPropagation(@Nullable TransitionPropagation transitionPropagation) {
mChangeTransform.setPropagation(transitionPropagation);
mChangeBounds.setPropagation(transitionPropagation);
super.setPropagation(transitionPropagation);
}
@Override
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) {
mChangeTransform.setReparent(false);
Animator changeTransformAnimator = mChangeTransform.createAnimator(sceneRoot, startValues, endValues);
Animator changeBoundsAnimator = mChangeBounds.createAnimator(sceneRoot, startValues, endValues);
if (changeTransformAnimator == null) {
return changeBoundsAnimator;
}
if (changeBoundsAnimator == null) {
return changeTransformAnimator;
}
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(changeTransformAnimator, changeBoundsAnimator);
return animatorSet;
}
}

View File

@ -0,0 +1,22 @@
package com.swmansion.reanimated.transitions;
import androidx.transition.SidePropagation;
import androidx.transition.Transition;
import androidx.transition.TransitionValues;
import android.view.View;
import android.view.ViewGroup;
public class SaneSidePropagation extends SidePropagation {
@Override
public long getStartDelay(ViewGroup sceneRoot, Transition transition, TransitionValues startValues, TransitionValues endValues) {
long delay = super.getStartDelay(sceneRoot, transition, startValues, endValues);
if (delay != 0) {
if (endValues == null || getViewVisibility(startValues) == View.VISIBLE) {
return -delay;
}
}
return delay;
}
}

View File

@ -0,0 +1,83 @@
package com.swmansion.reanimated.transitions;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import androidx.transition.Transition;
import androidx.transition.TransitionListenerAdapter;
import androidx.transition.TransitionValues;
import androidx.transition.Visibility;
import android.view.View;
import android.view.ViewGroup;
public class Scale extends Visibility {
static final String PROPNAME_SCALE_X = "scale:scaleX";
static final String PROPNAME_SCALE_Y = "scale:scaleY";
@Override
public void captureStartValues(TransitionValues transitionValues) {
super.captureStartValues(transitionValues);
transitionValues.values.put(PROPNAME_SCALE_X, transitionValues.view.getScaleX());
transitionValues.values.put(PROPNAME_SCALE_Y, transitionValues.view.getScaleY());
}
public Scale setDisappearedScale(float disappearedScale) {
if (disappearedScale < 0f) {
throw new IllegalArgumentException("disappearedScale cannot be negative!");
}
return this;
}
private Animator createAnimation(final View view, float startScale, float endScale, TransitionValues values) {
final float initialScaleX = view.getScaleX();
final float initialScaleY = view.getScaleY();
float startScaleX = initialScaleX * startScale;
float endScaleX = initialScaleX * endScale;
float startScaleY = initialScaleY * startScale;
float endScaleY = initialScaleY * endScale;
if (values != null) {
Float savedScaleX = (Float) values.values.get(PROPNAME_SCALE_X);
Float savedScaleY = (Float) values.values.get(PROPNAME_SCALE_Y);
// if saved value is not equal initial value it means that previous
// transition was interrupted and in the onTransitionEnd
// we've applied endScale. we should apply proper value to
// continue animation from the interrupted state
if (savedScaleX != null && savedScaleX != initialScaleX) {
startScaleX = savedScaleX;
}
if (savedScaleY != null && savedScaleY != initialScaleY) {
startScaleY = savedScaleY;
}
}
view.setScaleX(startScaleX);
view.setScaleY(startScaleY);
AnimatorSet animator = new AnimatorSet();
animator.playTogether(
ObjectAnimator.ofFloat(view, View.SCALE_X, startScaleX, endScaleX),
ObjectAnimator.ofFloat(view, View.SCALE_Y, startScaleY, endScaleY));
addListener(new TransitionListenerAdapter() {
@Override
public void onTransitionEnd(Transition transition) {
view.setScaleX(initialScaleX);
view.setScaleY(initialScaleY);
transition.removeListener(this);
}
});
return animator;
}
@Override
public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues) {
return createAnimation(view, 0f, 1f, startValues);
}
@Override
public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues) {
return createAnimation(view, 1f, 0f, startValues);
}
}

View File

@ -0,0 +1,45 @@
package com.swmansion.reanimated.transitions;
import androidx.transition.TransitionManager;
import android.view.View;
import android.view.ViewGroup;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.uimanager.IllegalViewOperationException;
import com.facebook.react.uimanager.NativeViewHierarchyManager;
import com.facebook.react.uimanager.UIBlock;
import com.facebook.react.uimanager.UIManagerModule;
public class TransitionModule {
private final UIManagerModule mUIManager;
public TransitionModule(UIManagerModule uiManager) {
mUIManager = uiManager;
}
public void animateNextTransition(final int rootTag, final ReadableMap config) {
mUIManager.prependUIBlock(new UIBlock() {
@Override
public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
try {
View rootView = nativeViewHierarchyManager.resolveView(rootTag);
if (rootView instanceof ViewGroup) {
ReadableArray transitions = config.getArray("transitions");
for (int i = 0, size = transitions.size(); i < size; i++) {
TransitionManager.beginDelayedTransition(
(ViewGroup) rootView,
TransitionUtils.inflate(transitions.getMap(i)));
}
}
} catch (IllegalViewOperationException ex) {
// ignore, view might have not been registered yet
}
}
});
}
}

View File

@ -0,0 +1,162 @@
package com.swmansion.reanimated.transitions;
import androidx.transition.ChangeBounds;
import androidx.transition.ChangeTransform;
import androidx.transition.Fade;
import androidx.transition.SidePropagation;
import androidx.transition.Slide;
import androidx.transition.Transition;
import androidx.transition.TransitionSet;
import androidx.transition.Visibility;
import android.view.Gravity;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.LinearInterpolator;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import javax.annotation.Nullable;
class TransitionUtils {
static @Nullable Transition inflate(ReadableMap config) {
String type = config.getString("type");
if ("group".equals(type)) {
return inflateGroup(config);
} else if ("in".equals(type)) {
return inflateIn(config);
} else if ("out".equals(type)) {
return inflateOut(config);
} else if ("change".equals(type)) {
return inflateChange(config);
}
throw new JSApplicationIllegalArgumentException("Unrecognized transition type " + type);
}
private static @Nullable Transition inflateGroup(ReadableMap config) {
TransitionSet set = new TransitionSet();
if (config.hasKey("sequence") && config.getBoolean("sequence")) {
set.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
} else {
set.setOrdering(TransitionSet.ORDERING_TOGETHER);
}
ReadableArray transitions = config.getArray("transitions");
for (int i = 0, size = transitions.size(); i < size; i++) {
Transition next = inflate(transitions.getMap(i));
if (next != null) {
set.addTransition(next);
}
}
return set;
}
static Visibility createVisibilityTransition(String type) {
if (type == null || "none".equals(type)) {
return null;
} else if ("fade".equals(type)) {
return new Fade(Fade.IN | Fade.OUT);
} else if ("scale".equals(type)) {
return new Scale();
} else if ("slide-top".equals(type)) {
return new Slide(Gravity.TOP);
} else if ("slide-bottom".equals(type)) {
return new Slide(Gravity.BOTTOM);
} else if ("slide-right".equals(type)) {
return new Slide(Gravity.RIGHT);
} else if ("slide-left".equals(type)) {
return new Slide(Gravity.LEFT);
}
throw new JSApplicationIllegalArgumentException("Invalid transition type " + type);
}
private static Transition inflateIn(ReadableMap config) {
Visibility transition = createTransition(config.getString("animation"));
if (transition == null) {
return null;
}
transition.setMode(Visibility.MODE_IN);
configureTransition(transition, config);
return transition;
}
private static Transition inflateOut(ReadableMap config) {
Visibility transition = createTransition(config.getString("animation"));
if (transition == null) {
return null;
}
transition.setMode(Visibility.MODE_OUT);
configureTransition(transition, config);
return transition;
}
private static Transition inflateChange(ReadableMap config) {
ChangeBounds changeBounds = new ChangeBounds();
ChangeTransform changeTransform = new ChangeTransform();
configureTransition(changeBounds, config);
configureTransition(changeTransform, config);
return new TransitionSet().addTransition(changeBounds).addTransition(changeTransform);
}
private static Visibility createTransition(String type) {
if (type == null || "none".equals(type)) {
return null;
} else if ("fade".equals(type)) {
return new Fade(Fade.IN | Fade.OUT);
} else if ("scale".equals(type)) {
return new Scale();
} else if ("slide-top".equals(type)) {
return new Slide(Gravity.TOP);
} else if ("slide-bottom".equals(type)) {
return new Slide(Gravity.BOTTOM);
} else if ("slide-right".equals(type)) {
return new Slide(Gravity.RIGHT);
} else if ("slide-left".equals(type)) {
return new Slide(Gravity.LEFT);
}
throw new JSApplicationIllegalArgumentException("Invalid transition type " + type);
}
private static void configureTransition(Transition transition, ReadableMap params) {
if (params.hasKey("durationMs")) {
int durationMs = params.getInt("durationMs");
transition.setDuration(durationMs);
}
if (params.hasKey("interpolation")) {
String interpolation = params.getString("interpolation");
if (interpolation.equals("easeIn")) {
transition.setInterpolator(new AccelerateInterpolator());
} else if (interpolation.equals("easeOut")) {
transition.setInterpolator(new DecelerateInterpolator());
} else if (interpolation.equals("easeInOut")) {
transition.setInterpolator(new AccelerateDecelerateInterpolator());
} else if (interpolation.equals("linear")) {
transition.setInterpolator(new LinearInterpolator());
} else {
throw new JSApplicationIllegalArgumentException("Invalid interpolation type " + interpolation);
}
}
if (params.hasKey("propagation")) {
String propagation = params.getString("propagation");
SidePropagation sidePropagation = new SaneSidePropagation();
if ("top".equals(propagation)) {
sidePropagation.setSide(Gravity.BOTTOM);
} else if ("bottom".equals(propagation)) {
sidePropagation.setSide(Gravity.TOP);
} else if ("left".equals(propagation)) {
sidePropagation.setSide(Gravity.RIGHT);
} else if ("right".equals(propagation)) {
sidePropagation.setSide(Gravity.LEFT);
}
transition.setPropagation(sidePropagation);
} else {
transition.setPropagation(null);
}
if (params.hasKey("delayMs")) {
int delayMs = params.getInt("delayMs");
transition.setStartDelay(delayMs);
}
}
}