Skip to content

Commit

Permalink
Add ReducedMotionConfig component (#6164)
Browse files Browse the repository at this point in the history
## Summary

This PR adds `ReduceMotionConfig` component that allows to determine the
default animation behavior in response to the device's reduced motion
accessibility setting. It affects application globally. The default
behavior disables all animation if reduced motion is enabled on a
device. You can utilize this component to override that behavior.

### Usage

```js
function App() {
  
  return (
    // ...
    <ReduceMotionConfig mode={ReduceMotion.Never} />
    // ...
  );
}
```

### Demo


https://github.com/software-mansion/react-native-reanimated/assets/36106620/e15a484e-bdc1-454c-b9ff-3ae8863db62e


## Test plan

Open `Reduce Motion` example from example app.
  • Loading branch information
piaskowyk authored Jul 15, 2024
1 parent 1081ca1 commit 7194196
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 20 deletions.
16 changes: 15 additions & 1 deletion apps/common-app/src/examples/ReducedMotionExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Animated, {
withSpring,
withSequence,
ReduceMotion,
ReducedMotionConfig,
} from 'react-native-reanimated';
import {
Gesture,
Expand Down Expand Up @@ -126,18 +127,31 @@ const EXAMPLES = [

export default function ReducedMotionExample() {
const [currentExample, setCurrentExample] = useState(0);

const [reduceMotion, setReduceMotion] = useState(ReduceMotion.Never);
const { component, exampleList } = EXAMPLES[currentExample];

function handleReduceMotionModeChange() {
setReduceMotion(
reduceMotion === ReduceMotion.System
? ReduceMotion.Never
: ReduceMotion.System
);
}

return (
<View style={styles.container}>
<Button
onPress={handleReduceMotionModeChange}
title={`Overwrite reduce motion: ${reduceMotion}`}
/>
{EXAMPLES.map((example, i) => (
<Button
key={i}
onPress={() => setCurrentExample(i)}
title={example.title}
/>
))}
<ReducedMotionConfig mode={reduceMotion} />
{component}
<View key={currentExample}>
{exampleList.map((example, i) => {
Expand Down
9 changes: 0 additions & 9 deletions packages/react-native-reanimated/src/PlatformChecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,3 @@ export function isWindowAvailable() {
// @ts-ignore Fallback if `window` is undefined.
return typeof window !== 'undefined';
}

export function isReducedMotion() {
return isWeb()
? isWindowAvailable()
? // @ts-ignore Fallback if `window` is undefined.
!window.matchMedia('(prefers-reduced-motion: no-preference)').matches
: false
: !!(global as localGlobal)._REANIMATED_IS_REDUCED_MOTION;
}
25 changes: 25 additions & 0 deletions packages/react-native-reanimated/src/ReducedMotion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use strict';
import { isWeb, isWindowAvailable } from './PlatformChecker';
import { makeMutable } from './mutables';

type localGlobal = typeof global & Record<string, unknown>;

export function isReducedMotionEnabledInSystem() {
return isWeb()
? isWindowAvailable()
? // @ts-ignore Fallback if `window` is undefined.
!window.matchMedia('(prefers-reduced-motion: no-preference)').matches
: false
: !!(global as localGlobal)._REANIMATED_IS_REDUCED_MOTION;
}

const IS_REDUCED_MOTION_ENABLED_IN_SYSTEM = isReducedMotionEnabledInSystem();

export const ReducedMotionManager = {
jsValue: IS_REDUCED_MOTION_ENABLED_IN_SYSTEM,
uiValue: makeMutable(IS_REDUCED_MOTION_ENABLED_IN_SYSTEM),
setEnabled(value: boolean) {
ReducedMotionManager.jsValue = value;
ReducedMotionManager.uiValue.value = value;
},
};
9 changes: 5 additions & 4 deletions packages/react-native-reanimated/src/animation/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ import {
subtractMatrices,
getRotationMatrix,
} from './transformationMatrix/matrixUtils';
import { isReducedMotion, shouldBeUseWeb } from '../PlatformChecker';
import { shouldBeUseWeb } from '../PlatformChecker';
import type { EasingFunction, EasingFunctionFactory } from '../Easing';
import { ReducedMotionManager } from '../ReducedMotion';

let IN_STYLE_UPDATER = false;
const IS_REDUCED_MOTION = isReducedMotion();
const SHOULD_BE_USE_WEB = shouldBeUseWeb();

if (__DEV__ && IS_REDUCED_MOTION) {
if (__DEV__ && ReducedMotionManager.jsValue) {
console.warn(
`[Reanimated] Reduced motion setting is enabled on this device. This warning is visible only in the development mode. Some animations will be disabled by default. You can override the behavior for individual animations, see https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#reduced-motion-setting-is-enabled-on-this-device.`
);
Expand Down Expand Up @@ -108,10 +108,11 @@ export function recognizePrefixSuffix(
* Returns whether the motion should be reduced for a specified config.
* By default returns the system setting.
*/
const isReduceMotionOnUI = ReducedMotionManager.uiValue;
export function getReduceMotionFromConfig(config?: ReduceMotion) {
'worklet';
return !config || config === ReduceMotion.System
? IS_REDUCED_MOTION
? isReduceMotionOnUI.value
: config === ReduceMotion.Always;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use strict';
import { useEffect } from 'react';
import { ReduceMotion } from '../commonTypes';
import {
ReducedMotionManager,
isReducedMotionEnabledInSystem,
} from '../ReducedMotion';

/**
* A component that lets you overwrite default reduce motion behavior globally in your application.
*
* @param mode - Determines default reduce motion behavior globally in your application. Configured with {@link ReduceMotion} enum.
* @see https://docs.swmansion.com/react-native-reanimated/docs/components/ReduceMotionConfig
*/
export function ReducedMotionConfig({ mode }: { mode: ReduceMotion }) {
useEffect(() => {
if (!__DEV__) {
return;
}
console.warn(
`[Reanimated] Reduced motion setting is overwritten with mode '${mode}'.`
);
}, []);

useEffect(() => {
const wasEnabled = ReducedMotionManager.jsValue;
switch (mode) {
case ReduceMotion.System:
ReducedMotionManager.setEnabled(isReducedMotionEnabledInSystem());
break;
case ReduceMotion.Always:
ReducedMotionManager.setEnabled(true);
break;
case ReduceMotion.Never:
ReducedMotionManager.setEnabled(false);
break;
}
return () => {
ReducedMotionManager.setEnabled(wasEnabled);
};
}, [mode]);

return null;
}
6 changes: 3 additions & 3 deletions packages/react-native-reanimated/src/hook/useReducedMotion.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';
import { isReducedMotion } from '../PlatformChecker';
import { isReducedMotionEnabledInSystem } from '../ReducedMotion';

const IS_REDUCED_MOTION = isReducedMotion();
const IS_REDUCED_MOTION_ENABLED_IN_SYSTEM = isReducedMotionEnabledInSystem();

/**
* Lets you query the reduced motion system setting.
Expand All @@ -12,5 +12,5 @@ const IS_REDUCED_MOTION = isReducedMotion();
* @see https://docs.swmansion.com/react-native-reanimated/docs/device/useReducedMotion
*/
export function useReducedMotion() {
return IS_REDUCED_MOTION;
return IS_REDUCED_MOTION_ENABLED_IN_SYSTEM;
}
1 change: 1 addition & 0 deletions packages/react-native-reanimated/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ export {
export { LayoutAnimationConfig } from './component/LayoutAnimationConfig';
export { PerformanceMonitor } from './component/PerformanceMonitor';
export type { PerformanceMonitorProps } from './component/PerformanceMonitor';
export { ReducedMotionConfig } from './component/ReducedMotionConfig';
export type {
Adaptable,
AdaptTransforms,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ import { scheduleAnimationCleanup } from './domUtils';
import { _updatePropsJS } from '../../js-reanimated';
import type { ReanimatedHTMLElement } from '../../js-reanimated';
import { ReduceMotion } from '../../commonTypes';
import { isReducedMotion } from '../../PlatformChecker';
import { LayoutAnimationType } from '../animationBuilder/commonTypes';
import type { ReanimatedSnapshot, ScrollOffsets } from './componentStyle';
import { setElementPosition, snapshots } from './componentStyle';
import { Keyframe } from '../animationBuilder';
import { ReducedMotionManager } from '../../ReducedMotion';

function getEasingFromConfig(config: CustomConfig): string {
const easingName =
Expand Down Expand Up @@ -51,7 +51,7 @@ function getDelayFromConfig(config: CustomConfig): number {

export function getReducedMotionFromConfig(config: CustomConfig) {
if (!config.reduceMotionV) {
return isReducedMotion();
return ReducedMotionManager.jsValue;
}

switch (config.reduceMotionV) {
Expand All @@ -60,7 +60,7 @@ export function getReducedMotionFromConfig(config: CustomConfig) {
case ReduceMotion.Always:
return true;
default:
return isReducedMotion();
return ReducedMotionManager.jsValue;
}
}

Expand Down

0 comments on commit 7194196

Please sign in to comment.