react-native-gesture-handler#PanGestureHandler TypeScript Examples

The following examples show how to use react-native-gesture-handler#PanGestureHandler. 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: Slider.tsx    From reanimated-arc with MIT License 6 votes vote down vote up
render() {
    return (
      <View>
        <View style={styles.container}>
          <ReanimatedArcBase
            color="lightgrey"
            diameter={containerWidth}
            width={15}
            arcSweepAngle={150}
            lineCap="round"
            rotation={285}
            style={styles.absolute}
          />
          <Reanimated.View
            style={[styles.absolute, {opacity: this.barOpacity}]}>
            <ReanimatedArcBase
              color="yellow"
              diameter={containerWidth}
              width={15}
              arcSweepAngle={this.arcAngle}
              lineCap="round"
              rotation={285}
            />
          </Reanimated.View>
          <View style={[styles.absolute, {alignSelf: 'flex-start'}]}>
            <PanGestureHandler onGestureEvent={this._onPanGestureEvent}>
              <Reanimated.View
                style={{
                  width: containerWidth - 10,
                  height: 160,
                }}
              />
            </PanGestureHandler>
          </View>
        </View>
      </View>
    );
  }
Example #2
Source File: AlphabetScroller.tsx    From jellyfin-audio-player with MIT License 6 votes vote down vote up
AlphabetScroller: React.FC<Props> = ({ onSelect }) => {
    const [ height, setHeight ] = useState(0);
    const [ index, setIndex ] = useState<number>();

    // Handler for setting the correct height for a single alphabet item
    const handleLayout = useCallback((event: LayoutChangeEvent) => {
        setHeight(event.nativeEvent.layout.height);
    }, []);

    // Handler for passing on a new index when it is tapped or swiped
    const handleGestureEvent = useCallback((event: PanGestureHandlerGestureEvent | TapGestureHandlerGestureEvent) => {
        const newIndex = Math.floor(event.nativeEvent.y / height);

        if (newIndex !== index) {
            setIndex(newIndex);
            onSelect(newIndex);
        }
    }, [height, index, onSelect]);

    return (
        <Container>
            <TapGestureHandler onHandlerStateChange={handleGestureEvent}>
                <PanGestureHandler onGestureEvent={handleGestureEvent}>
                    <View>
                        {ALPHABET_LETTERS.split('').map((l, i) => (
                            <View
                                key={l}
                                onLayout={i === 0 ? handleLayout : undefined}
                            >
                                <Letter>{l}</Letter>
                            </View>
                        ))}
                    </View>
                </PanGestureHandler>              
            </TapGestureHandler>
        </Container>
    );
}
Example #3
Source File: index.tsx    From react-native-scroll-bottom-sheet with MIT License 5 votes vote down vote up
private drawerHandleRef = React.createRef<PanGestureHandler>();
Example #4
Source File: index.tsx    From react-native-scroll-bottom-sheet with MIT License 5 votes vote down vote up
private drawerContentRef = React.createRef<PanGestureHandler>();
Example #5
Source File: Notifier.tsx    From react-native-notifier with MIT License 5 votes vote down vote up
render() {
    const {
      title,
      description,
      swipeEnabled,
      Component,
      componentProps,
      translucentStatusBar,
      containerStyle,
      containerProps,
    } = this.state;

    const additionalContainerStyle =
      typeof containerStyle === 'function' ? containerStyle(this.translateY) : containerStyle;

    return (
      <PanGestureHandler
        enabled={swipeEnabled}
        onGestureEvent={this.onGestureEvent}
        onHandlerStateChange={this.onHandlerStateChange}
      >
        <Animated.View
          {...containerProps}
          style={[
            styles.container,
            {
              transform: [{ translateY: this.translateYInterpolated }],
            },
            additionalContainerStyle,
          ]}
        >
          <TouchableWithoutFeedback onPress={this.onPress}>
            <View
              onLayout={this.onLayout}
              style={
                Platform.OS === 'android' && translucentStatusBar
                  ? styles.translucentStatusBarPadding
                  : undefined
              }
            >
              <Component title={title} description={description} {...componentProps} />
            </View>
          </TouchableWithoutFeedback>
        </Animated.View>
      </PanGestureHandler>
    );
  }
Example #6
Source File: Picker.tsx    From swiftui-react-native with MIT License 4 votes vote down vote up
Picker = ({
  children,
  selection,
  backgroundColor,
  opacity,
  frame,
  cornerRadius,
  scaleEffect,
  rotationEffect,
  padding,
  border,
  shadow,
  zIndex,
  style,
  alert,
  onChange,
  onAppear,
  onDisappear,
}: PickerProps) => {
  useAlert(alert);
  useLifecycle(onAppear, onDisappear);
  const colorScheme = useColorScheme();
  const childCount = Children.count(children);
  const [optionDimensions, setOptionDimensions] = useState(null);
  const tempSelection = useSharedValue(selection.value);
  const width = optionDimensions ? optionDimensions.width : 0;
  const slidePosition = useSharedValue(
    selectedToPosition(selection.value, width, childCount)
  );

  const sliderHeight = optionDimensions ? optionDimensions.height - 5 : 0;
  const sliderWidth = optionDimensions
    ? optionDimensions.width / childCount
    : 0;

  const animatedSliderStyle = useAnimatedStyle(() => {
    return {
      transform: [{ translateX: slidePosition.value }],
    };
  }, [slidePosition.value]);

  const gestureHandler = useAnimatedGestureHandler<
    PanGestureHandlerGestureEvent,
    GestureHandlerContext
  >({
    onStart: (_, ctx) => {
      ctx.offsetX = slidePosition.value;
    },
    onActive: (e, ctx) => {
      const currentXPos = e.translationX + ctx.offsetX;
      const optionWidth = width / childCount;
      const slideTo = clamp(
        Math.round((currentXPos - optionWidth / 2) / optionWidth) * optionWidth,
        0,
        (childCount - 1) * optionWidth
      );
      if (slideTo !== slidePosition.value) {
        slidePosition.value = withTiming(slideTo);
        tempSelection.value = positionToSelected(slideTo, width, childCount);
        console.log(tempSelection.value);
      }
    },
    onEnd: () => {
      const newValue = positionToSelected(
        slidePosition.value,
        width,
        childCount
      );

      runOnJS(selection.setValue)(newValue);
      if (onChange) {
        runOnJS(onChange)(newValue);
      }
    },
  });

  return (
    <Fragment>
      <View
        onLayout={(e) => setOptionDimensions(e.nativeEvent.layout)}
        style={[
          styles.container,
          {
            opacity,
            zIndex,
            backgroundColor: getColor(
              backgroundColor,
              colorScheme,
              'secondarySystemBackground'
            ),
            ...getCornerRadius(cornerRadius),
            ...getShadow(shadow),
            ...getPadding(padding),
            ...getFrame(frame),
            ...getBorder(border),
            ...getTransform(scaleEffect, rotationEffect),
          },
          style,
        ]}
      >
        <View style={styles.options}>
          {React.Children.map(children as ReactElement<any>, (child, i) => {
            const textChild =
              child?.type === Text
                ? cloneElement(child, { fontSize: 12, ...child.props })
                : child;
            return (
              <Fragment key={i}>
                <TouchableOpacity
                  style={[styles.option, { flexBasis: `${100 / childCount}%` }]}
                  disabled={selection.value === i}
                  onPress={() => {
                    slidePosition.value = withTiming(
                      selectedToPosition(i, width, childCount)
                    );
                    tempSelection.value = i;
                    runOnJS(selection.setValue)(i);
                    if (onChange) {
                      runOnJS(onChange)(i);
                    }
                  }}
                >
                  {textChild}
                </TouchableOpacity>
                <Divider
                  color={getColor('systemGray4', colorScheme)}
                  index={i}
                  selection={tempSelection.value}
                  childCount={childCount}
                />
              </Fragment>
            );
          })}
        </View>
        <Animated.View
          style={[
            styles.slider,
            animatedSliderStyle,
            {
              width: sliderWidth,
              height: sliderHeight,
              backgroundColor:
                colorScheme === 'dark'
                  ? getColor('secondarySystemBackground', 'dark')
                  : getColor('systemBackground', 'light'),
            },
          ]}
        />
      </View>

      <PanGestureHandler onGestureEvent={gestureHandler}>
        <Animated.View
          style={[
            styles.slider,
            animatedSliderStyle,
            {
              width: sliderWidth,
              height: sliderHeight,
              zIndex: 0,
              left: 10,
              top: 12,
            },
          ]}
        />
      </PanGestureHandler>
    </Fragment>
  );
}
Example #7
Source File: Slider.tsx    From swiftui-react-native with MIT License 4 votes vote down vote up
Slider: React.FC<SliderProps> = ({
  tint,
  trackTint,
  thumbTint,
  range = [0, 10],
  step = 1,
  value,
  updateOnSlide = true,
  frame,
  backgroundColor,
  style,
  padding,
  cornerRadius,
  rotationEffect,
  scaleEffect,
  shadow,
  border,
  opacity,
  zIndex,
  alert,
  onAppear,
  onDisappear,
  onChange,
}) => {
  useAlert(alert);
  useLifecycle(onAppear, onDisappear);
  const colorScheme = useColorScheme();
  const [sliderWidth, sliderHeight] = getSliderWidth(frame);
  const [from, through] = range;
  const midPoint = (through + from) / 2;
  const slope = (midPoint - from) / (sliderWidth / 2);

  const translateX = useSharedValue(
    value2Position(value.value, midPoint, slope)
  );

  useEffect(() => {
    const newPos = value2Position(value.value, midPoint, slope);
    translateX.value = withTiming(newPos);
  }, [value.value]);

  const animatedCursorStyle = useAnimatedStyle(() => {
    return {
      transform: [
        {
          translateX: translateX.value,
        },
      ],
    };
  });

  const animatedFillStyle = useAnimatedStyle(() => {
    return {
      width: translateX.value + sliderWidth / 2,
    };
  });

  const gestureHandler = useAnimatedGestureHandler<
    PanGestureHandlerGestureEvent,
    GestureHandlerContext
  >({
    onStart: (_, ctx) => {
      ctx.offsetX = translateX.value;
    },
    onActive: (e, ctx) => {
      const prevPos = translateX.value;
      const newPos = e.translationX + ctx.offsetX;
      if (newPos < sliderWidth / 2 && newPos > -sliderWidth / 2) {
        translateX.value = newPos;
        const prevVal = position2Value(prevPos, midPoint, slope, step);
        const newVal = position2Value(newPos, midPoint, slope, step);
        if (updateOnSlide && prevVal !== newVal) {
          runOnJS(value.setValue)(newVal);
          if (onChange) runOnJS(onChange)(newVal);
        }
      }
    },
    onEnd: () => {
      if (!updateOnSlide) {
        const newVal = position2Value(translateX.value, midPoint, slope, step);
        runOnJS(value.setValue)(newVal);
        if (onChange) runOnJS(onChange)(newVal);
      }
    },
  });

  return (
    <View
      style={[
        {
          opacity,
          zIndex,
          backgroundColor: getColor(backgroundColor, colorScheme),
          ...getCornerRadius(cornerRadius),
          ...getPadding(padding),
          ...getBorder(border),
          ...getShadow(shadow),
          ...getTransform(scaleEffect, rotationEffect),
        },
        style,
      ]}
    >
      <View
        style={[
          styles.slider,
          {
            width: sliderWidth,
            height: sliderHeight,
            marginTop: CIRCLE_WIDTH / 2,
            marginBottom: CIRCLE_WIDTH / 2,
            backgroundColor: getColor(trackTint, colorScheme, 'systemGray4'),
          },
        ]}
      >
        <Animated.View
          style={[
            {
              height: sliderHeight,
              borderRadius: 10,
              backgroundColor: getColor(tint, colorScheme, 'systemBlue'),
            },
            animatedFillStyle,
          ]}
        />
        <PanGestureHandler onGestureEvent={gestureHandler}>
          <Animated.View
            style={[
              styles.cursor,
              {
                left: sliderWidth / 2 - CIRCLE_WIDTH / 2,
                top: -CIRCLE_WIDTH / 2,
                height: CIRCLE_WIDTH,
                width: CIRCLE_WIDTH,
                backgroundColor: getColor(thumbTint, colorScheme, '#fff'),
              },
              animatedCursorStyle,
            ]}
          />
        </PanGestureHandler>
      </View>
    </View>
  );
}
Example #8
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 #9
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 #10
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 #11
Source File: PaperOnboarding.tsx    From react-native-paper-onboarding with MIT License 4 votes vote down vote up
PaperOnboardingComponent = forwardRef<
  PaperOnboarding,
  PaperOnboardingProps
>(
  (
    {
      data,
      safeInsets: _safeInsets,
      direction = DEFAULT_DIRECTION,
      // indicator config
      indicatorSize = DEFAULT_INDICATOR_SIZE,
      indicatorBackgroundColor = DEFAULT_INDICATOR_BACKGROUND_COLOR,
      indicatorBorderColor = DEFAULT_INDICATOR_BORDER_COLOR,
      // override styles
      titleStyle,
      descriptionStyle,
      // close button config
      closeButton,
      closeButtonTextStyle,
      closeButtonText = DEFAULT_CLOSE_BUTTON_TEXT,
      onCloseButtonPress = DEFAULT_CLOSE_BUTTON_CALLBACK,
      onIndexChange,
    },
    ref
  ) => {
    // state
    const [dimensions, setDimensions] =
      useState<PaperOnboardingScreenDimensions>({
        width: Dimensions.get('window').width,
        height: Dimensions.get('window').height,
      });

    // refs
    const indexRef = useRef<number>(0);
    const pagesRef = useRef<Array<Animated.View | null>>(data.map(() => null));

    //#region variables
    const safeInsets = useMemo<Required<Insets>>(() => {
      return {
        top: _safeInsets?.top ?? DEFAULT_SAFE_INSET,
        bottom: _safeInsets?.bottom ?? DEFAULT_SAFE_INSET,
        left: _safeInsets?.left ?? DEFAULT_SAFE_INSET,
        right: _safeInsets?.right ?? DEFAULT_SAFE_INSET,
      };
    }, [_safeInsets]);
    const indicatorsContainerLeftPadding = useMemo(() => {
      const containerLeftPadding = dimensions.width / 2 - indicatorSize / 2;
      return I18nManager.isRTL
        ? -containerLeftPadding + indicatorSize * (data.length - 1)
        : containerLeftPadding;
    }, [dimensions.width, indicatorSize, data.length]);
    //#endregion

    //#region animated variables
    const { gestureHandler, state, translation, velocity } =
      usePanGestureHandler();
    const animatedStaticIndex = useValue<number>(0);
    const animatedOverrideIndex = useValue<number>(0);
    const animatedIndex = useTiming({
      animatedStaticIndex,
      animatedOverrideIndex,
      value: direction === 'horizontal' ? translation.x : translation.y,
      velocity: direction === 'horizontal' ? velocity.x : velocity.y,
      state: state,
      size: data.length,
      screenWidth: dimensions.width,
    });
    const indicatorsContainerPosition = useMemo(
      () => data.map((_, index) => index * indicatorSize * -1),
      [data, indicatorSize]
    );
    const animatedIndicatorsContainerPosition = useMemo(
      () =>
        add(
          interpolate(animatedIndex, {
            inputRange: data.map((_, index) => index),
            outputRange: I18nManager.isRTL
              ? indicatorsContainerPosition.reverse()
              : indicatorsContainerPosition,
            extrapolate: Animated.Extrapolate.CLAMP,
          }),
          indicatorsContainerLeftPadding
        ),
      [
        data,
        animatedIndex,
        indicatorsContainerLeftPadding,
        indicatorsContainerPosition,
      ]
    );
    //#endregion

    //#region callbacks
    const handlePageRef = useCallback((pageRef, index) => {
      pagesRef.current[index] = pageRef;
    }, []);

    const handleOnLayout = useCallback(
      ({
        nativeEvent: {
          layout: { width, height },
        },
      }: LayoutChangeEvent) => {
        setDimensions({
          width,
          height,
        });
      },
      []
    );
    //#endregion

    //#region public methods
    const handleNavigateToNextPage = useCallback(() => {
      const currentIndex = indexRef.current;
      if (currentIndex === data.length - 1) {
        return;
      }
      animatedOverrideIndex.setValue(currentIndex + 1);
    }, [data, animatedOverrideIndex]);
    const handleNavigateToPreviousPage = useCallback(() => {
      const currentIndex = indexRef.current;
      if (currentIndex === 0) {
        return;
      }
      animatedOverrideIndex.setValue(currentIndex - 1);
    }, [animatedOverrideIndex]);
    useImperativeHandle(
      ref,
      () => ({
        next: handleNavigateToNextPage,
        previous: handleNavigateToPreviousPage,
      }),
      [handleNavigateToNextPage, handleNavigateToPreviousPage]
    );
    //#endregion

    //#region effects
    useCode(
      () =>
        onChange(
          animatedStaticIndex,
          call([animatedStaticIndex], args => {
            indexRef.current = args[0];
            /**
             * @DEV
             * here we directly manipulate pages native props by setting `pointerEvents`
             * to `auto` for current page and `none` for others.
             */
            pagesRef.current.map((pageRef, _index) => {
              // @ts-ignore
              pageRef.setNativeProps({
                pointerEvents: _index === args[0] ? 'auto' : 'none',
              });
            });

            if (onIndexChange) {
              onIndexChange(args[0]);
            }
          })
        ),
      []
    );
    //#endregion

    // renders
    return (
      <PanGestureHandler {...gestureHandler}>
        <Animated.View onLayout={handleOnLayout} style={styles.container}>
          <Background
            animatedIndex={animatedIndex}
            data={data}
            safeInsets={safeInsets}
            screenDimensions={dimensions}
            indicatorSize={indicatorSize}
            animatedIndicatorsContainerPosition={
              animatedIndicatorsContainerPosition
            }
          />

          {data.map((item, index) => (
            <Page
              key={`page-${index}`}
              index={index}
              item={item}
              animatedIndex={animatedIndex}
              indicatorSize={indicatorSize}
              titleStyle={titleStyle}
              descriptionStyle={descriptionStyle}
              safeInsets={safeInsets}
              screenDimensions={dimensions}
              handleRef={handlePageRef}
            />
          ))}

          <IndicatorsContainer
            data={data}
            animatedIndex={animatedIndex}
            animatedIndicatorsContainerPosition={
              animatedIndicatorsContainerPosition
            }
            indicatorSize={indicatorSize}
            indicatorBackgroundColor={indicatorBackgroundColor}
            indicatorBorderColor={indicatorBorderColor}
            safeInsets={safeInsets}
          />

          <CloseButton
            data={data}
            animatedIndex={animatedIndex}
            safeInsets={safeInsets}
            closeButtonText={closeButtonText}
            closeButtonTextStyle={closeButtonTextStyle}
            closeButton={closeButton}
            onCloseButtonPress={onCloseButtonPress}
          />
        </Animated.View>
      </PanGestureHandler>
    );
  }
)
Example #12
Source File: index.tsx    From lets-fork-native with MIT License 4 votes vote down vote up
render(): React.ReactElement {
    const { onGestureEvent, translateX, translateY } = this
    const { restaurants } = this.props
    const { loading } = this.state
    const [lastRestaurant, ...rest] = restaurants

    const leftOpacity = interpolate(translateX, {
      inputRange: [0, width / 3],
      outputRange: [0, 1],
    })

    const rightOpacity = interpolate(translateX, {
      inputRange: [-width / 3, 0],
      outputRange: [1, 0],
    })

    // Workaround to prevent text from flashing on ios.
    // There is a moment where the image is fetched and isn't rendered.
    // The ios workaround on Party Screen renders the image, but this causes
    // the text from both the top and bottom cards to flash over top. This workaround
    // turns the opacity of the bottom text to 0 when the top card hasn't moved.
    // This makes it look like the card is rendered correctly the entire time.
    // [TODO] look into using react-native-fast-image for image caching; requires ejecting
    const textOpacity = cond(
      greaterThan(translateX, new Value(0)),
      interpolate(translateX, {
        inputRange: [0, 0.00001],
        outputRange: [0, 1],
      }),
      interpolate(translateX, {
        inputRange: [-0.000001, 0],
        outputRange: [1, 0],
      }),
    )

    const rotateZ = concat(
      interpolate(translateX, {
        inputRange: [-width / 2, width / 2],
        outputRange: [15, -15],
        extrapolate: Extrapolate.CLAMP,
      }),
      'deg',
    )

    const style = {
      ...StyleSheet.absoluteFillObject,
      zIndex: 900,
      transform: [
        { translateX },
        { translateY },
        { rotateZ },
      ],
    }

    return (
      <View style={styles.cards}>
        {
          rest.length
            ? (
              <Card
                textOpacity={textOpacity}
                rightOpacity={0}
                leftOpacity={0}
                restaurant={rest[0]}
              />
            ) : null
        }
        {
          lastRestaurant && (
            <PanGestureHandler
              onHandlerStateChange={onGestureEvent}
              onGestureEvent={onGestureEvent}
            >
              <Animated.View style={style}>
                <Card
                  textOpacity={1}
                  rightOpacity={rightOpacity}
                  leftOpacity={leftOpacity}
                  loading={loading}
                  restaurant={lastRestaurant}
                />
              </Animated.View>
            </PanGestureHandler>
          )
        }
      </View>
    )
  }
Example #13
Source File: index.tsx    From react-native-scroll-bottom-sheet with MIT License 4 votes vote down vote up
render() {
    const {
      renderHandle,
      snapPoints,
      initialSnapIndex,
      componentType,
      onSettle,
      animatedPosition,
      containerStyle,
      ...rest
    } = this.props;
    const AnimatedScrollableComponent = this.scrollComponent;
    const normalisedSnapPoints = this.getNormalisedSnapPoints();
    const initialSnap = normalisedSnapPoints[initialSnapIndex];

    const Content = (
      <Animated.View
        style={[
          StyleSheet.absoluteFillObject,
          containerStyle,
          // @ts-ignore
          {
            transform: [{ translateY: this.translateY }],
          },
        ]}
      >
        <PanGestureHandler
          ref={this.drawerHandleRef}
          shouldCancelWhenOutside={false}
          simultaneousHandlers={this.masterDrawer}
          onGestureEvent={this.onHandleGestureEvent}
          onHandlerStateChange={this.onHandleGestureEvent}
        >
          <Animated.View>{renderHandle()}</Animated.View>
        </PanGestureHandler>
        <PanGestureHandler
          ref={this.drawerContentRef}
          simultaneousHandlers={[this.scrollComponentRef, this.masterDrawer]}
          shouldCancelWhenOutside={false}
          onGestureEvent={this.onDrawerGestureEvent}
          onHandlerStateChange={this.onDrawerGestureEvent}
        >
          <Animated.View style={styles.container}>
            <NativeViewGestureHandler
              ref={this.scrollComponentRef}
              waitFor={this.masterDrawer}
              simultaneousHandlers={this.drawerContentRef}
            >
              <AnimatedScrollableComponent
                overScrollMode="never"
                bounces={false}
                {...rest}
                ref={this.props.innerRef}
                // @ts-ignore
                decelerationRate={this.decelerationRate}
                onScrollBeginDrag={this.onScrollBeginDrag}
                scrollEventThrottle={1}
                contentContainerStyle={[
                  rest.contentContainerStyle,
                  { paddingBottom: this.getNormalisedSnapPoints()[0] },
                ]}
              />
            </NativeViewGestureHandler>
          </Animated.View>
        </PanGestureHandler>
        {this.props.animatedPosition && (
          <Animated.Code
            exec={onChange(
              this.position,
              set(this.props.animatedPosition, this.position)
            )}
          />
        )}
        <Animated.Code
          exec={onChange(
            this.dragY,
            cond(not(eq(this.dragY, 0)), set(this.prevDragY, this.dragY))
          )}
        />
        <Animated.Code
          exec={onChange(
            this.didGestureFinish,
            cond(this.didGestureFinish, [
              this.didScrollUpAndPullDown,
              this.setTranslationY,
              set(
                this.tempDestSnapPoint,
                add(normalisedSnapPoints[0], this.extraOffset)
              ),
              set(this.nextSnapIndex, 0),
              set(this.destSnapPoint, this.calculateNextSnapPoint()),
              cond(
                and(
                  greaterThan(this.dragY, this.lastStartScrollY),
                  this.isAndroid,
                  not(this.dragWithHandle)
                ),
                call([], () => {
                  // This prevents the scroll glide from happening on Android when pulling down with inertia.
                  // It's not perfect, but does the job for now
                  const { method, args } = imperativeScrollOptions[
                    this.props.componentType
                  ];
                  // @ts-ignore
                  const node = this.props.innerRef.current?.getNode();

                  if (
                    node &&
                    node[method] &&
                    ((this.props.componentType === 'FlatList' &&
                      (this.props?.data?.length || 0) > 0) ||
                      (this.props.componentType === 'SectionList' &&
                        this.props.sections.length > 0) ||
                      this.props.componentType === 'ScrollView')
                  ) {
                    node[method](args);
                  }
                })
              ),
              set(this.dragY, 0),
              set(this.velocityY, 0),
              set(
                this.lastSnap,
                sub(
                  this.destSnapPoint,
                  cond(
                    eq(this.scrollUpAndPullDown, 1),
                    this.lastStartScrollY,
                    0
                  )
                )
              ),
              call([this.lastSnap], ([value]) => {
                // This is the TapGHandler trick
                // @ts-ignore
                this.masterDrawer?.current?.setNativeProps({
                  maxDeltaY: value - this.getNormalisedSnapPoints()[0],
                });
              }),
              set(
                this.decelerationRate,
                cond(
                  eq(this.isAndroid, 1),
                  cond(
                    eq(this.lastSnap, normalisedSnapPoints[0]),
                    ANDROID_NORMAL_DECELERATION_RATE,
                    0
                  ),
                  IOS_NORMAL_DECELERATION_RATE
                )
              ),
            ])
          )}
        />
        <Animated.Code
          exec={onChange(this.isManuallySetValue, [
            cond(
              this.isManuallySetValue,
              [
                set(this.destSnapPoint, this.manualYOffset),
                set(this.animationFinished, 0),
                set(this.lastSnap, this.manualYOffset),
                call([this.lastSnap], ([value]) => {
                  // This is the TapGHandler trick
                  // @ts-ignore
                  this.masterDrawer?.current?.setNativeProps({
                    maxDeltaY: value - this.getNormalisedSnapPoints()[0],
                  });
                }),
              ],
              [set(this.nextSnapIndex, 0)]
            ),
          ])}
        />
      </Animated.View>
    );

    // On Android, having an intermediary view with pointerEvents="box-none", breaks the
    // waitFor logic
    if (Platform.OS === 'android') {
      return (
        <TapGestureHandler
          maxDurationMs={100000}
          ref={this.masterDrawer}
          maxDeltaY={initialSnap - this.getNormalisedSnapPoints()[0]}
          shouldCancelWhenOutside={false}
        >
          {Content}
        </TapGestureHandler>
      );
    }

    // On iOS, We need to wrap the content on a view with PointerEvents box-none
    // So that we can start scrolling automatically when reaching the top without
    // Stopping the gesture
    return (
      <TapGestureHandler
        maxDurationMs={100000}
        ref={this.masterDrawer}
        maxDeltaY={initialSnap - this.getNormalisedSnapPoints()[0]}
      >
        <View style={StyleSheet.absoluteFillObject} pointerEvents="box-none">
          {Content}
        </View>
      </TapGestureHandler>
    );
  }
Example #14
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>
    );
  },
)
Example #15
Source File: Pager.tsx    From react-native-gallery-toolkit with MIT License 4 votes vote down vote up
Pager = typedMemo(function Pager<
  TPages,
  ItemT = UnpackItemT<TPages>,
>({
  pages,
  initialIndex,
  totalCount,
  numToRender = 2,
  onIndexChange = workletNoop,
  renderPage,
  width = dimensions.width,
  gutterWidth = GUTTER_WIDTH,
  shouldRenderGutter = true,
  keyExtractor,
  pagerWrapperStyles = {},
  getItem,
  springConfig,
  onPagerTranslateChange = workletNoop,
  onGesture = workletNoop,
  onEnabledGesture = workletNoop,
  shouldHandleGestureEvent = workletNoopTrue,
  initialDiffValue = 0,
  shouldUseInteractionManager = true,
  outerGestureHandlerRefs = [],
  verticallyEnabled = true,
}: PagerProps<TPages, ItemT>) {
  assertWorklet(onIndexChange);
  assertWorklet(onPagerTranslateChange);
  assertWorklet(onGesture);
  assertWorklet(onEnabledGesture);
  assertWorklet(shouldHandleGestureEvent);

  // make sure to not calculate translate with gutter
  // if we don't want to render it
  if (!shouldRenderGutter) {
    gutterWidth = 0;
  }

  const getPageTranslate = useWorkletCallback(
    (i: number) => {
      const t = i * width;
      const g = gutterWidth * i;
      return -(t + g);
    },
    [gutterWidth, width],
  );

  const pagerRef = useRef(null);
  const tapRef = useRef(null);

  const isActive = useSharedValue(true);

  const onPageStateChange = useWorkletCallback((value: boolean) => {
    isActive.value = value;
  }, []);

  const velocity = useSharedValue(0);
  const isMounted = useRef(false);

  const [diffValue, setDiffValue] = useState(initialDiffValue);
  useEffect(() => {
    if (shouldUseInteractionManager && !isMounted.current) {
      InteractionManager.runAfterInteractions(() => {
        setDiffValue(numToRender);
        console.log('numToRender in interaction');
      });
      isMounted.current = true;
    } else {
      setDiffValue(numToRender);
    }
  }, [numToRender]);

  // S2: Pager related stuff
  const [activeIndex, setActiveIndex] = useState(initialIndex);

  const index = useSharedValue(initialIndex);
  const length = useSharedValue(totalCount);
  const pagerX = useSharedValue(0);
  const toValueAnimation = useSharedValue(
    getPageTranslate(initialIndex),
  );

  const offsetX = useSharedValue(getPageTranslate(initialIndex));

  const totalWidth = useDerivedValue(() => {
    return length.value * width + gutterWidth * length.value - 2;
  }, []);

  const onIndexChangeCb = useWorkletCallback((nextIndex: number) => {
    onIndexChange(nextIndex);

    runOnJS(setActiveIndex)(nextIndex);
  }, []);

  const onIndexChangeWorklet = useWorkletCallback((i: number) => {
    offsetX.value = getPageTranslate(i);
    index.value = i;
    onIndexChangeCb(i);
  }, []);

  useEffect(() => {
    runOnUI(onIndexChangeWorklet)(initialIndex);
  }, [initialIndex]);

  const getSpringConfig = useWorkletCallback(
    (noVelocity?: boolean) => {
      const ratio = 1.1;
      const mass = 0.4;
      const stiffness = IS_ANDROID ? 200.0 : 100.0;
      const damping = ratio * 2.0 * Math.sqrt(mass * stiffness);

      const configToUse =
        typeof springConfig !== 'undefined'
          ? springConfig
          : {
              stiffness,
              mass,
              damping,
              restDisplacementThreshold: 1,
              restSpeedThreshold: 5,
            };

      // @ts-ignore
      // cannot use merge and spread here :(
      configToUse.velocity = noVelocity ? 0 : velocity.value;

      return configToUse;
    },
    [springConfig],
  );

  const onChangePageAnimation = useWorkletCallback(
    (noVelocity?: boolean) => {
      const config = getSpringConfig(noVelocity);

      if (offsetX.value === toValueAnimation.value) {
        return;
      }

      offsetX.value = withSpring(
        toValueAnimation.value,
        config,
        (isCanceled) => {
          if (!isCanceled) {
            velocity.value = 0;
          }
        },
      );
    },
    [getSpringConfig],
  );

  // S3 Pager
  const getCanSwipe = useWorkletCallback(
    (currentTranslate: number = 0) => {
      const nextTranslate = offsetX.value + currentTranslate;

      if (nextTranslate > 0) {
        return false;
      }

      const totalTranslate =
        width * (length.value - 1) + gutterWidth * (length.value - 1);

      if (Math.abs(nextTranslate) >= totalTranslate) {
        return false;
      }

      return true;
    },
    [width, gutterWidth],
  );

  const getNextIndex = useWorkletCallback(
    (v: number) => {
      const currentTranslate = Math.abs(
        getPageTranslate(index.value),
      );
      const currentIndex = index.value;
      const currentOffset = Math.abs(offsetX.value);

      const nextIndex = v < 0 ? currentIndex + 1 : currentIndex - 1;

      if (
        nextIndex < currentIndex &&
        currentOffset > currentTranslate
      ) {
        return currentIndex;
      }

      if (
        nextIndex > currentIndex &&
        currentOffset < currentTranslate
      ) {
        return currentIndex;
      }

      if (nextIndex > length.value - 1 || nextIndex < 0) {
        return currentIndex;
      }

      return nextIndex;
    },
    [getPageTranslate],
  );

  const isPagerInProgress = useDerivedValue(() => {
    return (
      Math.floor(Math.abs(getPageTranslate(index.value))) !==
      Math.floor(Math.abs(offsetX.value + pagerX.value))
    );
  }, [getPageTranslate]);

  const onPan = useAnimatedGestureHandler<
    PanGestureHandlerGestureEvent,
    {
      pagerActive: boolean;
      offsetX: null | number;
    }
  >({
    onGesture: (evt) => {
      onGesture(evt, isActive);

      if (isActive.value && !isPagerInProgress.value) {
        onEnabledGesture(evt);
      }
    },

    onInit: (_, ctx) => {
      ctx.offsetX = null;
    },

    shouldHandleEvent: (evt) => {
      return (
        (evt.numberOfPointers === 1 &&
          isActive.value &&
          Math.abs(evt.velocityX) > Math.abs(evt.velocityY) &&
          shouldHandleGestureEvent(evt)) ||
        isPagerInProgress.value
      );
    },

    onEvent: (evt) => {
      velocity.value = clampVelocity(
        evt.velocityX,
        MIN_VELOCITY,
        MAX_VELOCITY,
      );
    },

    onStart: (_, ctx) => {
      ctx.offsetX = null;
    },

    onActive: (evt, ctx) => {
      // workaround alert
      // the event triggers with a delay and first frame value jumps
      // we capture that value and subtract from the actual one
      // so the translate happens on a second frame
      if (ctx.offsetX === null) {
        ctx.offsetX =
          evt.translationX < 0 ? evt.translationX : -evt.translationX;
      }

      const val = evt.translationX - ctx.offsetX;

      const canSwipe = getCanSwipe(val);
      pagerX.value = canSwipe ? val : friction(val);
    },

    onEnd: (evt, ctx) => {
      const val = evt.translationX - ctx.offsetX!;

      const canSwipe = getCanSwipe(val);

      offsetX.value += pagerX.value;
      pagerX.value = 0;

      const nextIndex = getNextIndex(evt.velocityX);

      const vx = Math.abs(evt.velocityX);

      const translation = Math.abs(val);
      const isHalf = width / 2 < translation;

      const shouldMoveToNextPage = (vx > 10 || isHalf) && canSwipe;

      // we invert the value since the translationY is left to right
      toValueAnimation.value = -(shouldMoveToNextPage
        ? -getPageTranslate(nextIndex)
        : -getPageTranslate(index.value));

      onChangePageAnimation(!shouldMoveToNextPage);

      if (shouldMoveToNextPage) {
        index.value = nextIndex;
        onIndexChangeCb(nextIndex);
      }
    },
  });

  const onTap = useAnimatedGestureHandler({
    shouldHandleEvent: (evt) => {
      return evt.numberOfPointers === 1 && isActive.value;
    },

    onStart: () => {
      cancelAnimation(offsetX);
    },

    onEnd: () => {
      onChangePageAnimation(true);
    },
  });

  const pagerStyles = useAnimatedStyle<ViewStyle>(() => {
    const translateX = pagerX.value + offsetX.value;

    onPagerTranslateChange(translateX);

    return {
      width: totalWidth.value,
      transform: [
        {
          translateX,
        },
      ],
    };
  }, []);

  const pagerRefs = useMemo<PageRefs>(() => [pagerRef, tapRef], []);

  const pagesToRender = useMemo(() => {
    const temp = [];

    for (let i = 0; i < totalCount; i += 1) {
      let itemToUse;

      if (typeof getItem === 'function') {
        itemToUse = getItem(pages, i);
      } else if (Array.isArray(pages)) {
        itemToUse = pages[i];
      } else {
        throw new Error(
          'Pager: items either should be an array of getItem should be defined',
        );
      }

      const shouldRender = getShouldRender(i, activeIndex, diffValue);

      if (!shouldRender) {
        temp.push(null);
      } else {
        temp.push(
          <Page
            key={keyExtractor(itemToUse, i)}
            item={itemToUse}
            currentIndex={index}
            pagerRefs={pagerRefs}
            onPageStateChange={onPageStateChange}
            index={i}
            length={totalCount}
            gutterWidth={gutterWidth}
            renderPage={renderPage}
            getPageTranslate={getPageTranslate}
            width={width}
            isPagerInProgress={isPagerInProgress}
            shouldRenderGutter={shouldRenderGutter}
          />,
        );
      }
    }

    return temp;
  }, [
    activeIndex,
    diffValue,
    keyExtractor,
    getItem,
    totalCount,
    pages,
    getShouldRender,
    index,
    pagerRefs,
    onPageStateChange,
    gutterWidth,
    renderPage,
    getPageTranslate,
    width,
    isPagerInProgress,
    shouldRenderGutter,
  ]);

  return (
    <View style={StyleSheet.absoluteFillObject}>
      <Animated.View style={[StyleSheet.absoluteFill]}>
        <PanGestureHandler
          ref={pagerRef}
          minDist={0.1}
          minVelocityX={0.1}
          activeOffsetX={[-4, 4]}
          activeOffsetY={verticallyEnabled ? [-4, 4] : undefined}
          simultaneousHandlers={[tapRef, ...outerGestureHandlerRefs]}
          onGestureEvent={onPan}
        >
          <Animated.View style={StyleSheet.absoluteFill}>
            <TapGestureHandler
              ref={tapRef}
              maxDeltaX={10}
              maxDeltaY={10}
              simultaneousHandlers={pagerRef}
              onGestureEvent={onTap}
            >
              <Animated.View
                style={[StyleSheet.absoluteFill, pagerWrapperStyles]}
              >
                <Animated.View style={StyleSheet.absoluteFill}>
                  <Animated.View style={[styles.pager, pagerStyles]}>
                    {pagesToRender}
                  </Animated.View>
                </Animated.View>
              </Animated.View>
            </TapGestureHandler>
          </Animated.View>
        </PanGestureHandler>
      </Animated.View>
    </View>
  );
})