feat(hooks): usePulseAnimation for live indicators
This commit is contained in:
1 parent
ea70ecbaa4
commit
e8f828d90a
2 files changed
+93
No files matched your search
@@ -0,0 +1,49 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import TestRenderer from 'react-test-renderer';
|
||||||
|
|
||||||
|
jest.mock('react-native-reanimated', () => {
|
||||||
|
return {
|
||||||
|
Easing: { out: () => (() => 0), inOut: () => (() => 0), quad: () => 0 },
|
||||||
|
useSharedValue: (initial: number) => ({ value: initial }),
|
||||||
|
useAnimatedStyle: (fn: () => unknown) => fn(),
|
||||||
|
withTiming: (to: number) => to,
|
||||||
|
withRepeat: (to: number) => to,
|
||||||
|
cancelAnimation: () => undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
import { usePulseAnimation } from '../hooks/usePulseAnimation';
|
||||||
|
|
||||||
|
function Probe() {
|
||||||
|
const style = usePulseAnimation(false) as any;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('usePulseAnimation', () => {
|
||||||
|
it('returns initial opacity 0.5 and scale 1 when inactive', () => {
|
||||||
|
let captured: any = null;
|
||||||
|
function Capture() {
|
||||||
|
const style = usePulseAnimation(false) as any;
|
||||||
|
captured = style;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
TestRenderer.act(() => {
|
||||||
|
TestRenderer.create(React.createElement(Capture));
|
||||||
|
});
|
||||||
|
expect(captured.transform[0].scale).toBe(1);
|
||||||
|
expect(captured.opacity).toBe(0.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns opacity 1 when active', () => {
|
||||||
|
let captured: any = null;
|
||||||
|
function Capture() {
|
||||||
|
const style = usePulseAnimation(true) as any;
|
||||||
|
captured = style;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
TestRenderer.act(() => {
|
||||||
|
TestRenderer.create(React.createElement(Capture));
|
||||||
|
});
|
||||||
|
expect(captured.opacity).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Easing,
|
||||||
|
cancelAnimation,
|
||||||
|
useAnimatedStyle,
|
||||||
|
useSharedValue,
|
||||||
|
withRepeat,
|
||||||
|
withTiming,
|
||||||
|
} from 'react-native-reanimated';
|
||||||
|
|
||||||
|
type AnimatedStyle = {
|
||||||
|
transform: Array<{ scale: number }>;
|
||||||
|
opacity: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function usePulseAnimation(active: boolean, durationMs = 1600): AnimatedStyle {
|
||||||
|
const scale = useSharedValue(1);
|
||||||
|
const opacity = useSharedValue(active ? 1 : 0.5);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (active) {
|
||||||
|
scale.value = withRepeat(
|
||||||
|
withTiming(1.4, { duration: durationMs / 2, easing: Easing.out(Easing.quad) }),
|
||||||
|
-1,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
opacity.value = withRepeat(
|
||||||
|
withTiming(0.55, { duration: durationMs / 2, easing: Easing.inOut(Easing.quad) }),
|
||||||
|
-1,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
cancelAnimation(scale);
|
||||||
|
cancelAnimation(opacity);
|
||||||
|
scale.value = withTiming(1, { duration: 160 });
|
||||||
|
opacity.value = withTiming(0.5, { duration: 160 });
|
||||||
|
}
|
||||||
|
}, [active, durationMs, scale, opacity]);
|
||||||
|
|
||||||
|
return useAnimatedStyle<AnimatedStyle>(() => ({
|
||||||
|
transform: [{ scale: scale.value }],
|
||||||
|
opacity: opacity.value,
|
||||||
|
}));
|
||||||
|
}
|
||||||
Reference in new issue
Block a user