Open breakingrobot opened 1 month ago
MotiView depending on which transition config is sent through props will not pass reduceMotion correctly.
MotiView
transition
reduceMotion
reduceMotion is broken while using:
delay
loop
type: timing
MotiView should respect the reduceMotion config passed through transition with any type or repeating and delaying props.
type
transition={{ type: 'timing' }}
import { View as MotiView } from 'moti'; import { ReduceMotion } from 'react-native-reanimated';
<MotiView style={{ backgroundColor: 'rgba(255, 0, 0, 0.3)', }} from={{ backgroundColor: 'rgba(255, 0, 0, 0.3)', }} animate={{ backgroundColor: colorAnimation, }} transition={{ type: 'timing', duration: 3000, reduceMotion: ReduceMotion.Never, }} />
2. Create a MotiView Component of transition `type: 'spring'` with loop or delaying transition config: ```ts import { View as MotiView } from 'moti'; import { ReduceMotion } from 'react-native-reanimated'; <MotiView style={{ position: 'relative', width: 250, height: 250, backgroundColor: 'rgba(255, 0, 0, 0.3)' }} from={{ rotate: '0deg', scale: 1, }} animate={{ rotate: '360deg', scale: scaleAnimation, }} transition={{ type: 'spring', duration: 3000, loop: true, reduceMotion: ReduceMotion.Never, }} /> ### Versions ```markdown - Moti: 0.29.0 - Reanimated: 3.11.0 - React Native: 0.74.1
Here, the red circle should rotate and loop even with ReduceMotion.Never
ReduceMotion.Never
import { View as MotiView } from 'moti'; import { View } from 'react-native' import { useEffect, useState } from 'react'; import { ReduceMotion } from 'react-native-reanimated'; const DebugMoti = () => { const [colorAnimation, setColorAnimation] = useState('rgba(255, 0, 0, 0.3)'); const [scaleAnimation, setScaleAnimation] = useState(1); useEffect(() => { setColorAnimation('rgba(0, 0, 255, 0.3)'); setScaleAnimation(Math.random() * 2 + 1); }, []); return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <MotiView style={{ position: 'relative', width: 250, height: 250 }} from={{ rotate: '0deg', scale: 1, }} animate={{ rotate: '360deg', scale: scaleAnimation, }} transition={{ type: 'timing', duration: 3000, loop: true, reduceMotion: ReduceMotion.Never, }} > <View style={{ width: 250, height: 250, backgroundColor: 'green' }} /> <MotiView style={{ position: 'absolute', top: 0, left: 0, width: 250, height: 250, backgroundColor: 'rgba(255, 0, 0, 0.3)', }} from={{ backgroundColor: 'rgba(255, 0, 0, 0.3)', }} animate={{ backgroundColor: colorAnimation, }} transition={{ type: 'sprint', duration: 3000, loop: true, reduceMotion: ReduceMotion.Never, }} /> </MotiView> </View> ); }; export default DebugMoti;
I have written a bit of a patch-package that fixes the problem but it is not perfect in any way.
diff --git a/src/core/use-motify.ts b/src/core/use-motify.ts index 5bbcd8f..bebb725 100644 --- a/src/core/use-motify.ts +++ b/src/core/use-motify.ts @@ -13,7 +13,7 @@ import { withDelay, withRepeat, withSequence, - runOnJS, + runOnJS, ReduceMotion, } from 'react-native-reanimated' import type { WithDecayConfig, @@ -165,6 +165,7 @@ function animationConfig<Animate>( // debug({ loop, key, repeatCount, animationType }) let config = {} + let reduceMotion = ReduceMotion.System; // so sad, but fix it later :( let animation = (...props: any): any => props @@ -177,12 +178,20 @@ function animationConfig<Animate>( (transition?.[key] as WithTimingConfig | undefined)?.easing ?? (transition as WithTimingConfig | undefined)?.easing + const timingReduceMotion = + (transition?.[key] as WithTimingConfig | undefined)?.reduceMotion ?? + (transition as WithTimingConfig | undefined)?.reduceMotion + if (easing) { config['easing'] = easing } if (duration != null) { config['duration'] = duration } + if (reduceMotion) { + reduceMotion = timingReduceMotion ?? reduceMotion + config['reduceMotion'] = reduceMotion + } animation = withTiming } else if (animationType === 'spring') { animation = withSpring @@ -191,6 +200,10 @@ function animationConfig<Animate>( const styleSpecificConfig = transition?.[key]?.[configKey] const transitionConfigForKey = transition?.[configKey] + if (configKey === 'reduceMotion') { + reduceMotion = transitionConfigForKey || styleSpecificConfig + } + if (styleSpecificConfig != null) { config[configKey] = styleSpecificConfig } else if (transitionConfigForKey != null) { @@ -212,6 +225,10 @@ function animationConfig<Animate>( const styleSpecificConfig = transition?.[key]?.[configKey] const transitionConfigForKey = transition?.[configKey] + if (configKey === 'reduceMotion') { + reduceMotion = transitionConfigForKey || styleSpecificConfig + } + if (styleSpecificConfig != null) { config[configKey] = styleSpecificConfig } else if (transitionConfigForKey != null) { @@ -227,6 +244,7 @@ function animationConfig<Animate>( return { animation, config, + reduceMotion, repeatReverse, repeatCount, shouldRepeat: !!repeatCount, @@ -260,6 +278,7 @@ const getSequenceArray = ( if (shouldPush) { let stepDelay = delayMs let stepValue = step + let stepReduceMotion = ReduceMotion.System; let stepConfig = Object.assign({}, config) let stepAnimation = animation as | typeof withTiming @@ -272,13 +291,14 @@ const getSequenceArray = ( delete stepTransition.delay delete stepTransition.value - const { config: inlineStepConfig, animation } = animationConfig( + const { config: inlineStepConfig, animation, reduceMotion } = animationConfig( sequenceKey, - stepTransition + stepTransition, ) stepConfig = Object.assign({}, stepConfig, inlineStepConfig) stepAnimation = animation + stepReduceMotion = reduceMotion if (step.delay != null) { stepDelay = step.delay @@ -304,7 +324,7 @@ const getSequenceArray = ( } ) if (stepDelay != null) { - sequence.push(withDelay(stepDelay, sequenceValue)) + sequence.push(withDelay(stepDelay, sequenceValue, stepReduceMotion)) } else { sequence.push(sequenceValue) } @@ -467,7 +487,7 @@ export function useMotify<Animate>({ value = value.value } - const { animation, config, shouldRepeat, repeatCount, repeatReverse } = + const { animation, config, reduceMotion, shouldRepeat, repeatCount, repeatReverse } = animationConfig(key, transition) const callback: ( @@ -543,7 +563,9 @@ export function useMotify<Animate>({ finalValue = withRepeat( finalValue, repeatCount, - repeatReverse + repeatReverse, + callback, + reduceMotion ) } transform[transformKey] = finalValue @@ -572,10 +594,10 @@ export function useMotify<Animate>({ let finalValue = animation(transformValue, config, callback) if (shouldRepeat) { - finalValue = withRepeat(finalValue, repeatCount, repeatReverse) + finalValue = withRepeat(finalValue, repeatCount, repeatReverse, undefined, reduceMotion) } if (delayMs != null) { - transform[transformKey] = withDelay(delayMs, finalValue) + transform[transformKey] = withDelay(delayMs, finalValue, reduceMotion) } else { transform[transformKey] = finalValue } @@ -602,7 +624,7 @@ export function useMotify<Animate>({ ) let finalValue = withSequence(sequence[0], ...sequence.slice(1)) if (shouldRepeat) { - finalValue = withRepeat(finalValue, repeatCount, repeatReverse) + finalValue = withRepeat(finalValue, repeatCount, repeatReverse, undefined, reduceMotion) } if (isTransform(key)) { @@ -634,10 +656,10 @@ export function useMotify<Animate>({ const transform = {} let finalValue = animation(value, config, callback) if (shouldRepeat) { - finalValue = withRepeat(finalValue, repeatCount, repeatReverse) + finalValue = withRepeat(finalValue, repeatCount, repeatReverse, undefined, reduceMotion) } if (delayMs != null) { - transform[key] = withDelay(delayMs, finalValue) + transform[key] = withDelay(delayMs, finalValue, reduceMotion) } else { transform[key] = finalValue } @@ -651,11 +673,11 @@ export function useMotify<Animate>({ let finalValue = animation(value, config, callback) if (shouldRepeat) { - finalValue = withRepeat(finalValue, repeatCount, repeatReverse) + finalValue = withRepeat(finalValue, repeatCount, repeatReverse, undefined, reduceMotion) } if (delayMs != null) { - final[key][innerStyleKey] = withDelay(delayMs, finalValue) + final[key][innerStyleKey] = withDelay(delayMs, finalValue, reduceMotion) } else { final[key][innerStyleKey] = finalValue } @@ -663,11 +685,11 @@ export function useMotify<Animate>({ } else { let finalValue = animation(value, config, callback) if (shouldRepeat) { - finalValue = withRepeat(finalValue, repeatCount, repeatReverse) + finalValue = withRepeat(finalValue, repeatCount, repeatReverse, undefined, reduceMotion) } if (delayMs != null && typeof delayMs === 'number') { - final[key] = withDelay(delayMs, finalValue) + final[key] = withDelay(delayMs, finalValue, reduceMotion) } else { final[key] = finalValue }
Is there an existing issue for this?
Do you want this issue prioritized?
Current Behavior
MotiView
depending on whichtransition
config is sent through props will not passreduceMotion
correctly.reduceMotion
is broken while using:delay
loop
or any repeat props.type: timing
Expected Behavior
MotiView
should respect thereduceMotion
config passed through transition with anytype
or repeating and delaying props.Steps To Reproduce
transition={{ type: 'timing' }}
<MotiView style={{ backgroundColor: 'rgba(255, 0, 0, 0.3)', }} from={{ backgroundColor: 'rgba(255, 0, 0, 0.3)', }} animate={{ backgroundColor: colorAnimation, }} transition={{ type: 'timing', duration: 3000, reduceMotion: ReduceMotion.Never, }} />
Screenshots
Here, the red circle should rotate and loop even with
ReduceMotion.Never
Reproduction
Workaround
I have written a bit of a patch-package that fixes the problem but it is not perfect in any way.