Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Are flexbox animations from JS possible? #46

Closed
jlongster opened this issue Feb 6, 2015 · 16 comments
Closed

Are flexbox animations from JS possible? #46

jlongster opened this issue Feb 6, 2015 · 16 comments
Labels
Resolution: Locked This issue was locked by the bot.

Comments

@jlongster
Copy link

I'm writing another blog post about the theoretical possibility of applying React Native to the browser, running JS in a worker and using an asm.js-powered renderer on the main thread. (let's just assume that we can do text measurement and ignore a bunch of other big problems). I have an abstract question.

I know React Native handles animations, but I must be missing it in the source. I see InteractionManager which lets you schedule code to be run after animations are completed. But what is actually applying the animation?

Are we able to animate elements of a flexbox? Say we animated the width of an element from 50 to 100. If we did that, other elements with flex: 1 would need to re-adjust. Since we do the flexbox layout in the JS side (right?) we would need to perform the animation on the JS side. But that's not possible is it? Wouldn't they prone to all sorts of jank, not even from talking over the bridge but even GC stuff?

Of course native animations are supported, but I'm wondering about transitions with the flexbox layout system.

@jlongster jlongster changed the title What's the deal with animations? Are flexbox animations from JS possible? Feb 6, 2015
@gaearon
Copy link
Collaborator

gaearon commented Feb 6, 2015

I know React Native handles animations, but I must be missing it in the source. I see InteractionManager which lets you schedule code to be run after animations are completed. But what is actually applying the animation?

I'm not a React Native expert but I bet animations are supposed to be done with requestAnimationFrame which seems to be implemented. You may then be able to use something like:

https://github.com/chenglou/react-tween-state (stable)
https://github.com/chenglou/react-state-stream (bad perf, unstable, but potentially much more fun)

@gaearon
Copy link
Collaborator

gaearon commented Feb 6, 2015

I'm pretty sure I read some tweet saying requestAnimationFrame bridges to CADisplayLink so it should work properly.

@sahrens
Copy link
Contributor

sahrens commented Feb 6, 2015

This is a very complicated space, and we have several implementations internally that we are trying to unify and nail down.

It's definitely possible to use requestAnimationFrame and do whatever you want, but it takes some TLC to get good perf on slower devices. You can use setNativeProp to directly update specific properties without doing a full setState/render pass as a potential perf optimization. Internally we have an implementation that does this well (and also integrates with continuous gestures) that we would like to release soon.

We also have a system for doing global layout animations. Basically you just setState and update whatever (can be as complex a change as you want, creating new nodes, change flow direction from row to column, etc), and configure the global layout animation before returning control to native. Then the layout system (which does run in native but on a background thread) will compute the new layout, then animate all the resulting changes according to the specified config using standard UIView animation.

On Feb 6, 2015, at 8:38 AM, Dan Abramov notifications@github.com wrote:

I'm pretty sure I read some tweet saying requestAnimationFrame bridges to CADisplayLink so it should work properly.


Reply to this email directly or view it on GitHub.

@vjeux
Copy link
Contributor

vjeux commented Feb 6, 2015

Animations is a very complex and interesting problem. Here are the different options we've experimented with:

Declarative Animations

The first one is to have an API that starts an animation based on a ref.

this.startAnimation('ref', {
  type: this.AnimationTypes.easeInEaseOut,
  property: this.AnimationProperties.scaleXY,
  duration: 0.3,
  fromValue: [0, 0],
  toValue: [1, 1],
});

Right now we're using Pop to do that, but we should also be able to use CoreAnimation. CoreAnimation has the advantage of being executed in a different process with extremely high priority, whereas pop is in a different thread. However, CA doesn't support springs (unless you compute the keyframes yourself and send them to CA).

This kind of animation is very good for fire and forget, which is used in a lot of places. It is mostly insensible to JS thread stalls.

Gesture Driven Animations

If you want to implement a scroll away header, or an image viewer, you must animate based on touch position rather than time. The trigger is onScroll or onResponderMove and you've got to come up with the position/dimension/opacity of all the elements that are animated based on the position.

In order to apply those changes, you can either do setState, re-render and re-apply the diff algorithm. If you are careful with shouldComponentUpdate it is possible to get it fast enough. Another technique is to use ref.setNativeProps(). This is the equivalent of taking the dom node and modifying the attributes directly. This has almost no overhead but the modifications can be out of sync in the next render if not careful.

In order to come up with the interpolated values, there are two solutions. The first one is to do the math yourself, this works and is fast but the code is quickly impossible to understand and super hard to review. It's easier to factor your code a bit more declaratively by using interpolators.

var ToTheLeft = {
  opacity: {
    from: 1,
    to: 0.7,
    min: 0,
    max: 1,
    type: 'linear',
    extrapolate: false,
    round: 100,
  },
  left: {
    from: 0,
    to: -SCREEN_WIDTH * 0.3,
    min: 0,
    max: 1,
    type: 'linear',
    extrapolate: true,
    round: PixelRatio.get(),
  },
};

This makes it super clear how the animation works, but if implemented naively, is pretty slow. You've got to parse this structure and do dynamic execution based on what attributes there are. What we're doing instead is to pass this structure to a function called buildStyleInterpolator which generates specialized code to do the interpolation via a string that we then eval.

function(result, value) {
  var didChange = false;
  var nextScalarVal;
  var ratio;
  ratio = (value - 0) / 1;
  ratio = ratio > 1 ? 1 : (ratio < 0 ? 0 : ratio);
  nextScalarVal = Math.round(100 * (1 * (1 - ratio) + 0.7 * ratio)) / 100;
  if (!didChange) {
    var prevVal = result.opacity;
    result.opacity = nextScalarVal;
    didChange = didChange  || (nextScalarVal !== prevVal);
  } else {
    result.opacity = nextScalarVal;
  }
  ratio = (value - 0) / 1;
  nextScalarVal = Math.round(2 * (0 * (1 - ratio) + -30 * ratio)) / 2;
  if (!didChange) {
    var prevVal = result.left;
    result.left = nextScalarVal;
    didChange = didChange  || (nextScalarVal !== prevVal);
  } else {
    result.left = nextScalarVal;
  }
  return didChange;
}

This way we get the nice API and the nice perf :) We can also do some cool optimizations such as returning a boolean that tells us if anything changed to avoid sending unchanged values through the bridge.

Usually, once the touch is released, we compute the velocity and continue using a declarative animation.

Work Scheduling

There is no silver bullet for getting smooth 60fps animations, you've got to avoid doing anything else while animating. In React Native, none of the setTimeout and XHR callbacks are being invoked while there is a touch happening. This is a very strict rule that we're probably going to soften in the future.

We're also doing a similar orchestration technique via InteractionManager to make sure that no work is being done when an animation is being executed. Those techniques are very effective, when the animation/touch is running, there's almost no code except for computing top/left/width/height of a few elements that is being executed. Almost no code or allocations (no GC) happen and we get super smooth animations, even though they are in JS.

Yet, we still want to do some work while an interaction is going on, for example you want to display the next page of content that you fetched from the server during an infinite scroll. In order to be able to do that without dropping frames, Relay has been designed to be able to process data in small chunks, instead of freezing the JS thread for 300ms. We're also investigating running Relay data processing part in a separate thread.

Layout driven animations

The last piece that is unique to React Native, is the ability to animate based on layout changes. All the updates during a frame are batched together and we control the layout algorithm. This means that we can log the all the layout updates (top/left/width/height) that will happen and instead of setting them instantly, we can interpolate them over time. The code for it is extremely simple from a developer perspective:

componentWillUpdate: function(props) {
  if (props.isDatePickerShown !== this.props.isDatePickerShown) {
    Animation.Layout.configureNext(Animation.Layout.Presets.easeInOut);
  }
},

It will smoothly move around all the elements being displayed. There's a setting to decide how to interpolate new/deleted elements. This technique is useful for adding animations across the app very quickly.

Conclusion

By using a combination of those three techniques, we've been able to produce high quality, 60fps animations in many places of our apps. The great aspect is that, unlike with the web, we can play with various threading models and move work around to find the best tradeoffs.

There's still a lot of research to do here, for example it would be nice to send gesture driven animations to CoreAnimation for them to be executed on a different process. One idea we had was to run some React components in the main thread (similar to ScrollView being on the main thread but written in JS). The various APIs are still a bit hard to use and need some polish...

@vjeux
Copy link
Contributor

vjeux commented Feb 7, 2015

Closing as this is not super actionable. Please keep discussing here if need be :)

@vjeux vjeux closed this as completed Feb 7, 2015
@jlongster
Copy link
Author

Thanks for all the really detailed comments! I've been busy engaging people from yesterday's post so I haven't had a chance to read through it all yet. I'll probably respond here but it makes sense to close the issue.

@jordanna
Copy link
Contributor

jordanna commented Feb 9, 2015

Thank you @vjeux for the fantastic summary! I've been hacking in some pre-baked animations like zoom-in/out using CoreAnimation (applied post-layout) for a small prototype I'm working on. I noticed interface stubs like RCTAnimationConfig while digging in the guts of ReactKit, so this answers what it could be used for :)

Re: Declarative Animations

Are there any plans to make this fire and let me know when animation is done (vs. fire and forget)? I'm thinking either through callbacks that perhaps can be specified as additional arguments in this.startAnimation, or better yet, have it return a promise or something similar.

The primary use case for this is view dismissal animations, i.e. animation needs to finish before component unmounts. I admit I wasn't too keen on using ReactTransitionGroup (for the web project), so I developed a mixin that applied CSS animations in a similar declarative manner and allowed you to specify a callback function:

// zoom and fade out
this.animate({
    target: 'myRef',
    scale: 0.5,
    opacity: 0,
    duration: '250ms',
    callback: function() {
        // unmount
    }
});

This worked well for unmounting alone, but didn't nicely cover other use cases where other components or mixins were interested when the animation ends, e.g. logging. I'm looking to refactor it now with some RxJs, so this.animate returns an Rx Observable.

In any case, I'm going to hack in something similar in my ReactNative prototype for the time being. Definitely looking forward to trying out these animation APIs when they become available!

@vjeux
Copy link
Contributor

vjeux commented Feb 9, 2015

Sorry, "fire and forget" was misleading, there's a callback for when the animation is over so that you can chain another one/unmount the component/do some expensive computation

@jlongster
Copy link
Author

I've been thinking through this a lot still, and I don't really have a follow-up comment. Just wanted to say thanks again @vjeux for the detailed response. The "Layout driven animations" part was exactly what I was interested in.

@skevy
Copy link
Contributor

skevy commented Feb 9, 2015

I just want to chime in here and say @vjeux's explanation of animations is one of the best ways I've ever seen animation code described. +1 and bookmarking this thread for later reference.

@NicoleY77
Copy link

+1

@wootwoot1234
Copy link

Thanks @vjeux. I'm new to React Native and still trying to wrap my head around everything. I just re-read this thread after playing around with animations and it's very helpful.

I've been using AnimationExperimental, is that the same thing at Pop? If not, is there a good example out there of using Pop in React Native? If AnimationExperimental is an implementation of Pop, do you have an example of using the callback? I would like to chain animations.

@brentvatne
Copy link
Collaborator

@wootwoot1234 - AnimationExperimental is deprecated. It uses CoreAnimation explicit animations under the hood, not Pop :) Unfortunately we're in an awkward limbo phase where a new animation API is about to land, but isn't quite ready yet.. I would recommend looking at react-tween-state or rebound.js (which you can grep for in the react-native source and find it is used in multiple places) to do pure js animations for now.

@skevy
Copy link
Contributor

skevy commented Jun 19, 2015

@wootwoot1234 You can also use GSAP to do imperative animations on react
native. Simple demo here: http://github.com/skevy/react-native-gsap-demo
On Fri, Jun 19, 2015 at 2:33 PM Brent Vatne notifications@github.com
wrote:

@wootwoot1234 https://github.com/wootwoot1234 - AnimationExperimental
is deprecated. It uses CoreAnimation explicit animations under the hood,
not Pop :) Unfortunately we're in an awkward limbo phase where a new
animation API is about to land, but isn't quite ready yet.. I would
recommend looking at react-tween-state or rebound.js (which you can grep
for in the react-native source and find it is used in multiple places) to
do pure js animations for now.


Reply to this email directly or view it on GitHub
#46 (comment)
.

@wootwoot1234
Copy link

@brentvatne, you're all over the place! :) You've been really helpful to me today, thanks! I'll check out the js animations and will look forward to the new animations api.

@skevy thanks for the link, I'll check that out too.

dustturtle added a commit to dustturtle/react-native that referenced this issue Jul 6, 2016
…crash on simulator, on device I got nothing but app freezed)!

My app has an old version of JSONKit which is still using MRC. I think JSONKit is not needed if system version is available. Kicking out of JSONKit will make react native stronger.
Crash stack:
* thread facebook#11: tid = 0xbd672f, 0x000000010a10edeb imobii-waiqin`jk_encode_add_atom_to_buffer(encodeState=0x00007f9b820a1000, objectPtr=22 key/value pairs) + 16971 at JSONKit.m:2807, name = 'com.facebook.React.JavaScript', stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
    frame #0: 0x000000010a10edeb imobii-waiqin`jk_encode_add_atom_to_buffer(encodeState=0x00007f9b820a1000, objectPtr=22 key/value pairs) + 16971 at JSONKit.m:2807
    frame facebook#1: 0x000000010a10ef67 imobii-waiqin`jk_encode_add_atom_to_buffer(encodeState=0x00007f9b820a1000, objectPtr=2 key/value pairs) + 17351 at JSONKit.m:2811
    frame facebook#2: 0x000000010a10ef67 imobii-waiqin`jk_encode_add_atom_to_buffer(encodeState=0x00007f9b820a1000, objectPtr=25 key/value pairs) + 17351 at JSONKit.m:2811
    frame facebook#3: 0x000000010a10e768 imobii-waiqin`jk_encode_add_atom_to_buffer(encodeState=0x00007f9b820a1000, objectPtr=@"3 elements") + 15304 at JSONKit.m:2778
  * frame facebook#4: 0x000000010a10a26a imobii-waiqin`-[JKSerializer serializeObject:options:encodeOption:block:delegate:selector:error:](self=0x00007f9b831fbc80, _cmd="serializeObject:options:encodeOption:block:delegate:selector:error:", object=@"3 elements", optionFlags=0, encodeOption=10, block=0x0000000000000000, delegate=0x0000000000000000, selector=<no value available>, error=domain: class name = NSInvocation - code: 0) + 2250 at JSONKit.m:2876
    frame facebook#5: 0x000000010a109992 imobii-waiqin`+[JKSerializer serializeObject:options:encodeOption:block:delegate:selector:error:](self=JKSerializer, _cmd="serializeObject:options:encodeOption:block:delegate:selector:error:", object=@"3 elements", optionFlags=0, encodeOption=10, block=0x0000000000000000, delegate=0x0000000000000000, selector=<no value available>, error=domain: class name = NSInvocation - code: 0) + 178 at JSONKit.m:2831
    frame facebook#6: 0x000000010a10f700 imobii-waiqin`-[NSArray(self=@"3 elements", _cmd="JSONStringWithOptions:error:", serializeOptions=0, error=domain: class name = NSInvocation - code: 0) JSONStringWithOptions:error:] + 112 at JSONKit.m:2985
    frame facebook#7: 0x000000010ac13c02 imobii-waiqin`_RCTJSONStringifyNoRetry(jsonObject=@"3 elements", error=domain: class name = NSInvocation - code: 0) + 338 at RCTUtils.m:49
    frame facebook#8: 0x000000010ac13990 imobii-waiqin`RCTJSONStringify(jsonObject=@"3 elements", error=0x0000000000000000) + 128 at RCTUtils.m:77
    frame facebook#9: 0x000000010ab5fdfa imobii-waiqin`__27-[RCTContextExecutor setUp]_block_invoke_2(.block_descriptor=<unavailable>, moduleName=@"UIManager") + 218 at RCTContextExecutor.m:363
    frame facebook#10: 0x00000001134495cc CoreFoundation`__invoking___ + 140
    frame facebook#11: 0x000000011344941e CoreFoundation`-[NSInvocation invoke] + 286
    frame facebook#12: 0x000000010db13db3 JavaScriptCore`JSC::ObjCCallbackFunctionImpl::call(JSContext*, OpaqueJSValue*, unsigned long, OpaqueJSValue const* const*, OpaqueJSValue const**) + 451
    frame facebook#13: 0x000000010db13926 JavaScriptCore`JSC::objCCallbackFunctionCallAsFunction(OpaqueJSContext const*, OpaqueJSValue*, OpaqueJSValue*, unsigned long, OpaqueJSValue const* const*, OpaqueJSValue const**) + 262
    frame facebook#14: 0x000000010db14bad JavaScriptCore`long long JSC::APICallbackFunction::call<JSC::ObjCCallbackFunction>(JSC::ExecState*) + 573
    frame facebook#15: 0x000000010dade340 JavaScriptCore`JSC::LLInt::setUpCall(JSC::ExecState*, JSC::Instruction*, JSC::CodeSpecializationKind, JSC::JSValue, JSC::LLIntCallLinkInfo*) + 528
    frame facebook#16: 0x000000010dae535d JavaScriptCore`llint_entry + 22900
    frame facebook#17: 0x000000010dadf7d9 JavaScriptCore`vmEntryToJavaScript + 326
    frame facebook#18: 0x000000010d9b1959 JavaScriptCore`JSC::JITCode::execute(JSC::VM*, JSC::ProtoCallFrame*) + 169
    frame facebook#19: 0x000000010d9985ad JavaScriptCore`JSC::Interpreter::executeCall(JSC::ExecState*, JSC::JSObject*, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&) + 493
    frame facebook#20: 0x000000010d76cb7e JavaScriptCore`JSC::call(JSC::ExecState*, JSC::JSValue, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&) + 62
    frame facebook#21: 0x000000010d929a55 JavaScriptCore`JSC::callGetter(JSC::ExecState*, JSC::JSValue, JSC::JSValue) + 149
    frame facebook#22: 0x000000010dad49fb JavaScriptCore`llint_slow_path_get_by_id + 2203
    frame facebook#23: 0x000000010dae22f0 JavaScriptCore`llint_entry + 10503
    frame facebook#24: 0x000000010dae5368 JavaScriptCore`llint_entry + 22911
    frame facebook#25: 0x000000010dae52fd JavaScriptCore`llint_entry + 22804
    frame facebook#26: 0x000000010dae5368 JavaScriptCore`llint_entry + 22911
    frame facebook#27: 0x000000010dae5368 JavaScriptCore`llint_entry + 22911
    frame facebook#28: 0x000000010dae52fd JavaScriptCore`llint_entry + 22804
    frame facebook#29: 0x000000010dae5368 JavaScriptCore`llint_entry + 22911
    frame facebook#30: 0x000000010dae5368 JavaScriptCore`llint_entry + 22911
    frame facebook#31: 0x000000010dae5368 JavaScriptCore`llint_entry + 22911
    frame facebook#32: 0x000000010dae552a JavaScriptCore`llint_entry + 23361
    frame facebook#33: 0x000000010dae5368 JavaScriptCore`llint_entry + 22911
    frame facebook#34: 0x000000010dae5368 JavaScriptCore`llint_entry + 22911
    frame facebook#35: 0x000000010dadf7d9 JavaScriptCore`vmEntryToJavaScript + 326
    frame facebook#36: 0x000000010d9b1959 JavaScriptCore`JSC::JITCode::execute(JSC::VM*, JSC::ProtoCallFrame*) + 169
    frame facebook#37: 0x000000010d998264 JavaScriptCore`JSC::Interpreter::execute(JSC::ProgramExecutable*, JSC::ExecState*, JSC::JSObject*) + 10404
    frame facebook#38: 0x000000010d7a8786 JavaScriptCore`JSC::evaluate(JSC::ExecState*, JSC::SourceCode const&, JSC::JSValue, WTF::NakedPtr<JSC::Exception>&) + 470
    frame facebook#39: 0x000000010d9f6fb8 JavaScriptCore`JSEvaluateScript + 424
    frame facebook#40: 0x000000010ab6379e imobii-waiqin`__68-[RCTContextExecutor executeApplicationScript:sourceURL:onComplete:]_block_invoke.264(.block_descriptor=<unavailable>) + 414 at RCTContextExecutor.m:589
    frame facebook#41: 0x000000010ab63262 imobii-waiqin`__68-[RCTContextExecutor executeApplicationScript:sourceURL:onComplete:]_block_invoke(.block_descriptor=<unavailable>) + 498 at RCTContextExecutor.m:589
    frame facebook#42: 0x000000010ab63df8 imobii-waiqin`-[RCTContextExecutor executeBlockOnJavaScriptQueue:](self=0x00007f9b832f6040, _cmd="executeBlockOnJavaScriptQueue:", block=0x00007f9b80c92970) + 248 at RCTContextExecutor.m:627
    frame facebook#43: 0x000000010eb1d7a7 Foundation`__NSThreadPerformPerform + 283
    frame facebook#44: 0x0000000113486301 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    frame facebook#45: 0x000000011347c22c CoreFoundation`__CFRunLoopDoSources0 + 556
    frame facebook#46: 0x000000011347b6e3 CoreFoundation`__CFRunLoopRun + 867
    frame facebook#47: 0x000000011347b0f8 CoreFoundation`CFRunLoopRunSpecific + 488
    frame facebook#48: 0x000000010ab5e41b imobii-waiqin`+[RCTContextExecutor runRunLoopThread](self=RCTContextExecutor, _cmd="runRunLoopThread") + 363 at RCTContextExecutor.m:284
    frame facebook#49: 0x000000010ebc012b Foundation`__NSThread__start__ + 1198
    frame facebook#50: 0x00000001140869b1 libsystem_pthread.dylib`_pthread_body + 131
    frame facebook#51: 0x000000011408692e libsystem_pthread.dylib`_pthread_start + 168
    frame facebook#52: 0x0000000114084385 libsystem_pthread.dylib`thread_start + 13
@keithkml
Copy link

Hello, I read this entire thread and I don't understand the actual resolution.

Sorry for using terms from other bug tracking systems, but was this issue closed as "won't fix," "obsolete," or "invalid"? I'm trying to understand whether animating changes in flex components is possible in RN.

Thanks!

@facebook facebook locked as resolved and limited conversation to collaborators May 29, 2018
@react-native-bot react-native-bot added the Resolution: Locked This issue was locked by the bot. label Jul 23, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Resolution: Locked This issue was locked by the bot.
Projects
None yet
Development

No branches or pull requests