Animated.event() takes a mapping that mirrors the event object and names the field that feeds each value. Pass the result to an animated component's onScroll with useNativeDriver: true, and the scroll view writes its offset into the value on the UI thread, with no JavaScript per frame. A reading progress bar:
const [maxY, setMaxY] = useState(1); // content height minus viewport height
const onScroll = Animated.event(
[{ nativeEvent: { contentOffset: { y: scrollY } } }], // event path -> value
{ useNativeDriver: true, listener: () => { calls.current += 1; } });
const progress = scrollY.interpolate({ inputRange: [0, maxY], outputRange: [0, 1],
extrapolate: 'clamp' });
// ...the bar, and the scroll view (which logs where each fling comes to rest):
<Animated.View style={{ height: 6, backgroundColor: '#1565c0',
transformOrigin: 'left', transform: [{ scaleX: progress }] }} />
</View>
<Animated.ScrollView onScroll={onScroll} scrollEventThrottle={16}Output
LOG rest at y = 542 of 3160 | JS listener calls: 13
After one fling (adb shell input swipe), the bar stood at 17% while JavaScript heard of the scroll 13 times. It scales from its left edge instead of animating width, a layout prop the native driver cannot touch. For a header that hides on scroll down and returns on scroll up, use Animated.diffClamp(scrollY, 0, 56).