react-native-reanimated#useAnimatedStyle TypeScript Examples

The following examples show how to use react-native-reanimated#useAnimatedStyle. 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: Picker.tsx    From swiftui-react-native with MIT License 6 votes vote down vote up
Divider = ({
  color,
  index,
  selection,
  childCount,
}: {
  color: string;
  index: number;
  selection: number;
  childCount: number;
}) => {
  const animatedDividerStyle = useAnimatedStyle(() => {
    return {
      opacity:
        index === selection ||
        index === selection - 1 ||
        index === childCount - 1
          ? withTiming(0)
          : withTiming(1),
    };
  }, [index, selection, childCount]);

  return (
    <Animated.View
      style={[
        styles.divider,
        animatedDividerStyle,
        {
          borderRightColor: color,
        },
      ]}
    />
  );
}
Example #2
Source File: useControls.tsx    From react-native-gallery-toolkit with MIT License 6 votes vote down vote up
export function useControls() {
  const controlsHidden = useSharedValue(false);

  const translateYConfig = {
    duration: 400,
    easing: Easing.bezier(0.33, 0.01, 0, 1),
  };

  const controlsStyles = useAnimatedStyle(() => {
    return {
      opacity: controlsHidden.value ? withTiming(0) : withTiming(1),
      transform: [
        {
          translateY: controlsHidden.value
            ? withTiming(-100, translateYConfig)
            : withTiming(0, translateYConfig),
        },
      ],
      position: 'absolute',
      top: 0,
      width: '100%',
      zIndex: 1,
    };
  });

  const setControlsHidden = useWorkletCallback((hidden: boolean) => {
    if (controlsHidden.value === hidden) {
      return;
    }

    controlsHidden.value = hidden;
  }, []);

  return {
    controlsHidden,
    controlsStyles,
    setControlsHidden,
  };
}
Example #3
Source File: FullFeatured.tsx    From react-native-gallery-toolkit with MIT License 6 votes vote down vote up
function CustomHeader({
  bottomTranslateY,
  headerShown,
  topInset,
}: {
  bottomTranslateY: Animated.SharedValue<number>;
  headerShown: Animated.SharedValue<boolean>;
  topInset: number;
}) {
  const style = useAnimatedStyle(() => {
    return {
      paddingTop: topInset,
      zIndex: 1,
      transform: [
        {
          translateY: bottomTranslateY.value * -1,
        },
      ],
    };
  }, []);

  const opacityAnimatedStyles = useToggleOpacity(headerShown);

  return (
    <Animated.View style={[style, opacityAnimatedStyles]}>
      <DetachedHeader />
    </Animated.View>
  );
}
Example #4
Source File: FullFeatured.tsx    From react-native-gallery-toolkit with MIT License 6 votes vote down vote up
export function useToggleOpacity(
  prop: Animated.SharedValue<boolean>,
) {
  const translateY = useSharedValue(1);

  const styles = useAnimatedStyle(() => {
    if (prop.value) {
      return {
        opacity: withTiming(1),
        transform: [{ translateY: 0 }],
      };
    }

    return {
      opacity: withTiming(0, undefined, () => {
        translateY.value = -99999;
      }),
      transform: [{ translateY: translateY.value }],
    };
  }, []);

  return styles;
}
Example #5
Source File: CursorLine.tsx    From react-native-wagmi-charts with MIT License 6 votes vote down vote up
export function LineChartCursorLine({
  children,
  color = 'gray',
  lineProps = {},
}: LineChartCursorLineProps) {
  const { height } = React.useContext(LineChartDimensionsContext);
  const { currentX, isActive } = useLineChart();

  const vertical = useAnimatedStyle(() => ({
    opacity: isActive.value ? 1 : 0,
    height: '100%',
    transform: [{ translateX: currentX.value }],
  }));

  return (
    <LineChartCursor type="line">
      <Animated.View style={vertical}>
        <Svg style={styles.svg}>
          <SVGLine
            x1={0}
            y1={0}
            x2={0}
            y2={height}
            strokeWidth={2}
            stroke={color}
            strokeDasharray="3 3"
            {...lineProps}
          />
        </Svg>
      </Animated.View>
      {children}
    </LineChartCursor>
  );
}
Example #6
Source File: InstagramFeed.tsx    From react-native-gallery-toolkit with MIT License 6 votes vote down vote up
Pagination = ({ length, activeIndexInPager }: {
  length: number;
  activeIndexInPager: Animated.SharedValue<number>;
}) => {
  const dots = Array.from({ length: length }, (_, i) => {
    const animatedDotStyle = useAnimatedStyle(() => {
      const color =
        activeIndexInPager.value === i ? '#178EED' : '#A7A7A7';
      const size = activeIndexInPager.value === i ? 6 : 4.5;

      return {
        backgroundColor: color,
        width: size,
        height: size,
        borderRadius: 3,
        marginHorizontal: 1.5,
      };
    }, []);

    return (
      <Animated.View style={animatedDotStyle} key={i}></Animated.View>
    );
  });

  return <View style={[s.paginationContainer]}>{dots}</View>;
}
Example #7
Source File: Dot.tsx    From curved-bottom-navigation-bar with MIT License 5 votes vote down vote up
DotComponent = (props: DotProps) => {
  // props
  const {
    selectedIndex,
    routes,
    progress,
    width,
    dotColor,
    dotSize,
    barHeight,
    isRtl,
    navigationIndex,
  } = props;

  // const
  const {bottom} = useSafeAreaInsets();
  const inputRange = useMemo(
    () => routes.map((_: any, index: number) => index),
    [routes],
  );
  const outputRange = useMemo(
    () =>
      isRtl
        ? routes.map(
            (_: any, index: number) =>
              -(
                (index * width) / routes.length +
                width / routes.length / 2 -
                dotSize / 2
              ),
          )
        : routes.map(
            (_: any, index: number) =>
              (index * width) / routes.length +
              width / routes.length / 2 -
              dotSize / 2,
          ),
    [isRtl, routes, width, dotSize],
  );

  // reanimated
  const translateX = useInterpolate(selectedIndex, inputRange, outputRange);
  const translateY = useInterpolate(
    progress,
    [0, 1],
    [15 - bottom, -(barHeight - HEIGHT_HOLE + 5)],
  );

  const opacity = useInterpolate(progress, [0, 1], [0.2, 1]);

  // reanimated style
  const iconContainerStyle = useAnimatedStyle(() => ({
    opacity: opacity.value,
    justifyContent: 'center',
    alignItems: 'center',
  }));

  const dotStyle = useAnimatedStyle(() => ({
    width: dotSize,
    backgroundColor: dotColor,
    height: dotSize,
    bottom: 0,
    borderRadius: dotSize / 2,
    transform: [{translateX: translateX.value}, {translateY: translateY.value}],
  }));

  // render
  return (
    <Animated.View style={[styles.dot, dotStyle]}>
      <Animated.View style={iconContainerStyle}>
        {routes.map(({icon}, index: number) => (
          <IconDot key={index} index={index} selectedIndex={selectedIndex}>
            {icon({progress, focused: navigationIndex === index})}
          </IconDot>
        ))}
      </Animated.View>
    </Animated.View>
  );
}
Example #8
Source File: IconDot.tsx    From curved-bottom-navigation-bar with MIT License 5 votes vote down vote up
IconDotComponent = (props: IconDotProps) => {
  // props
  const { index, selectedIndex, children } = props;

  // reanimated
  const progress = useSharedValue(0);
  useAnimatedReaction(
    () => selectedIndex.value === index,
    (result, prevValue) => {
      if (result !== prevValue) {
        progress.value = sharedTiming(result ? 1 : 0);
      }
    }
  );
  const opacity = useInterpolate(progress, [0, 0.6, 1], [0, 0, 1]);
  const scale = useInterpolate(progress, [0, 1], [0.2, 1]);

  // reanimated style
  const style = useAnimatedStyle(() => ({
    position: 'absolute',
    opacity: opacity.value,
    transform: [{ scale: scale.value }],
  }));

  // render
  return <Animated.View style={[style]}>{children}</Animated.View>;
}
Example #9
Source File: CrosshairTooltip.tsx    From react-native-wagmi-charts with MIT License 5 votes vote down vote up
export function CandlestickChartCrosshairTooltip({
  children,
  xGutter = 8,
  yGutter = 8,
  tooltipTextProps,
  textStyle,
  ...props
}: CandlestickChartCrosshairTooltipProps) {
  const { width, height } = React.useContext(CandlestickChartDimensionsContext);
  const { currentY } = useCandlestickChart();
  const { position } = React.useContext(
    CandlestickChartCrosshairTooltipContext
  );

  const elementHeight = useSharedValue(0);
  const elementWidth = useSharedValue(0);

  const handleLayout = React.useCallback(
    (event) => {
      elementHeight.value = event.nativeEvent.layout.height;
      elementWidth.value = event.nativeEvent.layout.width;
    },
    [elementHeight, elementWidth]
  );

  const topOffset = useDerivedValue(() => {
    let offset = 0;
    if (currentY.value < elementHeight.value / 2 + yGutter) {
      offset = currentY.value - (elementHeight.value / 2 + yGutter);
    } else if (currentY.value + elementHeight.value / 2 > height - yGutter) {
      offset = currentY.value + elementHeight.value / 2 - height + yGutter;
    }

    return offset;
  });

  const tooltip = useAnimatedStyle(() => ({
    backgroundColor: 'white',
    position: 'absolute',
    display: 'flex',
    padding: 4,
  }));
  const leftTooltip = useAnimatedStyle(() => ({
    left: xGutter,
    top: -(elementHeight.value / 2) - topOffset.value,
    opacity: position.value === 'left' ? 1 : 0,
  }));
  const rightTooltip = useAnimatedStyle(() => ({
    left: width - elementWidth.value - xGutter,
    top: -(elementHeight.value / 2) - topOffset.value,
    opacity: position.value === 'right' ? 1 : 0,
  }));

  return (
    <>
      <Animated.View
        onLayout={handleLayout}
        {...props}
        style={[tooltip, leftTooltip, props.style]}
      >
        {children || (
          <CandlestickChartPriceText
            {...tooltipTextProps}
            style={[styles.text, tooltipTextProps?.style, textStyle]}
          />
        )}
      </Animated.View>
      <Animated.View {...props} style={[tooltip, rightTooltip, props.style]}>
        {children || (
          <CandlestickChartPriceText
            {...tooltipTextProps}
            style={[styles.text, tooltipTextProps?.style, textStyle]}
          />
        )}
      </Animated.View>
    </>
  );
}
Example #10
Source File: ScalableImageExample.tsx    From react-native-gallery-toolkit with MIT License 5 votes vote down vote up
export default function StandaloneGalleryBasicScreen() {
  const { controlsStyles, setControlsHidden } = useControls();

  const opacity = useSharedValue(0);

  const overlayStyles = useAnimatedStyle(() => {
    return {
      opacity: opacity.value,
      backgroundColor: 'black',
    };
  });

  const onScale = useWorkletCallback((scale: number) => {
    opacity.value = interpolate(
      scale,
      [1, 2],
      [0, 0.3],
      Extrapolate.CLAMP,
    );
  }, []);

  const onGestureStartCallback = () => {
    StatusBar.setHidden(true);
  };
  const onGestureReleaseCallback = () => {
    StatusBar.setHidden(false);
  };

  const onGestureStart = useWorkletCallback(() => {
    setControlsHidden(true);
    runOnJS(onGestureStartCallback)();
  });

  const onGestureRelease = useWorkletCallback(() => {
    setControlsHidden(false);
    runOnJS(onGestureReleaseCallback)();
  });

  return (
    <View style={{ flex: 1, backgroundColor: 'transparent' }}>
      <Animated.View
        pointerEvents="none"
        style={[StyleSheet.absoluteFill, overlayStyles]}
      />

      <View
        style={{
          zIndex: 0,
          flex: 1,
          justifyContent: 'center',
        }}
      >
        <ScalableImage
          width={image.width}
          height={image.height}
          source={image.uri}
          onScale={onScale}
          onGestureStart={onGestureStart}
          onGestureRelease={onGestureRelease}
        />
      </View>

      <Animated.View style={controlsStyles}>
        <DetachedHeader.Container>
          <DetachedHeader />
        </DetachedHeader.Container>
      </Animated.View>
    </View>
  );
}
Example #11
Source File: InstagramFeed.tsx    From react-native-gallery-toolkit with MIT License 5 votes vote down vote up
export default function InstagramFeed() {
  const scrollViewRef = useRef<ScrollView | null>(null);
  const activeItemIndex = useSharedValue(-1);

  const { controlsStyles, setControlsHidden } = useControls();

  const CellRendererComponent = useMemo<
    FlatListProps<ListItemT>['CellRendererComponent']
  >(
    () => ({ children, index, style, ...props }) => {
      const animatedStyles = useAnimatedStyle(() => {
        const isActive =
          activeItemIndex.value !== -1 &&
          activeItemIndex.value === index;

        return {
          zIndex: isActive ? 1 : 0,
        };
      });
      return (
        <Animated.View {...props} style={animatedStyles}>
          {children}
        </Animated.View>
      );
    },
    [],
  );

  return (
    <>
      <FlatList
        contentContainerStyle={{
          // paddingTop: APPBAR_HEIGHT + STATUSBAR_HEIGHT,
        }}
        initialNumToRender={2}
        maxToRenderPerBatch={2}
        data={data}
        keyExtractor={({ id }) => id}
        renderItem={(item) => (
          <RenderItem
            {...item}
            scrollViewRef={scrollViewRef}
            activeItemIndex={activeItemIndex}
            setControlsHidden={setControlsHidden}
          />
        )}
        renderScrollComponent={(props) => (
          // @ts-ignore
          <ScrollView {...props} ref={scrollViewRef} />
        )}
        CellRendererComponent={CellRendererComponent}
      />

      <Animated.View style={controlsStyles}>
        <DetachedHeader.Container>
          <DetachedHeader />
        </DetachedHeader.Container>
      </Animated.View>
    </>
  );
}
Example #12
Source File: ScalableImage.tsx    From react-native-gallery-toolkit with MIT License 4 votes vote down vote up
ScalableImage = React.memo<ScalableImageProps>(
  ({
    outerGestureHandlerRefs = [],
    source,
    width,
    height,
    onStateChange = workletNoop,
    renderImage,

    canvasDimensions,
    outerGestureHandlerActive,

    style,
    onScale = workletNoop,
    onGestureRelease = workletNoop,
    onGestureStart = workletNoop,
    onEnd = workletNoop,

    MAX_SCALE = 3,
    MIN_SCALE = 1,
    timingConfig = defaultTimingConfig,

    enabled = true,
  }) => {
    fixGestureHandler();

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

    const imageSource =
      typeof source === 'string'
        ? {
            uri: source,
          }
        : source;

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

    const pinchRef = useRef(null);

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

    const { targetWidth, targetHeight } = normalizeDimensions(
      {
        width,
        height,
      },
      canvasDimensions?.width ?? Dimensions.get('window').width,
    );

    const canvas = vectors.create(
      canvasDimensions?.width ?? targetWidth,
      canvasDimensions?.height ?? targetHeight,
    );

    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;
      },

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

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

        if (ctx.nextScale > MIN_SCALE && ctx.nextScale < MAX_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);

        // it alow us to scale into different point even then we pan the image
        ctx.adjustFocal = vectors.sub(focal, CENTER);
      },

      afterEach: (evt, ctx) => {
        if (evt.state === 5) {
          return;
        }

        scale.value = ctx.nextScale;
      },

      onStart: (_, ctx) => {
        vectors.set(ctx.origin, ctx.adjustFocal);

        onGestureStart();
      },

      onActive: (_, ctx) => {
        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);
      },

      onEnd: (_, ctx) => {
        onGestureRelease();
        // reset gestureScale value
        ctx.gestureScale = 1;

        // store scale value
        scale.value = withTiming(1, timingConfig, () => {
          'worklet';

          onEnd();
        });
        vectors.set(scaleTranslation, () =>
          withTiming(0, timingConfig),
        );
      },
    });

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

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

      onStateChange(isInactive);

      onScale(scale.value);

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

    return (
      <Animated.View
        style={[{ width: targetWidth, height: targetHeight }, style]}
      >
        <PinchGestureHandler
          enabled={enabled}
          ref={pinchRef}
          onGestureEvent={onScaleEvent}
          simultaneousHandlers={[...outerGestureHandlerRefs]}
        >
          <Animated.View style={StyleSheet.absoluteFillObject}>
            <Animated.View style={animatedStyles}>
              {typeof renderImage === 'function' ? (
                renderImage({
                  source: imageSource,
                  width: targetWidth,
                  height: targetHeight,
                  onLoad: onLoadImageSuccess,
                })
              ) : (
                <AnimatedImageComponent
                  onLoad={onLoadImageSuccess}
                  source={imageSource}
                  style={{
                    width: targetWidth,
                    height: targetHeight,
                  }}
                />
              )}
            </Animated.View>
          </Animated.View>
        </PinchGestureHandler>
      </Animated.View>
    );
  },
)
Example #13
Source File: ButtonTabItem.tsx    From curved-bottom-navigation-bar with MIT License 4 votes vote down vote up
ButtonTabItemComponent = (props: TabBarItemProps) => {
  // props
  const {
    index,
    selectedIndex,
    countTab,
    indexAnimated,
    width,
    icon,
    renderTitle,
    title,
    titleShown,
    focused,
  } = props;
  // reanimated
  const {bottom} = useSafeAreaInsets();
  const isActive = useDerivedValue(() => sharedRound(indexAnimated.value));
  const progress = useSharedValue(0);

  const opacity = useInterpolate(progress, [0, 0.8], [1, 0]);
  const translateY = useInterpolate(progress, [0, 0.4], [0, 10]);
  const scale = useInterpolate(progress, [0, 1], [1, 0.5]);

  // func
  const _onPress = useCallback(() => {
    selectedIndex.value = index;
  }, [index, selectedIndex]);

  // effect
  useAnimatedReaction(
    () => isActive.value === index,
    (result, prevValue) => {
      if (result !== prevValue) {
        progress.value = sharedTiming(result ? 1 : 0);
      }
    },
  );

  // reanimated style
  const containerIconStyle = useAnimatedStyle(() => ({
    opacity: opacity.value,
    justifyContent: 'center',
    alignItems: 'center',
    transform: [
      {
        translateY: translateY.value,
      },
    ],
  }));

  const titleStyle = useAnimatedStyle(() => ({
    transform: [{scale: scale.value}],
  }));

  const buttonTab = useMemo(
    () => ({
      width: width / countTab,
      paddingBottom: bottom,
    }),
    [width, countTab, bottom],
  );

  // render
  const renderIcon = useCallback(() => {
    return icon({progress, focused});
  }, [focused, icon, progress]);

  const _renderTitle = useCallback(() => {
    return renderTitle?.({progress, focused, title: title ?? ''});
  }, [focused, progress, renderTitle, title]);

  const showTitle = useCallback(() => {
    if (typeof renderTitle === 'function') {
      return _renderTitle();
    }
    return (
      <Animated.Text
        style={[styles.title, titleStyle]}
        allowFontScaling={false}
        numberOfLines={1}>
        {title ?? ''}
      </Animated.Text>
    );
  }, [_renderTitle, renderTitle, title, titleStyle]);

  // render
  return (
    <TouchableOpacity onPress={_onPress} activeOpacity={0.7}>
      <View style={[styles.buttonTab, buttonTab]}>
        <Animated.View style={[containerIconStyle]}>
          {renderIcon()}
          {titleShown ? showTitle() : null}
        </Animated.View>
      </View>
    </TouchableOpacity>
  );
}
Example #14
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>
  );
})
Example #15
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 #16
Source File: FullFeatured.tsx    From react-native-gallery-toolkit with MIT License 4 votes vote down vote up
export default function FullFeatured() {
  const nav = useNavigation();

  const [index, setIndex] = useState(1);
  const headerShown = useSharedValue(true);

  const translateY = useSharedValue(0);
  const bottomTranslateY = useSharedValue(0);

  const galleryRef = useRef<SimpleGallery<GalleryItemType[]>>(
    null,
  );

  const onIndexChange = useWorkletCallback((nextIndex: number) => {
    runOnJS(setIndex)(nextIndex);
  }, []);

  function onNext() {
    galleryRef.current!.goNext();
  }

  function onBack() {
    galleryRef.current!.goBack();
  }

  function setHeaderShown(value: boolean) {
    headerShown.value = value;
    nav.setParams({ headerShown: value });

    StatusBar.setHidden(!value);
  }

  function toggleHeaderShown() {
    const nextValue = !headerShown.value;
    setHeaderShown(nextValue);
  }

  function hide() {
    setHeaderShown(false);
  }

  const opacityAnimatedStyles = useToggleOpacity(headerShown);

  const translateYAnimatedStyles = useAnimatedStyle(() => {
    return {
      transform: [{ translateY: bottomTranslateY.value }],
    };
  }, []);

  function handleClose() {
    nav.goBack();
  }

  function shouldPagerHandleGestureEvent() {
    'worklet';

    return translateY.value === 0;
  }

  const handler = useCallback(
    createAnimatedGestureHandler<PanGestureHandlerGestureEvent, {}>({
      shouldHandleEvent: (evt) => {
        'worklet';

        return (
          evt.numberOfPointers === 1 &&
          Math.abs(evt.velocityX) < Math.abs(evt.velocityY)
        );
      },

      onActive: (evt) => {
        'worklet';

        translateY.value = evt.translationY;

        bottomTranslateY.value =
          evt.translationY > 0 ? evt.translationY : 0;
      },

      onEnd: () => {
        'worklet';

        if (translateY.value > 80) {
          translateY.value = withTiming(
            -800,
            undefined,
            runOnJS(handleClose),
          );
        } else {
          translateY.value = withTiming(0);
          bottomTranslateY.value = withTiming(0);
        }
      },
    }),
    [],
  );

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

  const onInteraction = useWorkletCallback(() => {
    runOnJS(hide)();
  }, []);

  const onTap = useWorkletCallback(() => {
    runOnJS(toggleHeaderShown)();
  }, []);

  const onDoubleTap = useWorkletCallback((isScaled: boolean) => {
    if (!isScaled) {
      runOnJS(hide);
    }
  }, []);

  const insets = useSafeAreaInsets();

  return (
    <View style={{ flex: 1, backgroundColor: 'black'  }}>
      <CustomHeader
        topInset={insets.top}
        bottomTranslateY={bottomTranslateY}
        headerShown={headerShown}
      />

      <Animated.View
        style={[translateStyles, StyleSheet.absoluteFill]}
      >
        <SimpleGallery
          ref={galleryRef}
          initialIndex={1}
          items={images}
          keyExtractor={(item) => item.id}
          gutterWidth={56}
          onIndexChange={onIndexChange}
          renderImage={(props, _, index) => {
            return <ImageRender index={index} {...props} />;
          }}
          renderPage={({ item, ...rest }) => {
            if (item.type === 'image') {
              return (
                <SimpleGallery.ImageRenderer item={item} {...rest} />
              );
            }

            // TODO: Figure out why Video component is not working
            return (
              <View>
                <Text>I can be a video</Text>
              </View>
            );
          }}
          onInteraction={onInteraction}
          onTap={onTap}
          onDoubleTap={onDoubleTap}
          numToRender={2}
          shouldPagerHandleGestureEvent={
            shouldPagerHandleGestureEvent
          }
          onPagerEnabledGesture={handler}
          // onPagerTranslateChange={(translateX) => {
          //   console.log(translateX);
          // }}
        />
      </Animated.View>

      <Animated.View
        style={[
          s.bottomBar,
          opacityAnimatedStyles,
          translateYAnimatedStyles,
        ]}
      >
        <Button onPress={onBack} text="Back" />

        <Text>Index: {index}</Text>

        <Button onPress={onNext} text="Next" />
      </Animated.View>
    </View>
  );
}
Example #17
Source File: InstagramFeed.tsx    From react-native-gallery-toolkit with MIT License 4 votes vote down vote up
function RenderItem({
  index: _index,
  activeItemIndex,
  item: { images, name },
  setControlsHidden,
  scrollViewRef,
}: RenderItemProps) {
  const opacity = useSharedValue(0);
  const backgroundScale = useSharedValue(0);
  const activeIndexInPager = useSharedValue(0);

  const normalizedImages = useMemo(
    () =>
      images.map((item) => {
        const { targetWidth, targetHeight } = normalizeDimensions(
          item,
        );

        return {
          ...item,
          width: targetWidth,
          height: targetHeight,
        };
      }),
    [images],
  );

  const onScale = useWorkletCallback((scale: number) => {
    opacity.value = interpolate(
      scale,
      [1, 2],
      [0, 0.7],
      Extrapolate.CLAMP,
    );

    backgroundScale.value = interpolate(
      scale,
      [1, 1.01, 2],
      [0, 4, 5],
      Extrapolate.CLAMP,
    );
  }, []);

  const onGestureStart = useWorkletCallback(() => {
    setControlsHidden(true);
    runOnJS(StatusBar.setHidden)(true);
    activeItemIndex.value = _index;
  }, []);

  const onGestureRelease = useWorkletCallback(() => {
    //delay for smooth hiding background opacity
    activeItemIndex.value = withDelay(200, withTiming(-1));
    setControlsHidden(false);
    runOnJS(StatusBar.setHidden)(false);
  }, []);

  const overlayStyles = useAnimatedStyle(() => {
    return {
      opacity: opacity.value,
      transform: [
        {
          scale: backgroundScale.value,
        },
      ],
    };
  });

  const keyExtractor = useCallback(
    ({ id }: { id: string }) => id,
    [],
  );

  const canvasHeight = useMemo(
    () => Math.max(...normalizedImages.map((item) => item.height)),
    [normalizedImages],
  );

  const renderPage = useCallback(({ item, pagerRefs }: RenderPageProps<SimpleGalleryItemType>) => {
    return (
      <ScalableImage
        outerGestureHandlerRefs={[...pagerRefs, scrollViewRef]}
        source={item.uri}
        width={item.width}
        height={item.height}
        onScale={onScale}
        onGestureStart={onGestureStart}
        onGestureRelease={onGestureRelease}
      />
    );
  }, []);

  const onIndexChangeWorklet = useWorkletCallback((nextIndex: number) => {
    activeIndexInPager.value = nextIndex;
  }, []);

  const content = (() => {
    if (images.length === 1) {
      return (
        <ScalableImage
          source={images[0].uri}
          width={images[0].width}
          height={images[0].height}
          onScale={onScale}
          outerGestureHandlerRefs={[scrollViewRef]}
          onGestureStart={onGestureStart}
          onGestureRelease={onGestureRelease}
        />
      );
    } else {
      return (
        <>
          <Pager
            pages={images}
            totalCount={images.length}
            keyExtractor={keyExtractor}
            initialIndex={0}
            width={width}
            gutterWidth={0}
            outerGestureHandlerRefs={[scrollViewRef]}
            verticallyEnabled={false}
            // @ts-expect-error
            renderPage={renderPage}
            onIndexChange={onIndexChangeWorklet}
          />

          <Pagination
            length={images.length}
            activeIndexInPager={activeIndexInPager}
          />
        </>
      );
    }
  })();

  return (
    <Animated.View style={s.itemContainer}>
      <Header uri={images[0].uri} name={name} />

      <Animated.View
        pointerEvents="none"
        style={[s.overlay, overlayStyles]}
      />

      <View style={[s.itemPager, { height: canvasHeight }]}>
        {content}
      </View>

      <Footer />
    </Animated.View>
  );
}
Example #18
Source File: ProgressBar.tsx    From jellyfin-audio-player with MIT License 4 votes vote down vote up
function ProgressBar() {
    const { position, buffered, duration } = useProgress();

    const width = useSharedValue(0);
    const pos = useSharedValue(0);
    const buf = useSharedValue(0);
    const dur = useSharedValue(0);

    const isDragging = useSharedValue(false);
    const offset = useSharedValue(0);

    const bufferAnimation = useDerivedValue(() => {
        return calculateProgressTranslation(buf.value, dur.value, width.value);
    }, [[dur, buf, width.value]]);

    const progressAnimation = useDerivedValue(() => {
        if (isDragging.value) {
            return calculateProgressTranslation(offset.value, width.value, width.value);
        } else {
            return calculateProgressTranslation(pos.value, dur.value, width.value);
        }
    });

    const timePassed = useDerivedValue(() => {
        if (isDragging.value) {
            const currentPosition = (offset.value - DRAG_HANDLE_SIZE / 2) / (width.value - DRAG_HANDLE_SIZE) * dur.value;
            return getMinutes(currentPosition) + ':' + getSeconds(currentPosition);
        } else {
            return getMinutes(pos.value) + ':' + getSeconds(pos.value);
        }
    }, [pos]);

    const timeRemaining = useDerivedValue(() => {
        if (isDragging.value) {
            const currentPosition = (offset.value - DRAG_HANDLE_SIZE / 2) / (width.value - DRAG_HANDLE_SIZE) * dur.value;
            const remaining = (currentPosition - dur.value) * -1;
            return `-${getMinutes(remaining)}:${getSeconds((remaining))}`;
        } else {
            const remaining = (pos.value - dur.value) * -1;
            return `-${getMinutes(remaining)}:${getSeconds((remaining))}`;
        }
    }, [pos, dur]);
    
    const pan = Gesture.Pan()
        .minDistance(1)
        .activeOffsetX(1)
        .activeOffsetY(1)
        .onBegin((e) => {
            isDragging.value = true;
            offset.value = Math.min(Math.max(DRAG_HANDLE_SIZE / 2, e.x), width.value - DRAG_HANDLE_SIZE / 2);
        }).onUpdate((e) => {
            offset.value = Math.min(Math.max(DRAG_HANDLE_SIZE / 2, e.x), width.value - DRAG_HANDLE_SIZE / 2);
        }).onFinalize(() => {
            pos.value = (offset.value - DRAG_HANDLE_SIZE / 2) / (width.value - DRAG_HANDLE_SIZE) * dur.value;
            isDragging.value = false;
            runOnJS(TrackPlayer.seekTo)(pos.value);
        });
    const tap = Gesture.Tap()
        .onBegin((e) => {
            isDragging.value = true;
            offset.value = Math.min(Math.max(DRAG_HANDLE_SIZE / 2, e.x), width.value - DRAG_HANDLE_SIZE / 2);
        }).onFinalize(() => {
            pos.value = (offset.value - DRAG_HANDLE_SIZE / 2) / (width.value - DRAG_HANDLE_SIZE) * dur.value;
            isDragging.value = false;
            runOnJS(TrackPlayer.seekTo)(pos.value);
        });
    const gesture = Gesture.Exclusive(pan, tap);

    useEffect(() => {
        pos.value = position;
        buf.value = buffered;
        dur.value = duration;
    // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [position, buffered, duration]);

    const dragHandleStyles = useAnimatedStyle(() => {
        return {
            transform: [
                { translateX: offset.value },
                { 
                    scale: withTiming(isDragging.value ? 1 : 0, {
                        duration: 100,
                        easing: Easing.out(Easing.ease),
                    })
                }
            ],
        };
    });

    const bufferStyles = useAnimatedStyle(() => ({
        transform: [
            { translateX: bufferAnimation.value }
        ]
    }));

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

    const timePassedStyles = useAnimatedStyle(() => {
        return {
            transform: [
                { translateY: withTiming(isDragging.value && offset.value < 48 ? 12 : 0, {
                    duration: 145,
                    easing: Easing.ease
                }) },
            ],
        };
    });

    const timeRemainingStyles = useAnimatedStyle(() => {
        return {
            transform: [
                { translateY: withTiming(isDragging.value && offset.value > width.value - 48 ? 12 : 0, {
                    duration: 150,
                    easing: Easing.ease
                }) },
            ],
        };
    });

    return (
        <GestureDetector gesture={gesture}>
            <Container onLayout={(e) => { width.value = e.nativeEvent.layout.width; }}>
                <ProgressTrackContainer>
                    <ProgressTrack
                        opacity={0.15}
                    />
                    <ProgressTrack
                        style={bufferStyles}
                        opacity={0.15}
                    />
                    <ProgressTrack
                        style={progressStyles}
                    />
                </ProgressTrackContainer>
                <DragHandle style={dragHandleStyles} />
                <NumberBar style={{ flex: 1 }}>
                    <Number text={timePassed} style={timePassedStyles} />
                    <Number text={timeRemaining} style={timeRemainingStyles} />
                </NumberBar>
            </Container>
        </GestureDetector>
    );
}
Example #19
Source File: Tooltip.tsx    From react-native-wagmi-charts with MIT License 4 votes vote down vote up
export function LineChartTooltip({
  children,
  xGutter = 8,
  yGutter = 8,
  cursorGutter = 48,
  position = 'top',
  textProps,
  textStyle,
  ...props
}: LineChartTooltipProps) {
  const { width, height } = React.useContext(LineChartDimensionsContext);
  const { type } = React.useContext(CursorContext);
  const { currentX, currentY, isActive } = useLineChart();

  const x = useSharedValue(0);
  const elementWidth = useSharedValue(0);
  const elementHeight = useSharedValue(0);

  const handleLayout = React.useCallback(
    (event) => {
      x.value = event.nativeEvent.layout.x;
      elementWidth.value = event.nativeEvent.layout.width;
      elementHeight.value = event.nativeEvent.layout.height;
    },
    [elementHeight, elementWidth, x]
  );

  const animatedCursorStyle = useAnimatedStyle(() => {
    let translateXOffset = elementWidth.value / 2;
    if (currentX.value < elementWidth.value / 2 + xGutter) {
      const xOffset = elementWidth.value / 2 + xGutter - currentX.value;
      translateXOffset = translateXOffset - xOffset;
    }
    if (currentX.value > width - elementWidth.value / 2 - xGutter) {
      const xOffset =
        currentX.value - (width - elementWidth.value / 2 - xGutter);
      translateXOffset = translateXOffset + xOffset;
    }

    let translateYOffset = 0;
    if (position === 'top') {
      translateYOffset = elementHeight.value / 2 + cursorGutter;
      if (currentY.value - translateYOffset < yGutter) {
        translateYOffset = currentY.value - yGutter;
      }
    } else if (position === 'bottom') {
      translateYOffset = -(elementHeight.value / 2) - cursorGutter / 2;
      if (
        currentY.value - translateYOffset + elementHeight.value >
        height - yGutter
      ) {
        translateYOffset =
          currentY.value - (height - yGutter) + elementHeight.value;
      }
    }

    return {
      transform: [
        { translateX: currentX.value - translateXOffset },
        {
          translateY:
            type === 'crosshair'
              ? currentY.value - translateYOffset
              : position === 'top'
              ? yGutter
              : height - elementHeight.value - yGutter,
        },
      ],
      opacity: isActive.value ? 1 : 0,
    };
  });

  return (
    <Animated.View
      onLayout={handleLayout}
      {...props}
      style={[
        {
          position: 'absolute',
          padding: 4,
          alignSelf: 'flex-start',
        },
        animatedCursorStyle,
        props.style,
      ]}
    >
      {children || <LineChartPriceText style={[textStyle]} {...textProps} />}
    </Animated.View>
  );
}
Example #20
Source File: CursorCrosshair.tsx    From react-native-wagmi-charts with MIT License 4 votes vote down vote up
export function LineChartCursorCrosshair({
  children,
  color = 'black',
  size = 8,
  outerSize = 32,
  crosshairWrapperProps = {},
  crosshairProps = {},
  crosshairOuterProps = {},
  ...props
}: LineChartCursorCrosshairProps) {
  const { currentX, currentY, isActive } = useLineChart();

  // It seems that enabling spring animation on initial render on Android causes a crash.
  const [enableSpringAnimation, setEnableSpringAnimation] = React.useState(
    Platform.OS === 'ios'
  );
  React.useEffect(() => {
    setTimeout(() => {
      setEnableSpringAnimation(true);
    }, 100);
  }, []);

  const animatedCursorStyle = useAnimatedStyle(() => ({
    transform: [
      { translateX: currentX.value - outerSize / 2 },
      { translateY: currentY.value - outerSize / 2 },
      {
        scale: enableSpringAnimation
          ? withSpring(isActive.value ? 1 : 0, {
              damping: 10,
            })
          : 0,
      },
    ],
  }));

  return (
    <LineChartCursor type="crosshair" {...props}>
      <Animated.View
        {...crosshairWrapperProps}
        style={[
          {
            width: outerSize,
            height: outerSize,
            alignItems: 'center',
            justifyContent: 'center',
          },
          animatedCursorStyle,
          crosshairWrapperProps.style,
        ]}
      >
        <View
          {...crosshairOuterProps}
          style={[
            {
              backgroundColor: color,
              width: outerSize,
              height: outerSize,
              borderRadius: outerSize,
              opacity: 0.1,
              position: 'absolute',
            },
            crosshairOuterProps.style,
          ]}
        />
        <View
          {...crosshairProps}
          style={[
            {
              backgroundColor: color,
              width: size,
              height: size,
              borderRadius: size,
            },
            crosshairProps.style,
          ]}
        />
      </Animated.View>
      {children}
    </LineChartCursor>
  );
}
Example #21
Source File: Crosshair.tsx    From react-native-wagmi-charts with MIT License 4 votes vote down vote up
export function CandlestickChartCrosshair({
  color,
  onCurrentXChange,
  children,
  horizontalCrosshairProps = {},
  verticalCrosshairProps = {},
  lineProps = {},
  ...props
}: CandlestickChartCrosshairProps) {
  const { width, height } = React.useContext(CandlestickChartDimensionsContext);
  const { currentX, currentY, step } = useCandlestickChart();

  const tooltipPosition = useSharedValue<'left' | 'right'>('left');

  const opacity = useSharedValue(0);
  const onGestureEvent = useAnimatedGestureHandler<
    GestureEvent<LongPressGestureHandlerEventPayload>
  >({
    onActive: ({ x, y }) => {
      const boundedX = x <= width - 1 ? x : width - 1;
      if (boundedX < 100) {
        tooltipPosition.value = 'right';
      } else {
        tooltipPosition.value = 'left';
      }
      opacity.value = 1;
      currentY.value = clamp(y, 0, height);
      currentX.value = boundedX - (boundedX % step) + step / 2;
    },
    onEnd: () => {
      opacity.value = 0;
      currentY.value = -1;
      currentX.value = -1;
    },
  });
  const horizontal = useAnimatedStyle(() => ({
    opacity: opacity.value,
    transform: [{ translateY: currentY.value }],
  }));
  const vertical = useAnimatedStyle(() => ({
    opacity: opacity.value,
    transform: [{ translateX: currentX.value }],
  }));

  useAnimatedReaction(
    () => currentX.value,
    (data, prevData) => {
      if (data !== -1 && data !== prevData && onCurrentXChange) {
        runOnJS(onCurrentXChange)(data);
      }
    }
  );

  return (
    <LongPressGestureHandler
      minDurationMs={0}
      maxDist={999999}
      onGestureEvent={onGestureEvent}
      {...props}
    >
      <Animated.View style={StyleSheet.absoluteFill}>
        <Animated.View
          style={[StyleSheet.absoluteFill, horizontal]}
          {...horizontalCrosshairProps}
        >
          <CandlestickChartLine color={color} x={width} y={0} {...lineProps} />
          <CandlestickChartCrosshairTooltipContext.Provider
            value={{ position: tooltipPosition }}
          >
            {children}
          </CandlestickChartCrosshairTooltipContext.Provider>
        </Animated.View>
        <Animated.View
          style={[StyleSheet.absoluteFill, vertical]}
          {...verticalCrosshairProps}
        >
          <CandlestickChartLine color={color} x={0} y={height} {...lineProps} />
        </Animated.View>
      </Animated.View>
    </LongPressGestureHandler>
  );
}
Example #22
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 #23
Source File: Linear.tsx    From swiftui-react-native with MIT License 4 votes vote down vote up
Linear = ({
  value,
  total = 100,
  backgroundColor,
  opacity,
  frame,
  cornerRadius,
  rotationEffect,
  scaleEffect,
  padding,
  border,
  shadow,
  zIndex,
  style,
  tint,
  alert,
  onAppear,
  onDisappear,
}: LinearProps) => {
  useAlert(alert);
  useLifecycle(onAppear, onDisappear);
  const colorScheme = useColorScheme();
  const [sliderWidth, sliderHeight] = getSliderWidth(frame);
  const midPoint = total / 2;
  const slope = midPoint / (sliderWidth / 2);
  const progress = useSharedValue(value2Position(value, midPoint, slope));

  useEffect(() => {
    if (value > total) {
      progress.value = withTiming(value2Position(total, midPoint, slope));
    } else if (value < 0) {
      progress.value = withTiming(0);
    } else {
      const newPos = value2Position(value, midPoint, slope);
      progress.value = withTiming(newPos);
    }
  }, [value, total]);

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

  return (
    <View
      style={[
        {
          opacity,
          zIndex,
          backgroundColor: getColor(backgroundColor, colorScheme),
          ...getCornerRadius(cornerRadius),
          ...getPadding(padding),
          ...getBorder(border),
          ...getShadow(shadow),
          ...getTransform(scaleEffect, rotationEffect),
        },
        style,
      ]}
    >
      <View
        style={[
          styles.progressBar,
          {
            width: sliderWidth,
            height: sliderHeight,
            backgroundColor: getColor('systemGray4', colorScheme),
          },
        ]}
      >
        <Animated.View
          style={[
            {
              height: sliderHeight,
              borderRadius: 10,
              backgroundColor: getColor(tint, colorScheme, 'systemBlue'),
            },
            animatedFillStyle,
          ]}
        />
      </View>
    </View>
  );
}
Example #24
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 #25
Source File: Dot.tsx    From curved-bottom-navigation-bar with MIT License 4 votes vote down vote up
DotComponent = (props: DotProps) => {
  // props
  const {
    selectedIndex,
    routes,
    progress,
    width,
    dotColor,
    dotSize,
    barHeight,
    isRtl,
    navigationIndex,
  } = props;

  // const
  const { bottom } = useSafeAreaInsets();
  const inputRange = useMemo(
    () => routes.map((_: any, index: number) => index),
    [routes]
  );
  const outputRange = useMemo(
    () =>
      isRtl
        ? routes.map(
            (_: any, index: number) =>
              -(
                (index * width) / routes.length +
                width / routes.length / 2 -
                dotSize / 2
              )
          )
        : routes.map(
            (_: any, index: number) =>
              (index * width) / routes.length +
              width / routes.length / 2 -
              dotSize / 2
          ),
    [isRtl, routes, width, dotSize]
  );

  // reanimated
  const translateX = useInterpolate(selectedIndex, inputRange, outputRange);
  const translateY = useInterpolate(
    progress,
    [0, 1],
    [15 - bottom, -(barHeight - HEIGHT_HOLE + 5)]
  );

  const opacity = useInterpolate(progress, [0, 1], [0.2, 1]);

  // reanimated style
  const iconContainerStyle = useAnimatedStyle(() => ({
    opacity: opacity.value,
    justifyContent: 'center',
    alignItems: 'center',
  }));

  const dotStyle = useAnimatedStyle(() => ({
    width: dotSize,
    backgroundColor: dotColor,
    height: dotSize,
    bottom: 0,
    borderRadius: dotSize / 2,
    transform: [
      { translateX: translateX.value },
      { translateY: translateY.value },
    ],
  }));

  // render
  return (
    <Animated.View style={[styles.dot, dotStyle]}>
      <Animated.View style={iconContainerStyle}>
        {routes.map(({ icon }, index: number) => (
          <IconDot key={index} index={index} selectedIndex={selectedIndex}>
            {icon({ progress, focused: navigationIndex === index })}
          </IconDot>
        ))}
      </Animated.View>
    </Animated.View>
  );
}
Example #26
Source File: ButtonTabItem.tsx    From curved-bottom-navigation-bar with MIT License 4 votes vote down vote up
ButtonTabItemComponent = (props: TabBarItemProps) => {
  // props
  const {
    index,
    selectedIndex,
    countTab,
    indexAnimated,
    width,
    icon,
    renderTitle,
    title,
    titleShown,
    focused,
  } = props;
  // reanimated
  const { bottom } = useSafeAreaInsets();
  const isActive = useDerivedValue(() => sharedRound(indexAnimated.value));
  const progress = useSharedValue(0);

  const opacity = useInterpolate(progress, [0, 0.8], [1, 0]);
  const translateY = useInterpolate(progress, [0, 0.4], [0, 10]);
  const scale = useInterpolate(progress, [0, 1], [1, 0.5]);

  // func
  const _onPress = useCallback(() => {
    selectedIndex.value = index;
  }, [index, selectedIndex]);

  // effect
  useAnimatedReaction(
    () => isActive.value === index,
    (result, prevValue) => {
      if (result !== prevValue) {
        progress.value = sharedTiming(result ? 1 : 0);
      }
    }
  );

  // reanimated style
  const containerIconStyle = useAnimatedStyle(() => ({
    opacity: opacity.value,
    justifyContent: 'center',
    alignItems: 'center',
    transform: [
      {
        translateY: translateY.value,
      },
    ],
  }));

  const titleStyle = useAnimatedStyle(() => ({
    transform: [{ scale: scale.value }],
  }));

  const buttonTab = useMemo(
    () => ({
      width: width / countTab,
      paddingBottom: bottom,
    }),
    [width, countTab, bottom]
  );

  // render
  const renderIcon = useCallback(() => {
    return icon({ progress, focused });
  }, [focused, icon, progress]);

  const _renderTitle = useCallback(() => {
    return renderTitle?.({ progress, focused, title: title ?? '' });
  }, [focused, progress, renderTitle, title]);

  const showTitle = useCallback(() => {
    if (typeof renderTitle === 'function') {
      return _renderTitle();
    }
    return (
      <Animated.Text
        style={[styles.title, titleStyle]}
        allowFontScaling={false}
        numberOfLines={1}
      >
        {title ?? ''}
      </Animated.Text>
    );
  }, [_renderTitle, renderTitle, title, titleStyle]);

  // render
  return (
    <TouchableOpacity onPress={_onPress} activeOpacity={0.7}>
      <View style={[styles.buttonTab, buttonTab]}>
        <Animated.View style={[containerIconStyle]}>
          {renderIcon()}
          {titleShown ? showTitle() : null}
        </Animated.View>
      </View>
    </TouchableOpacity>
  );
}