-
Notifications
You must be signed in to change notification settings - Fork 24.3k
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
Comments
I'm not a React Native expert but I bet animations are supposed to be done with https://github.com/chenglou/react-tween-state (stable) |
I'm pretty sure I read some tweet saying |
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.
|
Animations is a very complex and interesting problem. Here are the different options we've experimented with: Declarative AnimationsThe 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 AnimationsIf 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 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 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 SchedulingThere 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 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 animationsThe 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. ConclusionBy 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... |
Closing as this is not super actionable. Please keep discussing here if need be :) |
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. |
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 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 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
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 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! |
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 |
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. |
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. |
+1 |
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. |
@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. |
@wootwoot1234 You can also use GSAP to do imperative animations on react
|
@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. |
…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
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! |
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.
The text was updated successfully, but these errors were encountered: