Hani Q
Hani Q

Reputation: 155

scrollHandler and scrollTo in animated scrollview

Is there a way to use scrollHandler (for updating the shared value) and scrollTo together

https://snack.expo.dev/@haniq313/scrollview-with-handler-and-scrollto

Snack and code below.

Problem is when i press the Next button the scrollTo works but also triggers the scrollHandler. If i disable the scrollHandler then it works fine. but then i cant manually swipe and change values. Need to make both work. Is there someway to call scrollTo and at the same time not have the scrollHandler triggered ?

import React, { useState } from 'react';
import {
  Text,
  View,
  StyleSheet,
  ScrollView,
  Platform,
  SafeAreaView,
  TouchableOpacity,
} from 'react-native';
import Constants from 'expo-constants';
import Animated, {
  Extrapolate,
  interpolate,
  runOnJS,
  useAnimatedRef,
  useAnimatedScrollHandler,
  useAnimatedStyle,
  useDerivedValue,
  useSharedValue,
  scrollTo,
} from 'react-native-reanimated';
import { Ionicons } from '@expo/vector-icons';

const data = [
  'Jan',
  'Feb',
  'Mar',
  'Apr',
  'May',
  'Jun',
  'Jul',
  'Aug',
  'Sep',
  'Oct',
  'Nov',
  'Dec',
];

// You can import from local files
import AssetExample from './components/AssetExample';

const ITEM_HEIGHT = 25;
const ITEM_WIDTH = 200;
const VISIBLE_ITEMS = 3;
const FONT_SIZE = 16;

const SpinnerItem = (props) => {
  const { idx, label, scrollY } = props;

  const focusedStyle = useAnimatedStyle(() => {
    const opacity = interpolate(
      scrollY.value,
      [(idx - 2) * ITEM_HEIGHT, (idx - 1) * ITEM_HEIGHT, idx * ITEM_HEIGHT],
      [0.3, 1, 0.3],
      Extrapolate.CLAMP
    );

    const rotate = interpolate(
      scrollY.value,
      [(idx - 2) * ITEM_HEIGHT, (idx - 1) * ITEM_HEIGHT, idx * ITEM_HEIGHT],
      [-60, 0, 60],
      Extrapolate.CLAMP
    );

    return {
      opacity,
      transform: [{ rotateX: rotate + 'deg' }],
    };
  }, [idx, scrollY]);

  const fontStyle = useAnimatedStyle(() => {
    const fontStyle = interpolate(
      scrollY.value,
      [(idx - 2) * ITEM_HEIGHT, (idx - 1) * ITEM_HEIGHT, idx * ITEM_HEIGHT],
      [FONT_SIZE, FONT_SIZE + 8, FONT_SIZE],
      Extrapolate.CLAMP
    );

    return {
      fontSize: fontStyle,
    };
  }, [idx, scrollY]);

  return (
    <Animated.View
      style={[
        {
          alignItems: 'center',
          justifyContent: 'center',
          width: ITEM_WIDTH,
          height: ITEM_HEIGHT,
        },
        focusedStyle,
      ]}>
      <Animated.Text
        allowFontScaling={false}
        adjustsFontSizeToFit={false}
        style={[styles.label, fontStyle]}>
        {label}
      </Animated.Text>
    </Animated.View>
  );
};

export default function App() {
  const [selectedIndex, setSelectedIndex] = useState(0);
  const scrollY = useSharedValue(0);
  const aref = useAnimatedRef();
  const scrollHandler = useAnimatedScrollHandler({
    onScroll: (event) => {
      scrollY.value = event.contentOffset.y;
      runOnJS(setSelectedIndex)(Math.round(scrollY.value / ITEM_HEIGHT));
    },
  });

  useDerivedValue(() => {
    scrollTo(aref, 0, scrollY.value, true);
  });

  const incrementScroll = () => {
    'worklet';
    scrollY.value = scrollY.value + ITEM_HEIGHT;
    if (scrollY.value >= (data.length - 1) * ITEM_HEIGHT) scrollY.value = 0;
    runOnJS(setSelectedIndex)(Math.round(scrollY.value / ITEM_HEIGHT));
  };

  return (
    <SafeAreaView
      style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>{'Selected Index: ' + selectedIndex}</Text>
      <View style={styles.container}>
        <TouchableOpacity
          onPress={() => {
            incrementScroll();
          }}>
          <Ionicons name="ios-arrow-down-sharp" size={24} color="black" />
        </TouchableOpacity>
        <Animated.ScrollView
          showsVerticalScrollIndicator={false}
          bounces={false}
          ref={aref}
          decelerationRate={Platform.OS === 'ios' ? 0 : 0.98}
          renderToHardwareTextureAndroid
          contentContainerStyle={{
            alignItems: 'center',
            justifyContent: 'center',
          }}
          centerContent
          pinchGestureEnabled={false}
          snapToInterval={ITEM_HEIGHT}
          snapToAlignment="start"
          onScroll={scrollHandler}
          scrollEventThrottle={16}>
          {['spacer', ...data, 'spacer'].map((item, idx) => (
            <SpinnerItem
              key={idx}
              idx={idx}
              scrollY={scrollY}
              label={item === 'spacer' ? '' : item}
            />
          ))}
        </Animated.ScrollView>
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    alignSelf: 'center',
    backgroundColor: '#ecf0f1',
    height: ITEM_HEIGHT * 3,
    width: ITEM_WIDTH,
    overflow: 'hidden',
  },
  label: {
    textAlignVertical: 'center',
    textAlign: 'center',
    color: 'red',
    fontSize: FONT_SIZE,
    fontWeight: '700',
  },
});

Upvotes: 0

Views: 2833

Answers (1)

Alireza Hadjar
Alireza Hadjar

Reputation: 468

Try this one: link

You can simply add another animated variable and set it to true before scrolling and set it to false after scrolling. and in scroll handler check if the variable is true then don't execute inside of it

Upvotes: 3

Related Questions