@@ -10,11 +10,8 @@ import type {
1010} from 'react-native' ;
1111
1212import Animated , {
13- Easing ,
14- ReduceMotion ,
15- useAnimatedStyle ,
16- useSharedValue ,
17- withTiming ,
13+ cubicBezier ,
14+ type AnimatedStyle ,
1815} from 'react-native-reanimated' ;
1916
2017import { CheckboxTokens } from './tokens' ;
@@ -123,7 +120,9 @@ const Checkbox = ({
123120 ...rest
124121} : Props ) => {
125122 const theme = useInternalTheme ( themeOverrides ) ;
123+
126124 const reduceMotion = useReduceMotion ( ) ;
125+
127126 const { direction } = useLocale ( ) ;
128127 // Web (react-native-web) doesn't auto-mirror layout, so flip the mask
129128 // anchor manually for RTL. Native handles it via `I18nManager`.
@@ -132,56 +131,6 @@ const Checkbox = ({
132131
133132 const selected = status === 'checked' || status === 'indeterminate' ;
134133
135- const reanimatedReduceMotion = reduceMotion
136- ? ReduceMotion . Always
137- : ReduceMotion . Never ;
138-
139- const fillTimingConfig = React . useMemo (
140- ( ) => ( {
141- duration : theme . motion . duration . short2 ,
142- easing : Easing . bezier ( ...theme . motion . easing . standard ) ,
143- reduceMotion : reanimatedReduceMotion ,
144- } ) ,
145- [
146- theme . motion . duration . short2 ,
147- theme . motion . easing . standard ,
148- reanimatedReduceMotion ,
149- ]
150- ) ;
151- const checkTimingConfig = React . useMemo (
152- ( ) => ( {
153- duration : theme . motion . duration . short3 ,
154- easing : Easing . bezier ( ...theme . motion . easing . standard ) ,
155- reduceMotion : reanimatedReduceMotion ,
156- } ) ,
157- [
158- theme . motion . duration . short3 ,
159- theme . motion . easing . standard ,
160- reanimatedReduceMotion ,
161- ]
162- ) ;
163-
164- // 0 = unselected (outline only), 1 = selected (filled + drawn icon).
165- const fillProgress = useSharedValue ( selected ? 1 : 0 ) ;
166- const checkProgress = useSharedValue ( selected ? 1 : 0 ) ;
167- const firstRender = React . useRef ( true ) ;
168-
169- React . useEffect ( ( ) => {
170- if ( firstRender . current ) {
171- firstRender . current = false ;
172- return ;
173- }
174- const target = selected ? 1 : 0 ;
175- fillProgress . value = withTiming ( target , fillTimingConfig ) ;
176- checkProgress . value = withTiming ( target , checkTimingConfig ) ;
177- } , [
178- selected ,
179- fillProgress ,
180- checkProgress ,
181- fillTimingConfig ,
182- checkTimingConfig ,
183- ] ) ;
184-
185134 // Visual state (colors + opacity) for the static layers. `hovered` /
186135 // `pressed` aren't tracked here — `TouchableRipple` owns the press ripple
187136 // and hover overlay.
@@ -194,19 +143,39 @@ const Checkbox = ({
194143 customUncheckedColor : uncheckedColor ,
195144 } ) ;
196145
197- // Outline fades out as fill fades in (and vice versa).
198- const outlineAnimatedStyle = useAnimatedStyle ( ( ) => ( {
199- opacity : 1 - fillProgress . value ,
200- } ) ) ;
146+ const transitionTimingFunction = cubicBezier ( ...theme . motion . easing . standard ) ;
147+
148+ const fillTransitionDuration = reduceMotion
149+ ? 0
150+ : theme . motion . duration . short2 ;
151+
152+ const checkTransitionDuration = reduceMotion
153+ ? 0
154+ : theme . motion . duration . short3 ;
155+
156+ const outlineStyle : AnimatedStyle < ViewStyle > = {
157+ borderColor : visual . outlineColor ,
158+ opacity : selected ? 0 : 1 ,
159+ transitionDuration : fillTransitionDuration ,
160+ transitionProperty : [ 'opacity' ] ,
161+ transitionTimingFunction,
162+ } ;
201163
202- const fillAnimatedStyle = useAnimatedStyle ( ( ) => ( {
203- opacity : fillProgress . value ,
204- } ) ) ;
164+ const fillStyle : AnimatedStyle < ViewStyle > = {
165+ backgroundColor : visual . containerColor ,
166+ opacity : selected ? 1 : 0 ,
167+ transitionDuration : fillTransitionDuration ,
168+ transitionProperty : [ 'opacity' ] ,
169+ transitionTimingFunction,
170+ } ;
205171
206- const maskAnimatedStyle = useAnimatedStyle ( ( ) => ( {
207- width : checkProgress . value * CONTAINER_SIZE ,
208- opacity : checkProgress . value ,
209- } ) ) ;
172+ const maskStyle : AnimatedStyle < ViewStyle > = {
173+ width : selected ? CONTAINER_SIZE : 0 ,
174+ opacity : selected ? 1 : 0 ,
175+ transitionDuration : checkTransitionDuration ,
176+ transitionProperty : [ 'width' , 'opacity' ] ,
177+ transitionTimingFunction,
178+ } ;
210179
211180 // Remember the last drawn glyph so the reveal-mask can finish collapsing
212181 // when `selected` flips back to false. Computed via the "derive state
@@ -283,27 +252,19 @@ const Checkbox = ({
283252 < View style = { [ styles . container , { opacity : visual . containerOpacity } ] } >
284253 < Animated . View
285254 pointerEvents = "none"
286- style = { [
287- styles . outline ,
288- { borderColor : visual . outlineColor } ,
289- outlineAnimatedStyle ,
290- ] }
255+ style = { [ styles . outline , outlineStyle ] }
291256 />
292257 < Animated . View
293258 pointerEvents = "none"
294- style = { [
295- styles . fill ,
296- { backgroundColor : visual . containerColor } ,
297- fillAnimatedStyle ,
298- ] }
259+ style = { [ styles . fill , fillStyle ] }
299260 />
300261 { showIndeterminate ? (
301262 < Animated . View
302263 style = { [
303264 flipMaskForWebRTL
304265 ? styles . checkmarkMaskWebRTL
305266 : styles . checkmarkMask ,
306- maskAnimatedStyle ,
267+ maskStyle ,
307268 ] }
308269 >
309270 < View style = { styles . checkmarkContent } >
@@ -315,7 +276,7 @@ const Checkbox = ({
315276 ) : (
316277 < Checkmark
317278 color = { visual . iconColor }
318- maskAnimatedStyle = { maskAnimatedStyle }
279+ maskStyle = { maskStyle }
319280 flipMaskForWebRTL = { flipMaskForWebRTL }
320281 isRTL = { direction === 'rtl' }
321282 autoSwapsBorders = { Platform . OS !== 'web' }
@@ -334,13 +295,13 @@ const Checkbox = ({
334295 */
335296const Checkmark = ( {
336297 color,
337- maskAnimatedStyle ,
298+ maskStyle ,
338299 flipMaskForWebRTL,
339300 isRTL,
340301 autoSwapsBorders,
341302} : {
342303 color : ColorValue ;
343- maskAnimatedStyle : ReturnType < typeof useAnimatedStyle > ;
304+ maskStyle : AnimatedStyle < ViewStyle > ;
344305 flipMaskForWebRTL : boolean ;
345306 isRTL : boolean ;
346307 // True on platforms whose layout engine auto-swaps `borderLeftWidth`
@@ -351,7 +312,7 @@ const Checkmark = ({
351312 < Animated . View
352313 style = { [
353314 flipMaskForWebRTL ? styles . checkmarkMaskWebRTL : styles . checkmarkMask ,
354- maskAnimatedStyle ,
315+ maskStyle ,
355316 ] }
356317 >
357318 < View style = { styles . checkmarkContent } >
0 commit comments