react-native-gesture-handler#State TypeScript Examples

The following examples show how to use react-native-gesture-handler#State. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: index.tsx    From lets-fork-native with MIT License 6 votes vote down vote up
constructor(props: Props) {
    super(props)
    this.state = {
      loading: false,
    }
    this.translationX = new Value(0)
    this.translationY = new Value(0)
    this.velocityX = new Value(0)
    this.offsetY = new Value(0)
    this.offsetX = new Value(0)
    this.gestureState = new Value(State.UNDETERMINED)
    this.onGestureEvent = event(
      [{
        nativeEvent: {
          translationX: this.translationX,
          translationY: this.translationY,
          velocityX: this.velocityX,
          state: this.gestureState,
        },
      }],
      { useNativeDriver: true },
    )
    this.init()
  }
Example #2
Source File: Notifier.tsx    From react-native-notifier with MIT License 6 votes vote down vote up
private onHandlerStateChange({ nativeEvent }: PanGestureHandlerStateChangeEvent) {
    if (nativeEvent.state === State.ACTIVE) {
      clearTimeout(this.hideTimer);
    }
    if (nativeEvent.oldState !== State.ACTIVE) {
      return;
    }
    this.setHideTimer();

    const swipePixelsToClose = -(this.showParams?.swipePixelsToClose ?? SWIPE_PIXELS_TO_CLOSE);
    const isSwipedOut = nativeEvent.translationY < swipePixelsToClose;

    Animated.timing(this.translateY, {
      toValue: isSwipedOut ? this.hiddenComponentValue : MAX_TRANSLATE_Y,
      easing: this.showParams?.swipeEasing,
      duration: this.showParams?.swipeAnimationDuration ?? SWIPE_ANIMATION_DURATION,
      useNativeDriver: true,
    }).start(() => {
      if (isSwipedOut) {
        this.onHidden();
      }
    });

    if (isSwipedOut) {
      this.onStartHiding();
    }
  }
Example #3
Source File: index.tsx    From lets-fork-native with MIT License 5 votes vote down vote up
init = (): void => {
    const clockX = new Clock()
    const clockY = new Clock()
    const {
      translationX,
      translationY,
      velocityX,
      gestureState,
      offsetY,
      offsetX,
    } = this
    gestureState.setValue(State.UNDETERMINED)
    translationX.setValue(0)
    translationY.setValue(0)
    velocityX.setValue(0)
    offsetY.setValue(0)
    offsetX.setValue(0)

    const finalTranslateX = add(translationX, multiply(0.2, velocityX))
    const translationThreshold = width / 4
    const snapPoint = cond(
      lessThan(finalTranslateX, -translationThreshold),
      -rotatedWidth,
      cond(greaterThan(finalTranslateX, translationThreshold), rotatedWidth, 0),
    )
    // TODO: handle case where the user drags the card again before the spring animation finished
    this.translateY = cond(
      eq(gestureState, State.END),
      [
        set(translationY, runSpring(clockY, translationY, 0)),
        set(offsetY, translationY),
        translationY,
      ],
      cond(eq(gestureState, State.BEGAN), [stopClock(clockY), translationY], translationY),
    )
    this.translateX = cond(
      eq(gestureState, State.END),
      [
        set(translationX, runSpring(clockX, translationX, snapPoint)),
        set(offsetX, translationX),
        cond(and(eq(clockRunning(clockX), 0), neq(translationX, 0)), [
          call([translationX], this.swiped),
        ]),
        translationX,
      ],
      cond(eq(gestureState, State.BEGAN), [stopClock(clockX), translationX], translationX),

    )
  };
Example #4
Source File: ImageTransformer.tsx    From react-native-gallery-toolkit with MIT License 5 votes vote down vote up
function checkIsNotUsed(handlerState: Animated.SharedValue<State>) {
  'worklet';

  return (
    handlerState.value !== State.UNDETERMINED &&
    handlerState.value !== State.END
  );
}
Example #5
Source File: InputContainer.tsx    From SQL-Play with GNU Affero General Public License v3.0 4 votes vote down vote up
InputContainer: FC<Props> = ({
  inputValue,
  setInputValue,
  isPremium,
  setPremiumModalOpen,
}) => {
  const historyOffset = useRef<number>(-1);
  const [autoCompleteTxt, setAutoCompleteTxt] = useState<string>('');

  const showPremiumAlert = (): void => {
    Alert.alert(
      'Go premium to unlock query history',
      'By going premium, you can unlock history with autocomplete and many more',
      [
        {text: 'Go Premium', onPress: () => setPremiumModalOpen(true)},
        {text: 'Close', style: 'cancel'},
      ],
    );
  };

  const onUpArrowPress = async (): Promise<void> => {
    /** show premium alert when user is not premium */
    if (!isPremium) {
      showPremiumAlert();
      return;
    }
    const lastCommand = await getLastUserCommand(historyOffset.current + 1);
    // console.log(historyOffset.current + 1, lastCommand);

    // only set if command is there
    if (lastCommand) {
      setInputValue(lastCommand);
      historyOffset.current++;
    }
  };
  const onDownArrowPress = async (): Promise<void> => {
    /** show premium alert when user is not premium */
    if (!isPremium) {
      showPremiumAlert();
      return;
    }

    if (historyOffset.current === 0) {
      return;
    } // do nothing if offset it 0

    const lastCommand = await getLastUserCommand(historyOffset.current - 1);
    // console.log(historyOffset.current - 1, lastCommand);
    // only set if command is there
    if (lastCommand) {
      setInputValue(lastCommand);
      historyOffset.current--;
    }
  };

  type CallbackType = (...args: string[]) => void;

  // eslint-disable-next-line react-hooks/exhaustive-deps
  const getAutoComplete = useCallback<CallbackType>(
    debounce(
      (val: string) =>
        findUserCommands(val).then(e => {
          // console.log('autocomplete', e);
          setAutoCompleteTxt(e);
        }),
      100,
    ),
    [],
  );

  useEffect(() => {
    // console.log(inputValue);
    if (!isPremium) {
      return;
    }
    if (inputValue !== '') {
      getAutoComplete(inputValue);
    } else {
      setAutoCompleteTxt('');
    }
  }, [getAutoComplete, inputValue, isPremium]);

  const clearInput = () => {
    setInputValue('');
  };

  const setAutoInput = () => {
    isPremium && setInputValue(autoCompleteTxt);
  };

  const handleSwipeLeft = ({
    nativeEvent,
  }: FlingGestureHandlerStateChangeEvent) => {
    if (nativeEvent.state === State.ACTIVE) {
      isPremium && clearInput();
    }
  };

  const handleSwipeRight = ({
    nativeEvent,
  }: FlingGestureHandlerStateChangeEvent) => {
    if (nativeEvent.oldState === State.ACTIVE) {
      setAutoInput();
    }
  };
  const styles = useDynamicValue(dynamicStyles);
  return (
    <View>
      <Text style={styles.inputHeader}>Type your SQL Query</Text>
      <View style={styles.inputContainer}>
        <GestureRecognizer
          onSwipeRight={setAutoInput}
          onSwipeLeft={() => isPremium && clearInput()}
        >
          <FlingGestureHandler
            direction={Directions.RIGHT}
            onHandlerStateChange={handleSwipeRight}
          >
            <FlingGestureHandler
              direction={Directions.LEFT}
              onHandlerStateChange={handleSwipeLeft}
            >
              <TextInput
                style={styles.input}
                autoFocus={true}
                testID={ids.queryTextInput}
                onChangeText={text => setInputValue(text)}
                multiline
                placeholderTextColor="gray"
                textAlignVertical="top"
                defaultValue={inputValue}
                keyboardType="ascii-capable"
                autoCorrect={false}
                numberOfLines={4}
                placeholder="Type your SQL query"
              />
            </FlingGestureHandler>
          </FlingGestureHandler>
        </GestureRecognizer>
        <Text
          suppressHighlighting={true}
          onLongPress={setAutoInput}
          style={styles.autoCompleteTxt}
        >
          {autoCompleteTxt}
        </Text>
      </View>
      <View style={styles.sideButtonContainer}>
        <TouchableOpacity
          accessibilityLabel="Up Button"
          accessibilityHint="gets the previous command from history"
          onPress={onUpArrowPress}
        >
          <Icon size={30} name="arrow-up-bold-box" color={sideButton} />
        </TouchableOpacity>
        <TouchableOpacity
          style={styles.downArrow}
          accessibilityLabel="Down Button"
          accessibilityHint="gets the next command from history"
          onPress={onDownArrowPress}
        >
          <Icon size={30} name="arrow-up-bold-box" color={sideButton} />
        </TouchableOpacity>
        <TouchableOpacity
          onPress={clearInput}
          accessibilityLabel="Clear command button"
          accessibilityHint="clear the command input"
        >
          <Icon size={30} name="text-box-remove" color={sideButton} />
        </TouchableOpacity>
      </View>
    </View>
  );
}
Example #6
Source File: RangeSlider.tsx    From react-native-range-slider-expo with MIT License 4 votes vote down vote up
RangeSlider = memo(({
  min, max, fromValueOnChange, toValueOnChange,
  step = 1,
  styleSize = 'medium',
  fromKnobColor = '#00a2ff',
  toKnobColor = '#00a2ff',
  inRangeBarColor = 'rgb(100,100,100)',
  outOfRangeBarColor = 'rgb(200,200,200)',
  valueLabelsBackgroundColor = '#3a4766',
  rangeLabelsTextColor = 'rgb(60,60,60)',
  showRangeLabels = true,
  showValueLabels = true,
  initialFromValue,
  initialToValue,
  knobSize: _knobSize,
  knobBubbleTextStyle = {},
  containerStyle: customContainerStyle = {},
  barHeight: customBarHeight,
  labelFormatter,
}: SliderProps) => {
  
  // settings
  const [wasInitialized, setWasInitialized] = useState(false);
  const [knobSize, setknobSize] = useState(0);
  const [barHeight, setBarHeight] = useState(0);
  const [stepInPixels, setStepInPixels] = useState(0);

  // rtl settings
  const [flexDirection, setFlexDirection] = useState<"row" | "row-reverse" | "column" | "column-reverse" | undefined>('row');

  const [fromValueOffset, setFromValueOffset] = useState(0);
  const [toValueOffset, setToValueOffset] = useState(0);
  const [sliderWidth, setSliderWidth] = useState(0);
  const [fromElevation, setFromElevation] = useState(3);
  const [toElevation, setToElevation] = useState(3);

  // animation values
  const [translateXfromValue] = useState(new Animated.Value(0));
  const [translateXtoValue] = useState(new Animated.Value(0));
  const [fromValueScale] = useState(new Animated.Value(0.01));
  const [toValueScale] = useState(new Animated.Value(0.01));
  const [rightBarScaleX] = useState(new Animated.Value(0.01));
  const [leftBarScaleX] = useState(new Animated.Value(0.01));

  // refs
  const toValueTextRef = React.createRef<TextInput>();
  const fromValueTextRef = React.createRef<TextInput>();
  const opacity = React.useRef<Animated.Value>(new Animated.Value(0)).current;
  const {formatLabel, decimalRound} = useUtils({step, labelFormatter});

  // initalizing settings
  useEffect(() => {
    setFlexDirection(osRtl ? 'row-reverse' : 'row');
  }, [knobSize]);
  
  useEffect(() => {
    if (wasInitialized) {
      const stepSize = setStepSize(max, min, step);
      fromValueTextRef.current?.setNativeProps({ text: formatLabel(min) });
      toValueTextRef.current?.setNativeProps({ text: formatLabel(min) });
      if (typeof initialFromValue === 'number' && initialFromValue >= min && initialFromValue <= max) {
        const offset = ((initialFromValue - min) / step) * stepSize - (knobSize / 2);
        setFromValueStatic(offset, knobSize, stepSize);
        setValueText(offset + knobSize, true);
      }
      if (typeof initialToValue === 'number' && initialToValue >= min && initialToValue <= max && typeof initialFromValue === 'number' && initialToValue > initialFromValue) {
        const offset = ((initialToValue - min) / step) * stepSize - (knobSize / 2);
        setToValueStatic(offset, knobSize, stepSize);
        setValueText(offset, false);
      }
      Animated.timing(opacity, {
        toValue: 1,
        duration: 64,
        useNativeDriver: true
      }).start();
    }
  }, [min, max, step, initialFromValue, initialToValue, wasInitialized]);

  useEffect(() => {
    const sizeBasedOnStyleSize = typeof styleSize === 'number' ? styleSize : styleSize === 'small' ? SMALL_SIZE : styleSize === 'medium' ? MEDIUM_SIZE : LARGE_SIZE;
    const size = _knobSize ?? sizeBasedOnStyleSize;
    setknobSize(customBarHeight ? Math.max(customBarHeight, size) : size);
    setBarHeight(customBarHeight ?? sizeBasedOnStyleSize / 3)
    translateXfromValue.setValue(-size / 4);
  }, [styleSize, customBarHeight]);
  
  // initalizing settings helpers
  const setFromValueStatic = (newOffset: number, knobSize: number, stepInPixels: number) => {
    newOffset = Math.floor((newOffset + (knobSize / 2)) / stepInPixels) * stepInPixels - (knobSize / 2);
    setFromValue(newOffset);
    setFromValueOffset(newOffset);
    const changeTo = Math.floor(((newOffset + (knobSize / 2)) * (max - min) / sliderWidth) / step) * step + min;
    fromValueOnChange(decimalRound(changeTo));
  }
  const setFromValue = (newOffset: number) => {
    translateXfromValue.setValue(newOffset);
    leftBarScaleX.setValue((newOffset + (knobSize / 2)) / sliderWidth + 0.01);
  }
  const setToValueStatic = (newOffset: number, knobSize: number, stepInPixels: number) => {
    newOffset = Math.ceil((newOffset + (knobSize / 2)) / stepInPixels) * stepInPixels - (knobSize / 2);
    setToValue(newOffset);
    setToValueOffset(newOffset);
    const changeTo = Math.ceil(((newOffset + (knobSize / 2)) * (max - min) / sliderWidth) / step) * step + min;
    toValueOnChange(decimalRound(changeTo));
  }
  const setToValue = (newOffset: number) => {
    translateXtoValue.setValue(newOffset);
    rightBarScaleX.setValue(1.01 - ((newOffset + (knobSize / 2)) / sliderWidth));
  }
  const setStepSize = (max: number, min: number, step: number) => {
    const numberOfSteps = ((max - min) / step);
    const stepSize = sliderWidth / numberOfSteps;
    setStepInPixels(stepSize);
    return stepSize;
  }
  const setValueText = (totalOffset: number, from = true) => {
    const isFrom = from && fromValueTextRef != null;
    const isTo = !from && toValueTextRef != null;
    if (isFrom || isTo) {
      const numericValue: number = Math[isFrom ? 'floor' : 'ceil'](((totalOffset + (knobSize / 2)) * (max - min) / sliderWidth) / step) * step + min;
      const text = formatLabel(numericValue);
      (isFrom ? fromValueTextRef : toValueTextRef).current?.setNativeProps({ text });
    }
  }

  // from value gesture events ------------------------------------------------------------------------
  const onGestureEventFromValue = (event: PanGestureHandlerGestureEvent) => {
    let totalOffset = event.nativeEvent.translationX + fromValueOffset;
    if (totalOffset >= -knobSize / 2 && totalOffset < toValueOffset) {
      translateXfromValue.setValue(totalOffset);
      setValueText(totalOffset, true);
      leftBarScaleX.setValue((totalOffset + (knobSize / 2)) / sliderWidth + 0.01);
    }
  }
  const onHandlerStateChangeFromValue = (event: PanGestureHandlerGestureEvent) => {
    if (event.nativeEvent.state === State.BEGAN) {
      scaleTo(fromValueScale, 1);
      setElevations(6, 5);
    }
    if (event.nativeEvent.state === State.END) {
      let newOffset = event.nativeEvent.translationX + fromValueOffset;
      newOffset = Math.floor((newOffset + (knobSize / 2)) / stepInPixels) * stepInPixels - (knobSize / 2);
      if (newOffset < -knobSize / 2) {
        newOffset = -knobSize / 2;
      } else if (newOffset >= toValueOffset) {
        newOffset = toValueOffset - stepInPixels;
      }
      setFromValueStatic(newOffset, knobSize, stepInPixels)
      scaleTo(fromValueScale, 0.01);
    }
  }
  // ------------------------------------------------------------------------------------------------

  // to value gesture events ------------------------------------------------------------------------
  const onGestureEventToValue = (event: PanGestureHandlerGestureEvent) => {
    const totalOffset = event.nativeEvent.translationX + toValueOffset;
    if (totalOffset <= sliderWidth - knobSize / 2 && totalOffset > fromValueOffset) {
      translateXtoValue.setValue(totalOffset);
      setValueText(totalOffset, false);
      rightBarScaleX.setValue(1.01 - ((totalOffset + (knobSize / 2)) / sliderWidth));
    }
  }
  const onHandlerStateChangeToValue = (event: PanGestureHandlerGestureEvent) => {
    if (event.nativeEvent.state === State.BEGAN) {
      scaleTo(toValueScale, 1);
      setElevations(5, 6);
    }
    if (event.nativeEvent.state === State.END) {
      let newOffset = event.nativeEvent.translationX + toValueOffset;
      newOffset = Math.ceil((newOffset + (knobSize / 2)) / stepInPixels) * stepInPixels - (knobSize / 2);
      if (newOffset > sliderWidth - knobSize / 2) {
        newOffset = sliderWidth - knobSize / 2;
      } else if (newOffset <= fromValueOffset) {
        newOffset = fromValueOffset + stepInPixels;
      }
      setToValueOffset(newOffset);
      translateXtoValue.setValue(newOffset);
      rightBarScaleX.setValue(1.01 - ((newOffset + (knobSize / 2)) / sliderWidth));
      scaleTo(toValueScale, 0.01);
      const changeTo = Math.ceil(((newOffset + (knobSize / 2)) * (max - min) / sliderWidth) / step) * step + min;
      toValueOnChange(decimalRound(changeTo));
    }
  }
  // ------------------------------------------------------------------------------------------------

  // gesture events help functions ------------------------------------------------------------------
  const scaleTo = (param: Animated.Value, toValue: number) => Animated.timing(param, {
    toValue,
    duration: 150,
    useNativeDriver: true
  }).start();

  const setElevations = (fromValue: number, toValue: number) => {
    setFromElevation(fromValue);
    setToElevation(toValue)
  }
  // ------------------------------------------------------------------------------------------------

  // setting bar width ------------------------------------------------------------------------------
  const onLayout = (event: LayoutChangeEvent) => {
    if (wasInitialized === false) {
      const { width } = event.nativeEvent.layout;
      setSliderWidth(width);
      translateXtoValue.setValue(width - knobSize / 2);
      setToValueOffset(width - knobSize / 2);
      setWasInitialized(true);
    }
  }
  // ------------------------------------------------------------------------------------------------

  const padding = useMemo(() => styleSize === 'large' ? 17 : styleSize === 'medium' ? 24 : 31, [styleSize]);

  return (
    <GestureHandlerRootView>
      <Animated.View style={[styles.container, { opacity, padding }, customContainerStyle]}>
        {
          showValueLabels &&
          <View style={{ width: '100%', height: 1, flexDirection }}>
            <KnobBubble {...{ knobSize, valueLabelsBackgroundColor }}
              translateX={translateXfromValue}
              scale={fromValueScale}
              textInputRef={fromValueTextRef}
              textStyle={knobBubbleTextStyle}
            />
            <KnobBubble {...{ knobSize, valueLabelsBackgroundColor }}
              translateX={translateXtoValue}
              scale={toValueScale}
              textInputRef={toValueTextRef}
              textStyle={knobBubbleTextStyle}
            />
          </View>
        }
        <View style={{ width: '100%', height: knobSize, marginVertical: 4, position: 'relative', flexDirection, alignItems: 'center' }}>
          <View style={{ position: 'absolute', backgroundColor: inRangeBarColor, left: knobSize / 4, marginLeft: -knobSize / 4, right: knobSize / 4, height: barHeight }} onLayout={onLayout} />
          <Animated.View style={{ position: 'absolute', left: knobSize / 4, marginLeft: -knobSize / 4, right: knobSize / 4, height: barHeight, backgroundColor: outOfRangeBarColor, transform: [{ translateX: sliderWidth / 2 }, { scaleX: rightBarScaleX }, { translateX: -sliderWidth / 2 }] }} />
          <Animated.View style={{ position: 'absolute', left: -knobSize / 4, width: knobSize / 2, height: barHeight, borderRadius: barHeight, backgroundColor: outOfRangeBarColor }} />
          <Animated.View style={{ width: sliderWidth, height: barHeight, backgroundColor: outOfRangeBarColor, transform: [{ translateX: -sliderWidth / 2 }, { scaleX: leftBarScaleX }, { translateX: sliderWidth / 2 }] }} />
          <Animated.View style={{ position: 'absolute', left: sliderWidth - knobSize / 4, width: knobSize / 2, height: barHeight, borderRadius: barHeight, backgroundColor: outOfRangeBarColor }} />
          <PanGestureHandler onGestureEvent={onGestureEventFromValue} onHandlerStateChange={onHandlerStateChangeFromValue}>
            <Animated.View style={[styles.knob, { height: knobSize, width: knobSize, borderRadius: knobSize, backgroundColor: fromKnobColor, elevation: fromElevation, transform: [{ translateX: translateXfromValue }] }]} />
          </PanGestureHandler>
          <PanGestureHandler onGestureEvent={onGestureEventToValue} onHandlerStateChange={onHandlerStateChangeToValue}>
            <Animated.View style={[styles.knob, { height: knobSize, width: knobSize, borderRadius: knobSize, backgroundColor: toKnobColor, elevation: toElevation, transform: [{ translateX: translateXtoValue }] }]} />
          </PanGestureHandler>
        </View>
        {
          showRangeLabels &&
          <View style={{ width: '100%', flexDirection, justifyContent: 'space-between' }}>
            <Text style={{ color: rangeLabelsTextColor, fontWeight: "bold", fontSize }}>{min}</Text>
            <Text style={{ color: rangeLabelsTextColor, fontWeight: "bold", fontSize }}>{max}</Text>
          </View>
        }
      </Animated.View>
    </GestureHandlerRootView>
  );
})
Example #7
Source File: Slider.tsx    From react-native-range-slider-expo with MIT License 4 votes vote down vote up
Slider = gestureHandlerRootHOC(({
    min, max, valueOnChange,
    step = 1,
    styleSize = 'medium',
    knobColor = '#00a2ff',
    inRangeBarColor = 'rgb(200,200,200)',
    outOfRangeBarColor = 'rgb(100,100,100)',
    knobBubbleTextStyle = {},
    valueLabelsBackgroundColor = '#3a4766',
    rangeLabelsTextColor = 'rgb(60,60,60)',
    showRangeLabels = true,
    showValueLabels = true,
    initialValue,
    containerStyle: customContainerStyle = {},
    labelFormatter,
}: SliderProps) => {

    // settings
    const [stepInPixels, setStepInPixels] = useState(0);
    const [knobSize, setknobSize] = useState(0);
    const [fontSize] = useState(15);

    // rtl settings
    const [flexDirection, setFlexDirection] = useState<"row" | "row-reverse" | "column" | "column-reverse" | undefined>('row');
    const [svgOffset, setSvgOffset] = useState<object>({ left: (knobSize - 40) / 2 });

    const [valueOffset, setValueOffset] = useState(0);
    const [sliderWidth, setSliderWidth] = useState(0);

    // animation values
    const [translateX] = useState(new Animated.Value(0));
    const [valueLabelScale] = useState(new Animated.Value(0.01));
    const [inRangeScaleX] = useState(new Animated.Value(0.01));

    // refs
    const valueTextRef = React.createRef<TextInput>();
    const opacity = React.useRef<Animated.Value>(new Animated.Value(0)).current;

  const {decimalRound, formatLabel} = useUtils({step, labelFormatter});

    // initalizing settings
    useEffect(() => {
        setFlexDirection(osRtl ? 'row-reverse' : 'row');
        setSvgOffset(osRtl ? { right: (knobSize - 40) / 2 } : { left: (knobSize - 40) / 2 });
    }, [knobSize]);

    useEffect(() => {
        if (sliderWidth > 0) {
            const stepSize = setStepSize(max, min, step);
            valueTextRef.current?.setNativeProps({ text: formatLabel(min) });
            if (typeof initialValue === 'number' && initialValue >= min && initialValue <= max) {
                const offset = ((initialValue - min) / step) * stepSize - (knobSize / 2);
                setValueStatic(offset, knobSize, stepSize);
                setValueText(offset);
            }
            Animated.timing(opacity, {
                toValue: 1,
                duration: 64,
                useNativeDriver: true
            }).start();
        }
    }, [min, max, step, initialValue, sliderWidth]);
    
    useEffect(() => {
        const size = typeof styleSize === 'number' ? styleSize : styleSize === 'small' ? SMALL_SIZE : styleSize === 'medium' ? MEDIUM_SIZE : LARGE_SIZE;
        setknobSize(size);
        translateX.setValue(-size / 4);
    }, [styleSize]);

    const setValueStatic = (newOffset: number, knobSize: number, stepInPixels: number) => {
        newOffset = Math.round((newOffset + (knobSize / 2)) / stepInPixels) * stepInPixels - (knobSize / 2);
        settingValue(newOffset);
        setValueOffset(newOffset);
        const changeTo = Math.round(((newOffset + (knobSize / 2)) * (max - min) / sliderWidth) / step) * step + min;
        valueOnChange(decimalRound(changeTo));
    }
    const settingValue = (newOffset: number) => {
        translateX.setValue(newOffset);
        inRangeScaleX.setValue((newOffset + (knobSize / 2)) / sliderWidth + 0.01);
    }
    const setValueText = (totalOffset: number) => {
        const numericValue: number = Math.floor(((totalOffset + (knobSize / 2)) * (max - min) / sliderWidth) / step) * step + min;
        const text = formatLabel(numericValue);
        valueTextRef.current?.setNativeProps({ text });
    }
    const setStepSize = (max: number, min: number, step: number) => {
        const numberOfSteps = ((max - min) / step);
        const stepSize = sliderWidth / numberOfSteps;
        setStepInPixels(stepSize);
        return stepSize;
    }

    // value gesture events ------------------------------------------------------------------------
    const onGestureEvent = (event: PanGestureHandlerGestureEvent) => {
        let totalOffset = event.nativeEvent.translationX + valueOffset;
        if (totalOffset >= - knobSize / 2 && totalOffset <= sliderWidth - knobSize / 2) {
            translateX.setValue(totalOffset);
            if (valueTextRef != null) {
              const labelValue = Math.round(((totalOffset + (knobSize / 2)) * (max - min) / sliderWidth) / step) * step + min;
              valueTextRef.current?.setNativeProps({ text: formatLabel(labelValue) });
            }
            inRangeScaleX.setValue((totalOffset + (knobSize / 2)) / sliderWidth + 0.01);
        }
    }
    const onHandlerStateChange = (event: PanGestureHandlerGestureEvent) => {
        if (event.nativeEvent.state === State.BEGAN) {
            scaleTo(valueLabelScale, 1);
        }
        if (event.nativeEvent.state === State.END) {
            let newOffset = event.nativeEvent.translationX + valueOffset;
            newOffset = Math.round((newOffset + (knobSize / 2)) / stepInPixels) * stepInPixels - (knobSize / 2);
            if (newOffset < -knobSize / 2) {
                newOffset = -knobSize / 2;
            } else if (newOffset >= sliderWidth - knobSize / 2) {
                newOffset = sliderWidth - knobSize / 2;
            }
            setValueStatic(newOffset, knobSize, stepInPixels);
            scaleTo(valueLabelScale, 0.01);
        }
    }
    // ------------------------------------------------------------------------------------------------

    // gesture events help functions ------------------------------------------------------------------
    const scaleTo = (param: Animated.Value, toValue: number) => Animated.timing(param,
        {
            toValue,
            duration: 150,
            useNativeDriver: true
        }
    ).start();
    // ------------------------------------------------------------------------------------------------

    // setting bar width ------------------------------------------------------------------------------
    const onLayout = (event: LayoutChangeEvent) => {
        setSliderWidth(event.nativeEvent.layout.width);
    }
    // ------------------------------------------------------------------------------------------------

    const padding = useMemo(() => styleSize === 'large' ? 17 : styleSize === 'medium' ? 24 : 31, [styleSize]);

    return (
    <GestureHandlerRootView>
      <Animated.View style={[styles.container, { opacity, padding }, customContainerStyle]}>
            {
                showValueLabels &&
                <View style={{ width: '100%', height: 1, flexDirection }}>
                    <Animated.View
                        style={{ position: 'absolute', bottom: 0, left: 0, transform: [{ translateX }, { scale: valueLabelScale }] }}
                    >
                        <Svg width={40} height={56} style={{ ...svgOffset, justifyContent: 'center', alignItems: 'center' }} >
                            <Path
                                d="M20.368027196163986,55.24077513402203 C20.368027196163986,55.00364778429386 37.12897994729114,42.11537830086061 39.19501224411266,22.754628132990383 C41.26104454093417,3.393877965120147 24.647119286738516,0.571820003300814 20.368027196163986,0.7019902620266703 C16.088935105589453,0.8321519518460209 -0.40167016290734386,3.5393865664909434 0.7742997013327574,21.806127302984205 C1.950269565572857,40.07286803947746 20.368027196163986,55.4779024837502 20.368027196163986,55.24077513402203 z"
                                strokeWidth={1}
                                fill={valueLabelsBackgroundColor}
                                stroke={valueLabelsBackgroundColor}
                            />
                        </Svg>
                        <TextInput style={[styles.knobBubbleText, svgOffset, knobBubbleTextStyle]} ref={valueTextRef} />
                    </Animated.View>
                </View>
            }
            <View style={{ width: '100%', height: knobSize, marginVertical: 4, position: 'relative', flexDirection, alignItems: 'center' }}>
                <View style={[styles.bar, { backgroundColor: inRangeBarColor, left: knobSize / 4, marginLeft: -knobSize / 4, right: knobSize / 4, height: knobSize / 3 }]} onLayout={onLayout} />
                <Animated.View style={{ width: sliderWidth, height: knobSize / 3, backgroundColor: outOfRangeBarColor, transform: [{ translateX: -sliderWidth / 2 }, { scaleX: inRangeScaleX }, { translateX: sliderWidth / 2 }] }} />
                <Animated.View style={{ position: 'absolute', left: -knobSize / 4, width: knobSize / 2.5, height: knobSize / 3, borderRadius: knobSize / 3, backgroundColor: outOfRangeBarColor }} />
                <PanGestureHandler {...{ onGestureEvent, onHandlerStateChange }}>
                    <Animated.View style={[styles.knob, { height: knobSize, width: knobSize, borderRadius: knobSize, backgroundColor: knobColor, transform: [{ translateX }] }]} />
                </PanGestureHandler>
            </View>
            {
                showRangeLabels &&
                <View style={{ width: '100%', flexDirection, justifyContent: 'space-between' }}>
                    <Text style={{ color: rangeLabelsTextColor, fontWeight: "bold", fontSize, marginLeft: -7 }}>{min}</Text>
                    <Text style={{ color: rangeLabelsTextColor, fontWeight: "bold", fontSize, marginRight: 7 }}>{max}</Text>
                </View>
            }
        </Animated.View>
    </GestureHandlerRootView>
    );
})
Example #8
Source File: TextSlider.tsx    From react-native-range-slider-expo with MIT License 4 votes vote down vote up
TextualSlider = gestureHandlerRootHOC(({
    values, valueOnChange,
    styleSize = 'medium',
    knobColor = '#00a2ff',
    inRangeBarColor = 'rgb(200,200,200)',
    outOfRangeBarColor = 'rgb(100,100,100)',
    valueLabelsTextColor = 'white',
    valueLabelsBackgroundColor = '#3a4766',
    rangeLabelsStyle,
    showRangeLabels = true,
    showValueLabels = true,
    initialValue
}: TextualSliderProps) => {

    // settings
    const [stepInPixels, setStepInPixels] = useState(0);
    const [knobSize, setknobSize] = useState(0);

    const [max, setMax] = useState(1);

    // rtl settings
    const [flexDirection, setFlexDirection] = useState<"row" | "row-reverse" | "column" | "column-reverse" | undefined>('row');
    const [svgOffset, setSvgOffset] = useState<object>({ left: (knobSize - 40) / 2 });

    const [valueOffset, setValueOffset] = useState(0);
    const [TextualSliderWidth, setTextualSliderWidth] = useState(0);

    // animation values
    const [translateX] = useState(new Animated.Value(0));
    const [valueLabelScale] = useState(new Animated.Value(0.01));
    const [inRangeScaleX] = useState(new Animated.Value(0.01));

    // refs
    const valueTextRef = React.createRef<TextInput>();
    const opacity = React.useRef<Animated.Value>(new Animated.Value(0)).current;

    // initalizing settings
    useEffect(() => {
        setMax(values.length - 1);
        setFlexDirection(osRtl ? 'row-reverse' : 'row');
        setSvgOffset(osRtl ? { right: (knobSize - 40) / 2 } : { left: (knobSize - 40) / 2 });
    }, []);
    useEffect(() => {
        if (TextualSliderWidth > 0) {
            const stepSize = setStepSize(max, min, step);
            valueTextRef.current?.setNativeProps({ text: values[min].text });
            if (typeof initialValue === 'number' && initialValue >= min && initialValue <= max) {
                const offset = ((initialValue - min) / step) * stepSize - (knobSize / 2);
                setValueStatic(offset, knobSize, stepSize);
                setValueText(offset);
            }
            Animated.timing(opacity, {
                toValue: 1,
                duration: 64,
                useNativeDriver: true
            }).start();
        }
    }, [min, max, step, initialValue, TextualSliderWidth]);
    useEffect(() => {
        const size = typeof styleSize === 'number' ? styleSize : styleSize === 'small' ? SMALL_SIZE : styleSize === 'medium' ? MEDIUM_SIZE : LARGE_SIZE;
        setknobSize(size);
        translateX.setValue(-size / 4);
    }, [styleSize]);

    const setValueStatic = (newOffset: number, knobSize: number, stepInPixels: number) => {
        newOffset = Math.round((newOffset + (knobSize / 2)) / stepInPixels) * stepInPixels - (knobSize / 2);
        settingValue(newOffset);
        setValueOffset(newOffset);
        const index = Math.round(((newOffset + (knobSize / 2)) * (max - min) / TextualSliderWidth) / step) * step + min;
        valueOnChange(values[index]);
    }
    const settingValue = (newOffset: number) => {
        translateX.setValue(newOffset);
        inRangeScaleX.setValue((newOffset + (knobSize / 2)) / TextualSliderWidth + 0.01);
    }
    const setValueText = (totalOffset: number) => {
        const numericValue: number = Math.floor(((totalOffset + (knobSize / 2)) * (max - min) / TextualSliderWidth) / step) * step + min;
        valueTextRef.current?.setNativeProps({ text: values[numericValue].text });
    }
    const setStepSize = (max: number, min: number, step: number) => {
        const numberOfSteps = ((max - min) / step);
        const stepSize = TextualSliderWidth / numberOfSteps;
        setStepInPixels(stepSize);
        return stepSize;
    }

    // value gesture events ------------------------------------------------------------------------
    const onGestureEvent = (event: PanGestureHandlerGestureEvent) => {
        let totalOffset = event.nativeEvent.translationX + valueOffset;
        if (totalOffset >= - knobSize / 2 && totalOffset <= TextualSliderWidth - knobSize / 2) {
            translateX.setValue(totalOffset);
            if (valueTextRef != null) {
                const index = Math.round(((totalOffset + (knobSize / 2)) * (max - min) / TextualSliderWidth) / step) * step + min;
                valueTextRef.current?.setNativeProps({ text: values[index].text });
            }
            inRangeScaleX.setValue((totalOffset + (knobSize / 2)) / TextualSliderWidth + 0.01);
        }
    }
    const onHandlerStateChange = (event: PanGestureHandlerGestureEvent) => {
        if (event.nativeEvent.state === State.BEGAN) {
            scaleTo(valueLabelScale, 1);
        }
        if (event.nativeEvent.state === State.END) {
            let newOffset = event.nativeEvent.translationX + valueOffset;
            newOffset = Math.round((newOffset + (knobSize / 2)) / stepInPixels) * stepInPixels - (knobSize / 2);
            if (newOffset < -knobSize / 2) {
                newOffset = -knobSize / 2;
            } else if (newOffset >= TextualSliderWidth - knobSize / 2) {
                newOffset = TextualSliderWidth - knobSize / 2;
            }
            setValueStatic(newOffset, knobSize, stepInPixels);
            scaleTo(valueLabelScale, 0.01);
        }
    }
    // ------------------------------------------------------------------------------------------------

    // gesture events help functions ------------------------------------------------------------------
    const scaleTo = (param: Animated.Value, toValue: number) => Animated.timing(param,
        {
            toValue,
            duration: 150,
            useNativeDriver: true
        }
    ).start();
    // ------------------------------------------------------------------------------------------------

    // setting bar width ------------------------------------------------------------------------------
    const onLayout = (event: LayoutChangeEvent) => {
        setTextualSliderWidth(event.nativeEvent.layout.width);
    }
    // ------------------------------------------------------------------------------------------------

    const labelOpacity = valueLabelScale.interpolate({
        inputRange: [0.1, 1],
        outputRange: [0, 1]
    })
    return (
    <GestureHandlerRootView>
        <Animated.View style={[styles.container, { opacity, padding: styleSize === 'large' ? 7 : styleSize === 'medium' ? 14 : 21 }]}>
            {
                showValueLabels &&
                <View style={{ width: '100%', flexDirection }}>
                    <Animated.View
                        style={{ position: 'absolute', bottom: 0, left: 0, opacity: labelOpacity, transform: [{ translateX }, { scale: valueLabelScale }] }}
                    >
                        <View style={{ width: '100%', alignItems: 'center' }}>
                            <TextInput style={{ ...svgOffset, color: valueLabelsTextColor, fontWeight: 'bold', backgroundColor: valueLabelsBackgroundColor, paddingHorizontal: 20, paddingVertical: 5, borderRadius: 3 }} ref={valueTextRef} />
                        </View>
                    </Animated.View>
                </View>
            }
            <View style={{ width: '100%', height: knobSize, marginVertical: 4, position: 'relative', flexDirection, alignItems: 'center' }}>
                <View style={[styles.bar, { backgroundColor: inRangeBarColor, left: knobSize / 4, marginLeft: -knobSize / 4, right: knobSize / 4, height: knobSize / 3 }]} onLayout={onLayout} />
                <Animated.View style={{ width: TextualSliderWidth, height: knobSize / 3, backgroundColor: outOfRangeBarColor, transform: [{ translateX: -TextualSliderWidth / 2 }, { scaleX: inRangeScaleX }, { translateX: TextualSliderWidth / 2 }] }} />
                <Animated.View style={{ position: 'absolute', left: -knobSize / 4, width: knobSize / 2.5, height: knobSize / 3, borderRadius: knobSize / 3, backgroundColor: outOfRangeBarColor }} />
                <PanGestureHandler {...{ onGestureEvent, onHandlerStateChange }}>
                    <Animated.View style={[styles.knob, { height: knobSize, width: knobSize, borderRadius: knobSize, backgroundColor: knobColor, transform: [{ translateX }] }]} />
                </PanGestureHandler>
            </View>
            {
                showRangeLabels &&
                <View style={{ width: '100%', flexDirection, justifyContent: 'space-between' }}>
                    <Text style={[rangeLabelsStyle, { fontWeight: "bold", marginLeft: -7 }]}>{values.length > 1 ? values[0].text : ''}</Text>
                    <Text style={[rangeLabelsStyle, { fontWeight: "bold" }]}>{values.length > 1 ? values[max].text : ''}</Text>
                </View>
            }
        </Animated.View>
    </GestureHandlerRootView>
    );
})
Example #9
Source File: useTiming.ts    From react-native-paper-onboarding with MIT License 4 votes vote down vote up
useTiming = ({
  animatedStaticIndex,
  animatedOverrideIndex,
  value,
  velocity,
  state,
  size,
  screenWidth,
}: useTimingProps) => {
  const clock = useClock();
  const isManuallyAnimated = useValue(0);

  const config = useMemo(
    () => ({
      toValue: new Animated.Value(0),
      duration: 500,
      easing: Easing.out(Easing.exp),
    }),
    []
  );

  const animationState = useMemo(
    () => ({
      finished: new Animated.Value(0),
      position: new Animated.Value(0),
      frameTime: new Animated.Value(0),
      time: new Animated.Value(0),
    }),
    []
  );

  const valueClamp = useMemo(
    () =>
      interpolate(value, {
        inputRange: [screenWidth * -1, 0, screenWidth],
        outputRange: I18nManager.isRTL ? [-1, 0, 1] : [1, 0, -1],
        extrapolate: Animated.Extrapolate.CLAMP,
      }),
    [value, screenWidth]
  );

  const velocityClamp = useMemo(
    () =>
      interpolate(velocity, {
        inputRange: [screenWidth * -2, 0, screenWidth * 2],
        outputRange: I18nManager.isRTL ? [-0.5, 0, 0.5] : [0.5, 0, -0.5],
        extrapolate: Animated.Extrapolate.CLAMP,
      }),
    [screenWidth, velocity]
  );

  const isTimingInterrupted = and(eq(state, State.BEGAN), clockRunning(clock));
  const finishTiming = useMemo(
    () => [
      set(animatedStaticIndex, config.toValue),
      set(animatedOverrideIndex, config.toValue),
      set(animationState.frameTime, 0),
      set(animationState.time, 0),
      set(state, State.UNDETERMINED),
      set(isManuallyAnimated, 0),
      stopClock(clock),
    ],
    [
      state,
      animatedOverrideIndex,
      animatedStaticIndex,
      animationState.frameTime,
      animationState.time,
      clock,
      config.toValue,
      isManuallyAnimated,
    ]
  );

  const shouldAnimate = useMemo(
    () =>
      and(
        not(and(eq(animatedStaticIndex, 0), lessThan(valueClamp, 0))),
        not(and(eq(animatedStaticIndex, size - 1), greaterThan(valueClamp, 0)))
      ),
    [animatedStaticIndex, size, valueClamp]
  );
  const shouldReset = useMemo(
    () => not(greaterThan(add(abs(valueClamp), abs(velocityClamp)), 0.5)),
    [valueClamp, velocityClamp]
  );
  const shouldAnimateNext = useMemo(
    () =>
      greaterThan(
        add(animationState.position, velocityClamp),
        animatedStaticIndex
      ),
    [animatedStaticIndex, animationState.position, velocityClamp]
  );

  const animatedPosition = useMemo(
    () =>
      block([
        cond(isTimingInterrupted, finishTiming),
        cond(
          eq(state, State.ACTIVE),
          cond(
            and(
              not(and(eq(animatedStaticIndex, 0), lessThan(valueClamp, 0))),
              not(
                and(
                  eq(animatedStaticIndex, size - 1),
                  greaterThan(valueClamp, 0)
                )
              )
            ),
            [
              set(animationState.finished, 0),
              set(
                animationState.position,
                add(animatedStaticIndex, valueClamp)
              ),
            ]
          )
        ),

        onChange(animatedOverrideIndex, [
          set(isManuallyAnimated, 1),
          set(animationState.finished, 0),
        ]),

        cond(or(eq(state, State.END), isManuallyAnimated), [
          cond(and(not(clockRunning(clock)), not(animationState.finished)), [
            cond(
              isManuallyAnimated,
              set(config.toValue, animatedOverrideIndex),
              cond(
                or(shouldReset, not(shouldAnimate)),
                set(config.toValue, animatedStaticIndex),
                cond(
                  shouldAnimateNext,
                  set(config.toValue, add(animatedStaticIndex, 1)),
                  set(config.toValue, sub(animatedStaticIndex, 1))
                )
              )
            ),
            set(animationState.finished, 0),
            set(animationState.frameTime, 0),
            set(animationState.time, 0),
            startClock(clock),
          ]),
          timing(clock, animationState, config),
          cond(animationState.finished, finishTiming),
        ]),

        animationState.position,
      ]),
    [
      size,
      state,
      animatedOverrideIndex,
      animatedStaticIndex,
      animationState,
      clock,
      config,
      finishTiming,
      isManuallyAnimated,
      isTimingInterrupted,
      shouldAnimate,
      shouldAnimateNext,
      shouldReset,
      valueClamp,
    ]
  );
  return animatedPosition;
}
Example #10
Source File: StickyItemFlatList.tsx    From react-native-sticky-item with MIT License 4 votes vote down vote up
StickyItemFlatList = forwardRef(
  <T extends {}>(props: StickyItemFlatListProps<T>, ref: Ref<FlatList<T>>) => {
    const {
      initialScrollIndex = 0,
      decelerationRate = DEFAULT_DECELERATION_RATE,
      itemWidth,
      itemHeight,
      separatorSize = DEFAULT_SEPARATOR_SIZE,
      borderRadius = DEFAULT_BORDER_RADIUS,
      stickyItemActiveOpacity = DEFAULT_STICKY_ITEM_ACTIVE_OPACITY,
      stickyItemWidth,
      stickyItemHeight,
      stickyItemBackgroundColors,
      stickyItemContent,
      onStickyItemPress,
      isRTL = DEFAULT_IS_RTL,
      ItemSeparatorComponent = Separator,
      ...rest
    } = props;

    // refs
    const flatListRef = useRef<FlatList<T>>(null);
    const tapRef = useRef<TapGestureHandler>(null);

    //#region variables
    const itemWidthWithSeparator = useMemo(
      () => itemWidth + separatorSize,
      [itemWidth, separatorSize]
    );
    const separatorProps = useMemo(
      () => ({
        size: separatorSize,
      }),
      [separatorSize]
    );
    //#endregion

    //#region styles
    const contentContainerStyle = useMemo(
      () => [
        rest.contentContainerStyle,
        {
          paddingLeft: itemWidth + separatorSize * 2,
          paddingRight: separatorSize,
        },
      ],
      [rest.contentContainerStyle, itemWidth, separatorSize]
    );
    //#endregion

    //#region methods
    const getHitSlop = useCallback(
      isMinimized => {
        const verticalPosition = isMinimized
          ? -((itemHeight - stickyItemHeight) / 2)
          : 0;
        const startPosition = isMinimized ? 0 : -separatorSize;
        const endPosition = isMinimized
          ? -(SCREEN_WIDTH - stickyItemWidth)
          : -(SCREEN_WIDTH - separatorSize - itemWidth);

        return {
          top: verticalPosition,
          right: isRTL ? startPosition : endPosition,
          left: isRTL ? endPosition : startPosition,
          bottom: verticalPosition,
        };
      },
      [
        itemWidth,
        itemHeight,
        stickyItemWidth,
        stickyItemHeight,
        separatorSize,
        isRTL,
      ]
    );
    const getItemLayout = useCallback(
      (_, index) => {
        return {
          length: itemWidthWithSeparator,
          // sticky item + previous items width
          offset: itemWidthWithSeparator + itemWidthWithSeparator * index,
          index,
        };
      },
      [itemWidthWithSeparator]
    );
    //#endregion

    //#region gesture
    const x = useValue(0);
    const tapState = useValue(State.UNDETERMINED);
    const tapGestures = useGestureHandler({ state: tapState });
    const onScroll = event([
      {
        nativeEvent: {
          contentOffset: {
            x,
          },
        },
      },
    ]);
    const onScrollEnd = event([
      {
        nativeEvent: {
          contentOffset: {
            x,
          },
        },
      },
    ]);
    //#endregion

    //#region effects
    //@ts-ignore
    useImperativeHandle(ref, () => flatListRef.current!.getNode());
    useCode(
      () =>
        cond(eq(tapState, State.END), [
          call([tapState], () => {
            if (onStickyItemPress) {
              onStickyItemPress();
            }
          }),
          set(tapState, State.UNDETERMINED),
        ]),
      [tapState]
    );
    useCode(
      () =>
        onChange(
          x,
          call([x], args => {
            if (tapRef.current) {
              const isMinimized = args[0] > 0;
              // @ts-ignore
              tapRef.current.setNativeProps({
                hitSlop: getHitSlop(isMinimized),
              });
            }
          })
        ),
      [
        x,
        itemWidth,
        itemHeight,
        stickyItemWidth,
        stickyItemWidth,
        separatorSize,
      ]
    );
    useEffect(() => {
      /**
       * @DEV
       * to fix stick item position with fast refresh
       */
      x.setValue(0);

      if (tapRef.current) {
        // @ts-ignore
        tapRef.current.setNativeProps({
          hitSlop: getHitSlop(initialScrollIndex !== 0),
        });
      }
      // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [getHitSlop]);
    //#endregion

    // render
    const renderSeparator = useCallback(() => {
      if (typeof ItemSeparatorComponent === 'function') {
        // @ts-ignore
        return ItemSeparatorComponent(separatorProps);
      } else {
        // @ts-ignore
        return <ItemSeparatorComponent size={separatorProps.size} />;
      }
    }, [ItemSeparatorComponent, separatorProps]);

    return (
      <TapGestureHandler
        ref={tapRef}
        waitFor={flatListRef}
        shouldCancelWhenOutside={true}
        {...tapGestures}
      >
        <Animated.View>
          <AnimatedFlatList
            {...rest}
            ref={flatListRef}
            initialScrollIndex={initialScrollIndex}
            inverted={isRTL}
            ItemSeparatorComponent={renderSeparator}
            contentContainerStyle={contentContainerStyle}
            horizontal={true}
            showsHorizontalScrollIndicator={false}
            scrollEventThrottle={1}
            pagingEnabled={true}
            decelerationRate={decelerationRate}
            snapToAlignment={'start'}
            snapToInterval={itemWidth + separatorSize}
            onScroll={onScroll}
            onScrollAnimationEnd={onScrollEnd}
            getItemLayout={getItemLayout}
          />
          <StickyItem
            x={x}
            tapState={tapState}
            itemWidth={itemWidth}
            itemHeight={itemHeight}
            separatorSize={separatorSize}
            borderRadius={borderRadius}
            stickyItemActiveOpacity={stickyItemActiveOpacity}
            stickyItemWidth={stickyItemWidth}
            stickyItemHeight={stickyItemHeight}
            stickyItemBackgroundColors={stickyItemBackgroundColors}
            stickyItemContent={stickyItemContent}
            isRTL={isRTL}
          />
        </Animated.View>
      </TapGestureHandler>
    );
  }
)
Example #11
Source File: useAnimatedGestureHandler.tsx    From react-native-gallery-toolkit with MIT License 4 votes vote down vote up
export function createAnimatedGestureHandler<
  T extends GestureHandlerGestureEvent,
  TContext extends Context,
>(handlers: GestureHandlers<T['nativeEvent'], TContext>) {
  const context = useRemoteContext<any>({
    __initialized: false,
  });
  const isAndroid = Platform.OS === 'android';

  const handler = useWorkletCallback((event: T['nativeEvent']) => {
    'worklet';

    if (handlers.onInit && !context.__initialized) {
      context.__initialized = true;
      handlers.onInit(event, context);
    }

    if (handlers.onGesture) {
      handlers.onGesture(event, context);
    }

    const stateDiff = diff(context, 'pinchState', event.state);

    const pinchBeganAndroid =
      stateDiff === State.ACTIVE - State.BEGAN
        ? event.state === State.ACTIVE
        : false;

    const isBegan = isAndroid
      ? pinchBeganAndroid
      : event.state === State.BEGAN;

    if (isBegan) {
      if (handlers.shouldHandleEvent) {
        context._shouldSkip = !handlers.shouldHandleEvent(
          event,
          context,
        );
      } else {
        context._shouldSkip = false;
      }
    } else if (typeof context._shouldSkip === 'undefined') {
      return;
    }

    if (!context._shouldSkip && !context._shouldCancel) {
      if (handlers.onEvent) {
        handlers.onEvent(event, context);
      }

      if (handlers.shouldCancel) {
        context._shouldCancel = handlers.shouldCancel(event, context);

        if (context._shouldCancel) {
          if (handlers.onEnd) handlers.onEnd(event, context, true);
          return;
        }
      }

      if (handlers.beforeEach) {
        handlers.beforeEach(event, context);
      }

      if (isBegan && handlers.onStart) {
        handlers.onStart(event, context);
      }

      if (event.state === State.ACTIVE && handlers.onActive) {
        handlers.onActive(event, context);
      }
      if (
        event.oldState === State.ACTIVE &&
        event.state === State.END &&
        handlers.onEnd
      ) {
        handlers.onEnd(event, context, false);
      }
      if (
        event.oldState === State.ACTIVE &&
        event.state === State.FAILED &&
        handlers.onFail
      ) {
        handlers.onFail(event, context);
      }
      if (
        event.oldState === State.ACTIVE &&
        event.state === State.CANCELLED &&
        handlers.onCancel
      ) {
        handlers.onCancel(event, context);
      }

      if (event.oldState === State.ACTIVE) {
        if (handlers.onFinish) {
          handlers.onFinish(
            event,
            context,
            event.state === State.CANCELLED ||
              event.state === State.FAILED,
          );
        }
      }

      if (handlers.afterEach) {
        handlers.afterEach(event, context);
      }
    }
    // clean up context
    if (event.oldState === State.ACTIVE) {
      context._shouldSkip = undefined;
      context._shouldCancel = undefined;
    }
  }, []);

  return handler;
}
Example #12
Source File: ImageTransformer.tsx    From react-native-gallery-toolkit with MIT License 4 votes vote down vote up
ImageTransformer = React.memo<ImageTransformerProps>(
  ({
    outerGestureHandlerRefs = [],
    source,
    width,
    height,
    onStateChange = workletNoop,
    renderImage,
    windowDimensions = Dimensions.get('window'),
    isActive,
    outerGestureHandlerActive,
    style,
    onTap = workletNoop,
    onDoubleTap = workletNoop,
    onInteraction = workletNoop,
    DOUBLE_TAP_SCALE = 3,
    MAX_SCALE = 3,
    MIN_SCALE = 0.7,
    OVER_SCALE = 0.5,
    timingConfig = defaultTimingConfig,
    springConfig = defaultSpringConfig,
    enabled = true,
    ImageComponent = Image,
  }) => {
    // fixGestureHandler();

    assertWorklet(onStateChange);
    assertWorklet(onTap);
    assertWorklet(onDoubleTap);
    assertWorklet(onInteraction);

    if (typeof source === 'undefined') {
      throw new Error(
        'ImageTransformer: either source or uri should be passed to display an image',
      );
    }

    const imageSource = useMemo(
      () =>
        typeof source === 'string'
          ? {
              uri: source,
            }
          : source,
      [source],
    );

    const interactionsEnabled = useSharedValue(false);
    const setInteractionsEnabled = useCallback((value: boolean) => {
      interactionsEnabled.value = value;
    }, []);
    const onLoadImageSuccess = useCallback(() => {
      setInteractionsEnabled(true);
    }, []);

    // HACK ALERT
    // we disable pinch handler in order to trigger onFinish
    // in case user releases one finger
    const [pinchEnabled, setPinchEnabledState] = useState(true);
    useEffect(() => {
      if (!pinchEnabled) {
        setPinchEnabledState(true);
      }
    }, [pinchEnabled]);
    const disablePinch = useCallback(() => {
      setPinchEnabledState(false);
    }, []);
    // HACK ALERT END

    const pinchRef = useRef(null);
    const panRef = useRef(null);
    const tapRef = useRef(null);
    const doubleTapRef = useRef(null);

    const panState = useSharedValue<State>(State.UNDETERMINED);
    const pinchState = useSharedValue<State>(State.UNDETERMINED);

    const scale = useSharedValue(1);
    const scaleOffset = useSharedValue(1);
    const translation = vectors.useSharedVector(0, 0);
    const panVelocity = vectors.useSharedVector(0, 0);
    const scaleTranslation = vectors.useSharedVector(0, 0);
    const offset = vectors.useSharedVector(0, 0);

    const canvas = useMemo(
      () =>
        vectors.create(
          windowDimensions.width,
          windowDimensions.height,
        ),
      [windowDimensions.width, windowDimensions.height],
    );
    const targetWidth = windowDimensions.width;
    const scaleFactor = width / targetWidth;
    const targetHeight = height / scaleFactor;
    const image = useMemo(
      () => vectors.create(targetWidth, targetHeight),
      [targetHeight, targetWidth],
    );

    const canPanVertically = useDerivedValue(() => {
      return windowDimensions.height < targetHeight * scale.value;
    }, []);

    const resetSharedState = useWorkletCallback(
      (animated?: boolean) => {
        'worklet';

        if (animated) {
          scale.value = withTiming(1, timingConfig);
          scaleOffset.value = 1;

          vectors.set(offset, () => withTiming(0, timingConfig));
        } else {
          scale.value = 1;
          scaleOffset.value = 1;
          vectors.set(translation, 0);
          vectors.set(scaleTranslation, 0);
          vectors.set(offset, 0);
        }
      },
      [timingConfig],
    );

    const maybeRunOnEnd = useWorkletCallback(() => {
      'worklet';

      const target = vectors.create(0, 0);

      const fixedScale = clamp(MIN_SCALE, scale.value, MAX_SCALE);
      const scaledImage = image.y * fixedScale;
      const rightBoundary = (canvas.x / 2) * (fixedScale - 1);

      let topBoundary = 0;

      if (canvas.y < scaledImage) {
        topBoundary = Math.abs(scaledImage - canvas.y) / 2;
      }

      const maxVector = vectors.create(rightBoundary, topBoundary);
      const minVector = vectors.invert(maxVector);

      if (!canPanVertically.value) {
        offset.y.value = withSpring(target.y, springConfig);
      }

      // we should handle this only if pan or pinch handlers has been used already
      if (
        (checkIsNotUsed(panState) || checkIsNotUsed(pinchState)) &&
        pinchState.value !== State.CANCELLED
      ) {
        return;
      }

      if (
        vectors.eq(offset, 0) &&
        vectors.eq(translation, 0) &&
        vectors.eq(scaleTranslation, 0) &&
        scale.value === 1
      ) {
        // we don't need to run any animations
        return;
      }

      if (scale.value <= 1) {
        // just center it
        vectors.set(offset, () => withTiming(0, timingConfig));
        return;
      }

      vectors.set(
        target,
        vectors.clamp(offset, minVector, maxVector),
      );

      const deceleration = 0.9915;

      const isInBoundaryX = target.x === offset.x.value;
      const isInBoundaryY = target.y === offset.y.value;

      if (isInBoundaryX) {
        if (
          Math.abs(panVelocity.x.value) > 0 &&
          scale.value <= MAX_SCALE
        ) {
          offset.x.value = withDecay({
            velocity: panVelocity.x.value,
            clamp: [minVector.x, maxVector.x],
            deceleration,
          });
        }
      } else {
        offset.x.value = withSpring(target.x, springConfig);
      }

      if (isInBoundaryY) {
        if (
          Math.abs(panVelocity.y.value) > 0 &&
          scale.value <= MAX_SCALE &&
          offset.y.value !== minVector.y &&
          offset.y.value !== maxVector.y
        ) {
          offset.y.value = withDecay({
            velocity: panVelocity.y.value,
            clamp: [minVector.y, maxVector.y],
            deceleration,
          });
        }
      } else {
        offset.y.value = withSpring(target.y, springConfig);
      }
    }, [
      MIN_SCALE,
      MAX_SCALE,
      image,
      canvas,
      springConfig,
      timingConfig,
    ]);

    const onPanEvent = useAnimatedGestureHandler<
      PanGestureHandlerGestureEvent,
      {
        panOffset: vectors.Vector<number>;
        pan: vectors.Vector<number>;
      }
    >({
      onInit: (_, ctx) => {
        ctx.panOffset = vectors.create(0, 0);
      },

      shouldHandleEvent: () => {
        return (
          scale.value > 1 &&
          (typeof outerGestureHandlerActive !== 'undefined'
            ? !outerGestureHandlerActive.value
            : true) &&
          interactionsEnabled.value
        );
      },

      beforeEach: (evt, ctx) => {
        ctx.pan = vectors.create(evt.translationX, evt.translationY);
        const velocity = vectors.create(evt.velocityX, evt.velocityY);

        vectors.set(panVelocity, velocity);
      },

      onStart: (_, ctx) => {
        cancelAnimation(offset.x);
        cancelAnimation(offset.y);
        ctx.panOffset = vectors.create(0, 0);
        onInteraction('pan');
      },

      onActive: (evt, ctx) => {
        panState.value = evt.state;

        if (scale.value > 1) {
          if (evt.numberOfPointers > 1) {
            // store pan offset during the pan with two fingers (during the pinch)
            vectors.set(ctx.panOffset, ctx.pan);
          } else {
            // subtract the offset and assign fixed pan
            const nextTranslate = vectors.add(
              ctx.pan,
              vectors.invert(ctx.panOffset),
            );
            translation.x.value = nextTranslate.x;

            if (canPanVertically.value) {
              translation.y.value = nextTranslate.y;
            }
          }
        }
      },

      onEnd: (evt, ctx) => {
        panState.value = evt.state;

        vectors.set(ctx.panOffset, 0);
        vectors.set(offset, vectors.add(offset, translation));
        vectors.set(translation, 0);

        maybeRunOnEnd();

        vectors.set(panVelocity, 0);
      },
    });

    useAnimatedReaction(
      () => {
        if (typeof isActive === 'undefined') {
          return true;
        }

        return isActive.value;
      },
      (currentActive) => {
        if (!currentActive) {
          resetSharedState();
        }
      },
    );

    const onScaleEvent = useAnimatedGestureHandler<
      PinchGestureHandlerGestureEvent,
      {
        origin: vectors.Vector<number>;
        adjustFocal: vectors.Vector<number>;
        gestureScale: number;
        nextScale: number;
      }
    >({
      onInit: (_, ctx) => {
        ctx.origin = vectors.create(0, 0);
        ctx.gestureScale = 1;
        ctx.adjustFocal = vectors.create(0, 0);
      },

      shouldHandleEvent: (evt) => {
        return (
          evt.numberOfPointers === 2 &&
          (typeof outerGestureHandlerActive !== 'undefined'
            ? !outerGestureHandlerActive.value
            : true) &&
          interactionsEnabled.value
        );
      },

      beforeEach: (evt, ctx) => {
        // calculate the overall scale value
        // also limits this.event.scale
        ctx.nextScale = clamp(
          evt.scale * scaleOffset.value,
          MIN_SCALE,
          MAX_SCALE + OVER_SCALE,
        );

        if (
          ctx.nextScale > MIN_SCALE &&
          ctx.nextScale < MAX_SCALE + OVER_SCALE
        ) {
          ctx.gestureScale = evt.scale;
        }

        // this is just to be able to use with vectors
        const focal = vectors.create(evt.focalX, evt.focalY);
        const CENTER = vectors.divide(canvas, 2);

        // since it works even when you release one finger
        if (evt.numberOfPointers === 2) {
          // focal with translate offset
          // it alow us to scale into different point even then we pan the image
          ctx.adjustFocal = vectors.sub(
            focal,
            vectors.add(CENTER, offset),
          );
        } else if (
          evt.state === State.ACTIVE &&
          evt.numberOfPointers !== 2
        ) {
          runOnJS(disablePinch)();
        }
      },

      afterEach: (evt, ctx) => {
        if (
          evt.state === State.END ||
          evt.state === State.CANCELLED
        ) {
          return;
        }

        scale.value = ctx.nextScale;
      },

      onStart: (_, ctx) => {
        onInteraction('scale');
        cancelAnimation(offset.x);
        cancelAnimation(offset.y);
        vectors.set(ctx.origin, ctx.adjustFocal);
      },

      onActive: (evt, ctx) => {
        pinchState.value = evt.state;

        const pinch = vectors.sub(ctx.adjustFocal, ctx.origin);

        const nextTranslation = vectors.add(
          pinch,
          ctx.origin,
          vectors.multiply(-1, ctx.gestureScale, ctx.origin),
        );

        vectors.set(scaleTranslation, nextTranslation);
      },

      onFinish: (evt, ctx) => {
        // reset gestureScale value
        ctx.gestureScale = 1;
        pinchState.value = evt.state;
        // store scale value
        scaleOffset.value = scale.value;

        vectors.set(offset, vectors.add(offset, scaleTranslation));
        vectors.set(scaleTranslation, 0);

        if (scaleOffset.value < 1) {
          // make sure we don't add stuff below the 1
          scaleOffset.value = 1;

          // this runs the timing animation
          scale.value = withTiming(1, timingConfig);
        } else if (scaleOffset.value > MAX_SCALE) {
          scaleOffset.value = MAX_SCALE;
          scale.value = withTiming(MAX_SCALE, timingConfig);
        }

        maybeRunOnEnd();
      },
    });

    const onTapEvent = useAnimatedGestureHandler({
      shouldHandleEvent: (evt) => {
        return (
          evt.numberOfPointers === 1 &&
          (typeof outerGestureHandlerActive !== 'undefined'
            ? !outerGestureHandlerActive.value
            : true) &&
          interactionsEnabled.value
        );
      },

      onStart: () => {
        cancelAnimation(offset.x);
        cancelAnimation(offset.y);
      },

      onActive: () => {
        onTap(scale.value > 1);
      },

      onEnd: () => {
        maybeRunOnEnd();
      },
    });

    const handleScaleTo = useWorkletCallback(
      (x: number, y: number) => {
        'worklet';

        scale.value = withTiming(DOUBLE_TAP_SCALE, timingConfig);
        scaleOffset.value = DOUBLE_TAP_SCALE;

        const targetImageSize = vectors.multiply(
          image,
          DOUBLE_TAP_SCALE,
        );

        const CENTER = vectors.divide(canvas, 2);
        const imageCenter = vectors.divide(image, 2);

        const focal = vectors.create(x, y);

        const origin = vectors.multiply(
          -1,
          vectors.sub(vectors.divide(targetImageSize, 2), CENTER),
        );

        const koef = vectors.sub(
          vectors.multiply(vectors.divide(1, imageCenter), focal),
          1,
        );

        const target = vectors.multiply(origin, koef);

        if (targetImageSize.y < canvas.y) {
          target.y = 0;
        }

        offset.x.value = withTiming(target.x, timingConfig);
        offset.y.value = withTiming(target.y, timingConfig);
      },
      [DOUBLE_TAP_SCALE, timingConfig, image, canvas],
    );

    const onDoubleTapEvent = useAnimatedGestureHandler<
      TapGestureHandlerGestureEvent,
      {}
    >({
      shouldHandleEvent: (evt) => {
        return (
          evt.numberOfPointers === 1 &&
          (typeof outerGestureHandlerActive !== 'undefined'
            ? !outerGestureHandlerActive.value
            : true) &&
          interactionsEnabled.value
        );
      },

      onActive: ({ x, y }) => {
        onDoubleTap(scale.value > 1);

        if (scale.value > 1) {
          resetSharedState(true);
        } else {
          handleScaleTo(x, y);
        }
      },
    });

    const animatedStyles = useAnimatedStyle<ViewStyle>(() => {
      const noOffset = offset.x.value === 0 && offset.y.value === 0;
      const noTranslation =
        translation.x.value === 0 && translation.y.value === 0;
      const noScaleTranslation =
        scaleTranslation.x.value === 0 &&
        scaleTranslation.y.value === 0;

      const isInactive =
        scale.value === 1 &&
        noOffset &&
        noTranslation &&
        noScaleTranslation;

      onStateChange(isInactive);

      return {
        transform: [
          {
            translateX:
              scaleTranslation.x.value +
              translation.x.value +
              offset.x.value,
          },
          {
            translateY:
              scaleTranslation.y.value +
              translation.y.value +
              offset.y.value,
          },
          { scale: scale.value },
        ],
      };
    }, []);

    return (
      <Animated.View
        style={[styles.container, { width: targetWidth }, style]}
      >
        <PinchGestureHandler
          enabled={enabled && pinchEnabled}
          ref={pinchRef}
          onGestureEvent={onScaleEvent}
          simultaneousHandlers={[
            panRef,
            tapRef,
            ...outerGestureHandlerRefs,
          ]}
        >
          <Animated.View style={styles.fill}>
            <PanGestureHandler
              enabled={enabled}
              ref={panRef}
              minDist={4}
              avgTouches
              simultaneousHandlers={[
                pinchRef,
                tapRef,
                ...outerGestureHandlerRefs,
              ]}
              onGestureEvent={onPanEvent}
            >
              <Animated.View style={styles.fill}>
                <TapGestureHandler
                  enabled={enabled}
                  ref={tapRef}
                  numberOfTaps={1}
                  maxDeltaX={8}
                  maxDeltaY={8}
                  simultaneousHandlers={[
                    pinchRef,
                    panRef,
                    ...outerGestureHandlerRefs,
                  ]}
                  waitFor={doubleTapRef}
                  onGestureEvent={onTapEvent}
                >
                  <Animated.View style={[styles.fill]}>
                    <Animated.View style={styles.fill}>
                      <Animated.View style={styles.wrapper}>
                        <TapGestureHandler
                          enabled={enabled}
                          ref={doubleTapRef}
                          numberOfTaps={2}
                          maxDelayMs={140}
                          maxDeltaX={16}
                          maxDeltaY={16}
                          simultaneousHandlers={[
                            pinchRef,
                            panRef,
                            ...outerGestureHandlerRefs,
                          ]}
                          onGestureEvent={onDoubleTapEvent}
                        >
                          <Animated.View style={animatedStyles}>
                            {typeof renderImage === 'function' ? (
                              renderImage({
                                source: imageSource,
                                width: targetWidth,
                                height: targetHeight,
                                onLoad: onLoadImageSuccess,
                              })
                            ) : (
                              <ImageComponent
                                onLoad={onLoadImageSuccess}
                                source={imageSource}
                                style={{
                                  width: targetWidth,
                                  height: targetHeight,
                                }}
                              />
                            )}
                          </Animated.View>
                        </TapGestureHandler>
                      </Animated.View>
                    </Animated.View>
                  </Animated.View>
                </TapGestureHandler>
              </Animated.View>
            </PanGestureHandler>
          </Animated.View>
        </PinchGestureHandler>
      </Animated.View>
    );
  },
)