44 * This source code is licensed under the MIT license found in the
55 * LICENSE file in the root directory of this source tree.
66 *
7- * @fantom_flags useSharedAnimatedBackend:true updateRuntimeShadowNodeReferencesOnCommitThread:*
7+ * @fantom_flags useSharedAnimatedBackend:true updateRuntimeShadowNodeReferencesOnCommitThread:* optimizedAnimatedPropUpdates:*
88 * @flow strict-local
99 * @format
1010 */
@@ -17,7 +17,7 @@ import * as Fantom from '@react-native/fantom';
1717import nullthrows from 'nullthrows' ;
1818import * as React from 'react' ;
1919import { Component , createRef , memo , useEffect , useMemo , useState } from 'react' ;
20- import { Animated , View , useAnimatedValue } from 'react-native' ;
20+ import { Animated , Easing , View , useAnimatedValue } from 'react-native' ;
2121
2222// marginLeft (and the other margin props) are only on the native animated
2323// allowlist when the shared backend is enabled. This verifies the prop is
@@ -77,6 +77,196 @@ test('animate marginLeft layout prop', () => {
7777 ) ;
7878} ) ;
7979
80+ test ( 'non-layout props stay on the direct path while another view animates layout' , ( ) => {
81+ const movingRef = createRef < HostInstance > ( ) ;
82+
83+ let _translateX ;
84+ let _translateXAnimation ;
85+ let _siblingHeight ;
86+ let _siblingHeightAnimation ;
87+
88+ function MyApp ( ) {
89+ const translateX = useAnimatedValue ( 0 ) ;
90+ const siblingHeight = useAnimatedValue ( 10 ) ;
91+ _translateX = translateX ;
92+ _siblingHeight = siblingHeight ;
93+ return (
94+ < View collapsable = { false } >
95+ < Animated . View
96+ ref = { movingRef }
97+ style = { { width : 100 , height : 100 , transform : [ { translateX} ] } }
98+ />
99+ < Animated . View style = { { width : 100 , height : siblingHeight } } />
100+ </ View >
101+ ) ;
102+ }
103+
104+ const root = Fantom . createRoot ( ) ;
105+
106+ Fantom . runTask ( ( ) => {
107+ root . render ( < MyApp /> ) ;
108+ } ) ;
109+
110+ Fantom . runTask ( ( ) => {
111+ _translateXAnimation = Animated . timing ( _translateX , {
112+ toValue : 100 ,
113+ duration : 200 ,
114+ useNativeDriver : true ,
115+ } ) . start ( ) ;
116+ _siblingHeightAnimation = Animated . timing ( _siblingHeight , {
117+ toValue : 110 ,
118+ duration : 200 ,
119+ useNativeDriver : true ,
120+ } ) . start ( ) ;
121+ } ) ;
122+
123+ Fantom . unstable_produceFramesForDuration ( 100 ) ;
124+
125+ // The sibling's height went through a commit; the transform did not.
126+ expect ( root . getRenderedOutput ( { props : [ 'height' ] } ) . toJSX ( ) ) . toEqual (
127+ < rn-view >
128+ < rn-view key = { 0 } height = "100" />
129+ < rn-view key = { 1 } height = "60" />
130+ </ rn-view > ,
131+ ) ;
132+ expect (
133+ Fantom . unstable_getDirectManipulationProps ( nullthrows ( movingRef . current ) )
134+ . transform ,
135+ ) . toEqual ( [ { translateX : 50 } ] ) ;
136+
137+ Fantom . runTask ( ( ) => {
138+ _translateXAnimation ?. stop ( ) ;
139+ _siblingHeightAnimation ?. stop ( ) ;
140+ } ) ;
141+ } ) ;
142+
143+ test ( 'direct-path props survive a re-render while another view animates layout' , ( ) => {
144+ let _opacity ;
145+ let _opacityAnimation ;
146+ let _siblingHeight ;
147+ let _siblingHeightAnimation ;
148+ let _setWidth ;
149+
150+ function MyApp ( ) {
151+ const opacity = useAnimatedValue ( 0 ) ;
152+ const siblingHeight = useAnimatedValue ( 10 ) ;
153+ const [ width , setWidth ] = useState ( 100 ) ;
154+ _opacity = opacity ;
155+ _siblingHeight = siblingHeight ;
156+ _setWidth = setWidth ;
157+ return (
158+ < View collapsable = { false } >
159+ < Animated . View style = { { width, height : 100 , opacity} } />
160+ < Animated . View style = { { width : 100 , height : siblingHeight } } />
161+ </ View >
162+ ) ;
163+ }
164+
165+ const root = Fantom . createRoot ( ) ;
166+
167+ Fantom . runTask ( ( ) => {
168+ root . render ( < MyApp /> ) ;
169+ } ) ;
170+
171+ Fantom . runTask ( ( ) => {
172+ _opacityAnimation = Animated . timing ( _opacity , {
173+ toValue : 0.5 ,
174+ duration : 1000 ,
175+ useNativeDriver : true ,
176+ } ) . start ( ) ;
177+ _siblingHeightAnimation = Animated . timing ( _siblingHeight , {
178+ toValue : 110 ,
179+ duration : 1000 ,
180+ useNativeDriver : true ,
181+ } ) . start ( ) ;
182+ } ) ;
183+
184+ Fantom . unstable_produceFramesForDuration ( 500 ) ;
185+
186+ Fantom . runTask ( ( ) => {
187+ _setWidth ( 150 ) ;
188+ } ) ;
189+
190+ expect (
191+ root . getRenderedOutput ( { props : [ 'opacity' , 'width' , 'height' ] } ) . toJSX ( ) ,
192+ ) . toEqual (
193+ < rn-view >
194+ < rn-view key = { 0 } opacity = "0.25" width = "150" height = "100" />
195+ < rn-view key = { 1 } width = "100" height = "60" />
196+ </ rn-view > ,
197+ ) ;
198+
199+ Fantom . runTask ( ( ) => {
200+ _opacityAnimation ?. stop ( ) ;
201+ _siblingHeightAnimation ?. stop ( ) ;
202+ } ) ;
203+ } ) ;
204+
205+ test ( 'a view keeps writing its props directly after it starts animating layout' , ( ) => {
206+ const viewRef = createRef < HostInstance > ( ) ;
207+
208+ let _opacity ;
209+ let _opacityAnimation ;
210+ let _height ;
211+ let _heightAnimation ;
212+
213+ function MyApp ( ) {
214+ const opacity = useAnimatedValue ( 0 ) ;
215+ const height = useAnimatedValue ( 10 ) ;
216+ _opacity = opacity ;
217+ _height = height ;
218+ return (
219+ < Animated . View ref = { viewRef } style = { { width : 100 , height, opacity} } />
220+ ) ;
221+ }
222+
223+ const root = Fantom . createRoot ( ) ;
224+
225+ Fantom . runTask ( ( ) => {
226+ root . render ( < MyApp /> ) ;
227+ } ) ;
228+
229+ const viewElement = nullthrows ( viewRef . current ) ;
230+
231+ Fantom . runTask ( ( ) => {
232+ _opacityAnimation = Animated . timing ( _opacity , {
233+ toValue : 0.5 ,
234+ duration : 1000 ,
235+ easing : Easing . linear ,
236+ useNativeDriver : true ,
237+ } ) . start ( ) ;
238+ } ) ;
239+
240+ Fantom . unstable_produceFramesForDuration ( 500 ) ;
241+
242+ expect (
243+ Fantom . unstable_getDirectManipulationProps ( viewElement ) . opacity ,
244+ ) . toBeCloseTo ( 0.25 , 0.001 ) ;
245+
246+ Fantom . runTask ( ( ) => {
247+ _heightAnimation = Animated . timing ( _height , {
248+ toValue : 110 ,
249+ duration : 1000 ,
250+ easing : Easing . linear ,
251+ useNativeDriver : true ,
252+ } ) . start ( ) ;
253+ } ) ;
254+
255+ Fantom . unstable_produceFramesForDuration ( 250 ) ;
256+
257+ expect ( root . getRenderedOutput ( { props : [ 'height' ] } ) . toJSX ( ) ) . toEqual (
258+ < rn-view height = "35" /> ,
259+ ) ;
260+ expect (
261+ Fantom . unstable_getDirectManipulationProps ( viewElement ) . opacity ,
262+ ) . toBeCloseTo ( 0.375 , 0.001 ) ;
263+
264+ Fantom . runTask ( ( ) => {
265+ _opacityAnimation ?. stop ( ) ;
266+ _heightAnimation ?. stop ( ) ;
267+ } ) ;
268+ } ) ;
269+
80270test ( 'animated opacity' , ( ) => {
81271 let _opacity ;
82272 let _opacityAnimation ;
0 commit comments