react-native#LayoutChangeEvent TypeScript Examples

The following examples show how to use react-native#LayoutChangeEvent. 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: AutoSizer.tsx    From react-native-paper-dates with MIT License 6 votes vote down vote up
export default function AutoSizer({
  children,
}: {
  children: ({ width, height }: WidthAndHeight) => any
}) {
  const [layout, setLayout] = React.useState<WidthAndHeight | null>(null)
  const onLayout = React.useCallback(
    (event: LayoutChangeEvent) => {
      const nl = event.nativeEvent.layout

      // https://github.com/necolas/react-native-web/issues/1704
      if (!layout || layout.width !== nl.width || layout.height !== nl.height) {
        setLayout({ width: nl.width, height: nl.height })
      }
    },
    [layout, setLayout]
  )
  return (
    <View style={[styles.autoSizer, layout && layout]} onLayout={onLayout}>
      {layout ? children(layout) : null}
    </View>
  )
}
Example #2
Source File: withResponsiveWidth.tsx    From selftrace with MIT License 6 votes vote down vote up
export function withResponsiveWidth<T extends ResponsiveWidthRenderProps>(
  WrappedComponent: React.ComponentType<T>
) {
  return (props: T): React.ClassicElement<Omit<T, 'responsiveWidth'>> => {
    const [responsiveWidth, setResponsiveWidth] = React.useState<ResponsiveSize | undefined>(
      undefined
    );
    const [screenWidth, setScreenWidth] = React.useState<number | undefined>(undefined);

    const onLayout = (event: LayoutChangeEvent) => {
      const newScreenWidth = Dimensions.get('window').width;
      const newResponsiveWidth = getResponsiveSize(event.nativeEvent.layout.width);

      if (newScreenWidth !== screenWidth) {
        setScreenWidth(newScreenWidth);
      }

      if (newScreenWidth !== screenWidth && newResponsiveWidth !== responsiveWidth) {
        setResponsiveWidth(newResponsiveWidth);
      }
    };

    return (
      <View onLayout={onLayout} style={{ flex: 1 }}>
        <WrappedComponent {...(props as T)} responsiveWidth={responsiveWidth} />
      </View>
    );
  };
}
Example #3
Source File: ProgressBar.tsx    From react-native-jigsaw with MIT License 6 votes vote down vote up
handleLayout = (event: LayoutChangeEvent) => {
    const { width = 150, onLayout } = this.props;
    if (!width) {
      this.setState({ width: event.nativeEvent.layout.width });
    }
    if (onLayout) {
      onLayout(event);
    }
  };
Example #4
Source File: HeaderSegment.tsx    From nlw2-proffy with MIT License 6 votes vote down vote up
private handleTitleLayout = (e: LayoutChangeEvent) => {
    const { height, width } = e.nativeEvent.layout;

    this.setState(({ titleLayout }) => {
      if (
        titleLayout &&
        height === titleLayout.height &&
        width === titleLayout.width
      ) {
        return null;
      }

      return {
        titleLayout: { height, width },
      };
    });
  };
Example #5
Source File: HeaderSegment.tsx    From nlw2-proffy with MIT License 6 votes vote down vote up
private handleLeftLabelLayout = (e: LayoutChangeEvent) => {
    const { height, width } = e.nativeEvent.layout;
    const { leftLabelLayout } = this.state;

    if (
      leftLabelLayout &&
      height === leftLabelLayout.height &&
      width === leftLabelLayout.width
    ) {
      return;
    }

    this.setState({ leftLabelLayout: { height, width } });
  };
Example #6
Source File: CardStack.tsx    From nlw2-proffy with MIT License 6 votes vote down vote up
private handleLayout = (e: LayoutChangeEvent) => {
    const { height, width } = e.nativeEvent.layout;

    const layout = { width, height };

    this.setState((state, props) => {
      if (height === state.layout.height && width === state.layout.width) {
        return null;
      }

      return {
        layout,
        headerHeights: getHeaderHeights(
          props.routes,
          props.insets,
          state.descriptors,
          layout,
          state.headerHeights
        ),
      };
    });
  };
Example #7
Source File: BubbleChart.tsx    From companion-kit with MIT License 6 votes vote down vote up
onLayout = (e: LayoutChangeEvent) => {
        if (this._layoutReady) {
            // for some reason onLayout fires twice
            return;
        }

        const { height, width } = e.nativeEvent.layout;

        this.setState({ height, width }, () => {
            this._layoutReady = true;
            this.activateChart();
        });
    }
Example #8
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 #9
Source File: index.tsx    From frontatish with MIT License 5 votes vote down vote up
Button = (props: ButtonProps) => {
  const { customStyles, disabled, label, type, onPress, loading } = props;
  const [width, setWidth] = useState<number>(0);
  // getting the suitable color based on the theme
  // activated inside the app
  const Colors = useColors();
  // getting base button styles defined
  // by our design guidelines, component
  // also takes custom styles so that can also
  // be applied
  const baseBtnStyles: StyleType = getBtnStyles(type, Colors, disabled);
  const mainBtnStyles: StyleType = {
    ...baseBtnStyles,
    ...customStyles,
  };
  const baseLabelStyles: StyleType = getLabelStyles(type, Colors, disabled);
  const measureLayout = (e: LayoutChangeEvent) => {
    setWidth(e.nativeEvent.layout.width);
  };

  const renderProgressBar = () => {
    const barColor = type === 'primary' ? Colors.white : Colors.primary;
    return (
      <View
        style={{
          position: 'absolute',
          top: 0,
          borderRadius: 5,
        }}
      >
        <Progress width={width} barColor={barColor} />
      </View>
    );
  };

  return (
    <Ripple
      onPress={onPress}
      disabled={disabled}
      style={mainBtnStyles}
      onLayout={measureLayout}
    >
      {loading && renderProgressBar()}
      <Text style={baseLabelStyles}>{label}</Text>
    </Ripple>
  );
}
Example #10
Source File: Collapse.tsx    From react-native-design-kit with MIT License 5 votes vote down vote up
export default function Collapse({
  visible = false,
  animationDuration = 250,
  byPassAnimationCallback = false,
  children,
}: CollapseProps) {
  const [animating, setAnimating] = useState(false);
  const height = useRef<number>(0);
  const animation = useRef(new Animated.Value(visible ? 1 : 0)).current;
  const toggle = useRef(visible);
  const fakeContent = !animating && !toggle.current;

  const handleLayoutView = useCallback(
    (event: LayoutChangeEvent) => {
      if (!animating) {
        height.current = event.nativeEvent.layout.height;
      }
    },
    [animating],
  );

  const handleRunAnimation = useCallback(() => {
    setAnimating(true);
    Animated.timing(animation, {
      toValue: visible ? 1 : 0,
      duration: animationDuration,
      useNativeDriver: false,
    }).start(callback => {
      if (callback.finished || byPassAnimationCallback) {
        toggle.current = visible;
        setAnimating(false);
      }
    });
  }, [animation, visible, animationDuration]);

  useDidUpdate(handleRunAnimation, [handleRunAnimation]);

  return (
    <Animated.View
      style={StyleSheet.flatten([
        toggle.current && !animating
          ? undefined
          : {
              overflow: 'hidden',
              height: animation.interpolate({
                inputRange: [0, 1],
                outputRange: [0, height.current],
              }),
            },
      ])}>
      <View
        testID="view"
        pointerEvents={fakeContent ? 'none' : 'auto'}
        style={StyleSheet.flatten([fakeContent && {position: 'absolute'}])}
        onLayout={handleLayoutView}>
        {children}
      </View>
    </Animated.View>
  );
}
Example #11
Source File: SwitchableText.tsx    From react-native-design-kit with MIT License 5 votes vote down vote up
export default function SwitchableText({
  containerStyle,
  texts,
  textStyle,
  duration = 2000,
  progressBar = true,
  byPassAnimationCallback = false,
  progressBarStyle,
}: SwitchableTextProps) {
  const [index, setIndex] = useState(0);
  const [width, setWidth] = useState<number>();
  const animation = useRef(new Animated.Value(0)).current;

  const handleLayoutText = useCallback(
    (event: LayoutChangeEvent) =>
      progressBar && setWidth(event.nativeEvent.layout.width),
    [progressBar],
  );

  const handleRenderText = useMemo(
    () => (
      <Text
        testID="text"
        style={StyleSheet.flatten([styles.text, textStyle])}
        onLayout={handleLayoutText}>
        {texts[index]}
      </Text>
    ),
    [index, texts, textStyle, handleLayoutText],
  );

  const handleRenderBar = useMemo(() => {
    return (
      progressBar &&
      width !== undefined && (
        <Animated.View
          style={StyleSheet.flatten([
            styles.progress,
            progressBarStyle,
            {
              width: animation.interpolate({
                inputRange: [0, 1],
                outputRange: [0, width],
              }),
            },
          ])}
        />
      )
    );
  }, [width, animation, progressBar, progressBarStyle]);

  const handleRunAnimation = useCallback(() => {
    animation.setValue(0);
    Animated.timing(animation, {
      toValue: 1,
      duration,
      useNativeDriver: false,
    }).start(
      callback =>
        (byPassAnimationCallback || callback.finished) &&
        setIndex(index + 1 >= texts.length ? 0 : index + 1),
    );
  }, [animation, index, duration, texts]);

  useDidUpdate(() => {
    setIndex(0);
  }, [texts]);

  useEffect(handleRunAnimation, [handleRunAnimation]);

  return texts.length > 0 ? (
    <View style={containerStyle}>
      {handleRenderText}
      {handleRenderBar}
    </View>
  ) : null;
}
Example #12
Source File: index.d.ts    From react-native-actions-sheet with MIT License 5 votes vote down vote up
_showModal: (event: LayoutChangeEvent) => Promise<void>;
Example #13
Source File: index.d.ts    From react-native-actions-sheet with MIT License 5 votes vote down vote up
_onDeviceLayout: (_event: LayoutChangeEvent) => Promise<void>;
Example #14
Source File: index.tsx    From react-native-actions-sheet with MIT License 5 votes vote down vote up
_showModal = async (event: LayoutChangeEvent) => {
    let { gestureEnabled, delayActionSheetDraw, delayActionSheetDrawTime } =
      this.props;

    if (!event?.nativeEvent) return;
    let height = event.nativeEvent.layout.height;
    if (this.layoutHasCalled) {
      this.actionSheetHeight = height;
      this._returnToPrevScrollPosition(height);
      return;
    } else {
      this.initialScrolling = true;
      this.layoutHasCalled = true;
      this.actionSheetHeight = height;
      let scrollOffset = this.getInitialScrollPosition();
      this.isRecoiling = false;
      if (Platform.OS === "ios") {
        await waitAsync(delayActionSheetDrawTime ?? 0);
      } else {
        if (delayActionSheetDraw) {
          await waitAsync(delayActionSheetDrawTime ?? 0);
        }
      }
      this._scrollTo(scrollOffset, false);

      this.prevScroll = scrollOffset;
      if (Platform.OS === "ios") {
        await waitAsync(delayActionSheetDrawTime ?? 0 / 2);
      } else {
        if (delayActionSheetDraw) {
          await waitAsync((delayActionSheetDrawTime ?? 0) / 2);
        }
      }
      this._openAnimation(scrollOffset);
      this.underlayScale.setValue(1);
      this.underlayTranslateY.setValue(100);
      if (!gestureEnabled) {
        this.props.onPositionChanged && this.props.onPositionChanged(true);
      }
      this.updateActionSheetPosition(scrollOffset);
    }
  };
Example #15
Source File: index.tsx    From react-native-actions-sheet with MIT License 5 votes vote down vote up
_onDeviceLayout = async (_event: LayoutChangeEvent) => {
    let event = { ..._event };

    if (this.timeout) {
      clearTimeout(this.timeout);
    }

    this.timeout = setTimeout(async () => {
      let safeMarginFromTop = 0;
      let measuredPadding =
        Platform.OS === "ios" ? await this.measure() : StatusBar.currentHeight;

      if (!this.props.drawUnderStatusBar) {
        if (Platform.OS === "android" && !this.props.statusBarTranslucent)
          return;
        safeMarginFromTop = measuredPadding ?? 0;
        if (measuredPadding) {
          this.indicatorTranslateY.setValue(-measuredPadding);
        }
      } else {
        this.updateActionSheetPosition(this.offsetY);
      }
      let height = event.nativeEvent.layout.height - safeMarginFromTop;
      let width = Dimensions.get("window").width;
      if (
        height?.toFixed(0) === calculatedDeviceHeight?.toFixed(0) &&
        width?.toFixed(0) === this.state.deviceWidth?.toFixed(0) &&
        this.deviceLayoutCalled
      )
        return;
      this.deviceLayoutCalled = true;
      calculatedDeviceHeight = height;
      this.setState({
        deviceHeight: height,
        deviceWidth: width,
        portrait: height > width,
        paddingTop: measuredPadding ?? 0,
      });
    }, 1);
  };
Example #16
Source File: index.tsx    From frontatish with MIT License 5 votes vote down vote up
handleLayout = (e: LayoutChangeEvent) => {
    this.setState({
      measured: true,
      height: e.nativeEvent.layout.height,
    });
  };
Example #17
Source File: SimplePopover.tsx    From react-native-portal with MIT License 5 votes vote down vote up
SimplePopover = ({
  position,
  targetLayout,
  onPress,
}: SimplePopoverProps) => {
  const [layout, setLayout] = useState({
    width: 0,
    height: 0,
  });

  const popoverPosition = useMemo(
    () => ({
      opacity: layout.height === 0 || layout.width === 0 ? 0 : 1,
      top:
        position === 'bottom'
          ? targetLayout.y + targetLayout.height + SPACE
          : targetLayout.y - layout.height - SPACE,
      left: targetLayout.x + targetLayout.width / 2 - layout.width / 2,
    }),
    [position, layout, targetLayout]
  );

  const popoverContainerStyle = useMemo(
    () => [styles.popoverContainer, popoverPosition],
    [popoverPosition]
  );

  // callbacks
  const handlePopoverLayout = useCallback(
    ({
      nativeEvent: {
        layout: { height, width },
      },
    }: LayoutChangeEvent) => {
      setLayout(state => {
        if (state.height === height && state.width === width) {
          return state;
        }

        return {
          height,
          width,
        };
      });
    },
    []
  );

  return (
    <TouchableWithoutFeedback onPress={onPress} style={styles.buttonContainer}>
      <View style={styles.backdropContainer}>
        <View onLayout={handlePopoverLayout} style={popoverContainerStyle}>
          <Text>Simple Popover</Text>
        </View>
      </View>
    </TouchableWithoutFeedback>
  );
}
Example #18
Source File: Notifier.tsx    From react-native-notifier with MIT License 5 votes vote down vote up
private onLayout({ nativeEvent }: LayoutChangeEvent) {
    const heightWithMargin = nativeEvent.layout.height + 50;
    this.hiddenComponentValue = -Math.max(heightWithMargin, DEFAULT_COMPONENT_HEIGHT);
  }
Example #19
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 #20
Source File: HeaderBackButton.tsx    From nlw2-proffy with MIT License 4 votes vote down vote up
export default function HeaderBackButton({
  disabled,
  allowFontScaling,
  backImage,
  label,
  labelStyle,
  labelVisible = Platform.OS === 'ios',
  onLabelLayout,
  onPress,
  pressColorAndroid: customPressColorAndroid,
  screenLayout,
  tintColor: customTintColor,
  titleLayout,
  truncatedLabel = 'Back',
  accessibilityLabel = label && label !== 'Back' ? `${label}, back` : 'Go back',
  style,
}: Props) {
  const { dark, colors } = useTheme();

  const [initialLabelWidth, setInitialLabelWidth] = React.useState<
    undefined | number
  >(undefined);

  const tintColor =
    customTintColor !== undefined
      ? customTintColor
      : Platform.select({
          ios: colors.primary,
          default: colors.text,
        });

  const pressColorAndroid =
    customPressColorAndroid !== undefined
      ? customPressColorAndroid
      : dark
      ? 'rgba(255, 255, 255, .32)'
      : 'rgba(0, 0, 0, .32)';

  const handleLabelLayout = (e: LayoutChangeEvent) => {
    onLabelLayout?.(e);

    setInitialLabelWidth(e.nativeEvent.layout.x + e.nativeEvent.layout.width);
  };

  const shouldTruncateLabel = () => {
    return (
      !label ||
      (initialLabelWidth &&
        titleLayout &&
        screenLayout &&
        (screenLayout.width - titleLayout.width) / 2 < initialLabelWidth + 26)
    );
  };

  const renderBackImage = () => {
    if (backImage) {
      return backImage({ tintColor });
    } else {
      return (
        <Image
          style={[
            styles.icon,
            Boolean(labelVisible) && styles.iconWithLabel,
            Boolean(tintColor) && { tintColor },
          ]}
          source={require('../assets/back-icon.png')}
          fadeDuration={0}
        />
      );
    }
  };

  const renderLabel = () => {
    const leftLabelText = shouldTruncateLabel() ? truncatedLabel : label;

    if (!labelVisible || leftLabelText === undefined) {
      return null;
    }

    const labelElement = (
      <View
        style={
          screenLayout
            ? // We make the button extend till the middle of the screen
              // Otherwise it appears to cut off when translating
              [styles.labelWrapper, { minWidth: screenLayout.width / 2 - 27 }]
            : null
        }
      >
        <Animated.Text
          accessible={false}
          onLayout={
            // This measurement is used to determine if we should truncate the label when it doesn't fit
            // Only measure it when label is not truncated because we want the measurement of full label
            leftLabelText === label ? handleLabelLayout : undefined
          }
          style={[
            styles.label,
            tintColor ? { color: tintColor } : null,
            labelStyle,
          ]}
          numberOfLines={1}
          allowFontScaling={!!allowFontScaling}
        >
          {leftLabelText}
        </Animated.Text>
      </View>
    );

    if (backImage || Platform.OS !== 'ios') {
      // When a custom backimage is specified, we can't mask the label
      // Otherwise there might be weird effect due to our mask not being the same as the image
      return labelElement;
    }

    return (
      <MaskedView
        maskElement={
          <View style={styles.iconMaskContainer}>
            <Image
              source={require('../assets/back-icon-mask.png')}
              style={styles.iconMask}
            />
            <View style={styles.iconMaskFillerRect} />
          </View>
        }
      >
        {labelElement}
      </MaskedView>
    );
  };

  const handlePress = () => onPress && requestAnimationFrame(onPress);

  return (
    <TouchableItem
      disabled={disabled}
      accessible
      accessibilityRole="button"
      accessibilityComponentType="button"
      accessibilityLabel={accessibilityLabel}
      accessibilityTraits="button"
      testID="header-back"
      delayPressIn={0}
      onPress={disabled ? undefined : handlePress}
      pressColor={pressColorAndroid}
      style={[styles.container, disabled && styles.disabled, style]}
      hitSlop={Platform.select({
        ios: undefined,
        default: { top: 16, right: 16, bottom: 16, left: 16 },
      })}
      borderless
    >
      <React.Fragment>
        {renderBackImage()}
        {renderLabel()}
      </React.Fragment>
    </TouchableItem>
  );
}
Example #21
Source File: BottomTabBar.tsx    From nlw2-proffy with MIT License 4 votes vote down vote up
export default function BottomTabBar({
  state,
  navigation,
  descriptors,
  activeBackgroundColor,
  activeTintColor,
  adaptive = true,
  allowFontScaling,
  inactiveBackgroundColor,
  inactiveTintColor,
  keyboardHidesTabBar = false,
  labelPosition,
  labelStyle,
  iconStyle,
  safeAreaInsets,
  showLabel,
  style,
  tabStyle,
}: Props) {
  const { colors } = useTheme();
  const buildLink = useLinkBuilder();

  const focusedRoute = state.routes[state.index];
  const focusedDescriptor = descriptors[focusedRoute.key];
  const focusedOptions = focusedDescriptor.options;

  const dimensions = useWindowDimensions();
  const isKeyboardShown = useIsKeyboardShown();

  const shouldShowTabBar =
    focusedOptions.tabBarVisible !== false &&
    !(keyboardHidesTabBar && isKeyboardShown);

  const visibilityAnimationConfigRef = React.useRef(
    focusedOptions.tabBarVisibilityAnimationConfig
  );

  React.useEffect(() => {
    visibilityAnimationConfigRef.current =
      focusedOptions.tabBarVisibilityAnimationConfig;
  });

  const [isTabBarHidden, setIsTabBarHidden] = React.useState(!shouldShowTabBar);

  const [visible] = React.useState(
    () => new Animated.Value(shouldShowTabBar ? 1 : 0)
  );

  React.useEffect(() => {
    const visibilityAnimationConfig = visibilityAnimationConfigRef.current;

    if (shouldShowTabBar) {
      const animation =
        visibilityAnimationConfig?.show?.animation === 'spring'
          ? Animated.spring
          : Animated.timing;

      animation(visible, {
        toValue: 1,
        useNativeDriver,
        duration: 250,
        ...visibilityAnimationConfig?.show?.config,
      }).start(({ finished }) => {
        if (finished) {
          setIsTabBarHidden(false);
        }
      });
    } else {
      setIsTabBarHidden(true);

      const animation =
        visibilityAnimationConfig?.hide?.animation === 'spring'
          ? Animated.spring
          : Animated.timing;

      animation(visible, {
        toValue: 0,
        useNativeDriver,
        duration: 200,
        ...visibilityAnimationConfig?.hide?.config,
      }).start();
    }
  }, [visible, shouldShowTabBar]);

  const [layout, setLayout] = React.useState({
    height: 0,
    width: dimensions.width,
  });

  const handleLayout = (e: LayoutChangeEvent) => {
    const { height, width } = e.nativeEvent.layout;

    setLayout((layout) => {
      if (height === layout.height && width === layout.width) {
        return layout;
      } else {
        return {
          height,
          width,
        };
      }
    });
  };

  const { routes } = state;
  const shouldUseHorizontalLabels = () => {
    if (labelPosition) {
      return labelPosition === 'beside-icon';
    }

    if (!adaptive) {
      return false;
    }

    if (layout.width >= 768) {
      // Screen size matches a tablet
      let maxTabItemWidth = DEFAULT_MAX_TAB_ITEM_WIDTH;

      const flattenedStyle = StyleSheet.flatten(tabStyle);

      if (flattenedStyle) {
        if (typeof flattenedStyle.width === 'number') {
          maxTabItemWidth = flattenedStyle.width;
        } else if (typeof flattenedStyle.maxWidth === 'number') {
          maxTabItemWidth = flattenedStyle.maxWidth;
        }
      }

      return routes.length * maxTabItemWidth <= layout.width;
    } else {
      const isLandscape = dimensions.width > dimensions.height;

      return isLandscape;
    }
  };

  const defaultInsets = useSafeArea();

  const insets = {
    top: safeAreaInsets?.top ?? defaultInsets.top,
    right: safeAreaInsets?.right ?? defaultInsets.right,
    bottom: safeAreaInsets?.bottom ?? defaultInsets.bottom,
    left: safeAreaInsets?.left ?? defaultInsets.left,
  };

  const paddingBottom = Math.max(
    insets.bottom - Platform.select({ ios: 4, default: 0 }),
    0
  );

  return (
    <Animated.View
      style={[
        styles.tabBar,
        {
          backgroundColor: colors.card,
          borderTopColor: colors.border,
        },
        {
          transform: [
            {
              translateY: visible.interpolate({
                inputRange: [0, 1],
                outputRange: [layout.height + paddingBottom, 0],
              }),
            },
          ],
          // Absolutely position the tab bar so that the content is below it
          // This is needed to avoid gap at bottom when the tab bar is hidden
          position: isTabBarHidden ? 'absolute' : (null as any),
        },
        {
          height: DEFAULT_TABBAR_HEIGHT + paddingBottom,
          paddingBottom,
          paddingHorizontal: Math.max(insets.left, insets.right),
        },
        style,
      ]}
      pointerEvents={isTabBarHidden ? 'none' : 'auto'}
    >
      <View style={styles.content} onLayout={handleLayout}>
        {routes.map((route, index) => {
          const focused = index === state.index;
          const { options } = descriptors[route.key];

          const onPress = () => {
            const event = navigation.emit({
              type: 'tabPress',
              target: route.key,
              canPreventDefault: true,
            });

            if (!focused && !event.defaultPrevented) {
              navigation.dispatch({
                ...CommonActions.navigate(route.name),
                target: state.key,
              });
            }
          };

          const onLongPress = () => {
            navigation.emit({
              type: 'tabLongPress',
              target: route.key,
            });
          };

          const label =
            options.tabBarLabel !== undefined
              ? options.tabBarLabel
              : options.title !== undefined
              ? options.title
              : route.name;

          const accessibilityLabel =
            options.tabBarAccessibilityLabel !== undefined
              ? options.tabBarAccessibilityLabel
              : typeof label === 'string'
              ? `${label}, tab, ${index + 1} of ${routes.length}`
              : undefined;

          return (
            <NavigationContext.Provider
              key={route.key}
              value={descriptors[route.key].navigation}
            >
              <NavigationRouteContext.Provider value={route}>
                <BottomTabItem
                  route={route}
                  focused={focused}
                  horizontal={shouldUseHorizontalLabels()}
                  onPress={onPress}
                  onLongPress={onLongPress}
                  accessibilityLabel={accessibilityLabel}
                  to={buildLink(route.name, route.params)}
                  testID={options.tabBarTestID}
                  allowFontScaling={allowFontScaling}
                  activeTintColor={activeTintColor}
                  inactiveTintColor={inactiveTintColor}
                  activeBackgroundColor={activeBackgroundColor}
                  inactiveBackgroundColor={inactiveBackgroundColor}
                  button={options.tabBarButton}
                  icon={options.tabBarIcon}
                  badge={options.tabBarBadge}
                  label={label}
                  showLabel={showLabel}
                  labelStyle={labelStyle}
                  iconStyle={iconStyle}
                  style={tabStyle}
                />
              </NavigationRouteContext.Provider>
            </NavigationContext.Provider>
          );
        })}
      </View>
    </Animated.View>
  );
}
Example #22
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 #23
Source File: InputSelect.tsx    From react-native-paper-form-builder with MIT License 4 votes vote down vote up
function InputSelect(props: InputSelectProps) {
  const {
    formState,
    field,
    textInputProps,
    options,
    CustomTextInput,
    onDismiss = () => {},
  } = props;
  const theme = useTheme();
  const errorMessage = formState.errors?.[field.name]?.message;
  const textColor = errorMessage ? theme.colors.error : theme.colors.text;
  const [visible, setVisible] = useState(false);
  const [width, setWidth] = useState(0);
  const [height, setHeight] = useState(0);
  const INPUT = CustomTextInput ?? TextInput;

  const styles = useMemo(
    () =>
      StyleSheet.create({
        textInputStyle: {
          color: textColor,
        },
        menuStyle: {
          minWidth: width,
          width: width,
          marginTop: height,
        },
      }),
    [height, textColor, theme.colors.onSurface, theme.colors.surface, width],
  );

  const onLayout = useCallback((event: LayoutChangeEvent) => {
    const {width: _width, height: _height} = event.nativeEvent.layout;
    setWidth(_width);
    setHeight(_height);
  }, []);

  return (
    <Fragment>
      <Menu
        visible={visible}
        onDismiss={() => setVisible(false)}
        style={styles.menuStyle}
        anchor={
          <TouchableRipple
            onPress={() => {
              Keyboard.dismiss();
              if (!textInputProps.disabled) {
                setVisible(true);
              }
            }}>
            <View pointerEvents={'none'} onLayout={onLayout}>
              <INPUT
                ref={field.ref}
                mode={'outlined'}
                error={errorMessage ? true : false}
                {...textInputProps}
                value={
                  options.find(({value}) => `${value}` === `${field.value}`)
                    ?.label
                }
                onFocus={() => {
                  Keyboard.dismiss();
                  if (!textInputProps.disabled) {
                    setVisible(true);
                  }
                }}
                style={[styles.textInputStyle, textInputProps?.style]}
              />
            </View>
          </TouchableRipple>
        }>
        {options.map(({label: _label, value: _value}, _index) => {
          return (
            <Fragment key={_value}>
              <Menu.Item
                title={_label}
                style={{width, minWidth: width, maxWidth: width}}
                onPress={() => {
                  field.onChange(`${_value}`);
                  setVisible(false);
                  !!onDismiss && onDismiss();
                }}
                titleStyle={{
                  color:
                    `${_value}` === `${field.value}`
                      ? theme.colors.primary
                      : theme.colors.text,
                }}
              />
              {_index < options.length - 1 && <Divider />}
            </Fragment>
          );
        })}
      </Menu>
      {errorMessage && <HelperText type={'error'}>{errorMessage}</HelperText>}
    </Fragment>
  );
}
Example #24
Source File: TextField.tsx    From react-native-jigsaw with MIT License 4 votes vote down vote up
render() {
    const {
      Icon,
      type = "underline",
      disabled = false,
      label,
      error = false,
      leftIconName,
      leftIconMode,
      rightIconName,
      assistiveText,
      underlineColor: underlineColorProp,
      multiline = false,
      numberOfLines = 4,
      style,
      theme: { colors, typography, roundness, disabledOpacity },
      render = (props) => <NativeTextInput {...props} />,
      ...rest
    } = this.props;

    const MINIMIZED_LABEL_Y_OFFSET = -(typography.caption.lineHeight + 4);
    const OUTLINE_MINIMIZED_LABEL_Y_OFFSET = -(16 * 0.5 + 4);
    const MAXIMIZED_LABEL_FONT_SIZE = typography.subtitle1.fontSize;
    const MINIMIZED_LABEL_FONT_SIZE = typography.caption.fontSize;

    const hasActiveOutline = this.state.focused || error;

    let inputTextColor,
      activeColor,
      underlineColor,
      borderColor,
      placeholderColor,
      containerStyle: StyleProp<ViewStyle>,
      backgroundColor,
      inputStyle: StyleProp<TextStyle>;

    inputTextColor = colors.strong;
    if (disabled) {
      activeColor = colors.light;
      placeholderColor = colors.light;
      borderColor = "transparent";
      underlineColor = "transparent";
      backgroundColor = colors.divider;
    } else {
      activeColor = error ? colors.error : colors.primary;
      placeholderColor = borderColor = colors.light;
      underlineColor = underlineColorProp;
      backgroundColor = colors.background;
    }

    if (rest.placeholderTextColor) {
      placeholderColor = rest.placeholderTextColor;
    }

    const { lineHeight, ...subtitle1 } = typography.subtitle1;

    inputStyle = {
      paddingVertical: 0,
      color: inputTextColor,
      paddingLeft:
        leftIconName && leftIconMode === "inset"
          ? ICON_SIZE + 12 + (type === "solid" ? 16 : 0)
          : 0,
      paddingRight: rightIconName ? ICON_SIZE + 16 + 4 : 12,
      ...subtitle1,
    };

    if (!multiline) {
      inputStyle.height = lineHeight;
    }

    let assistiveTextLeftMargin;
    if (type === "underline") {
      containerStyle = {
        borderTopLeftRadius: roundness,
        borderTopRightRadius: roundness,
        paddingBottom: 12,
        marginTop: 16,
      };

      if (leftIconName && leftIconMode === "outset") {
        assistiveTextLeftMargin = ICON_SIZE + 8;
      } else {
        assistiveTextLeftMargin = 0;
      }
    } else {
      containerStyle = {
        borderRadius: roundness,
        borderColor: hasActiveOutline ? activeColor : borderColor,
        borderWidth: 1,
        paddingTop: label ? 16 * 1.5 : 16,
        paddingBottom: label ? 16 * 0.5 : 16,
        opacity: disabled ? disabledOpacity : 1,
        backgroundColor,
      };

      if (leftIconName && leftIconMode === "inset") {
        assistiveTextLeftMargin = 16 + 4;
      } else if (leftIconName && leftIconMode === "outset") {
        assistiveTextLeftMargin = ICON_SIZE + 8 + 12;
      } else {
        assistiveTextLeftMargin = 12;
      }

      inputStyle.paddingHorizontal = 12;
    }

    if (leftIconName && leftIconMode === "outset") {
      containerStyle.marginLeft = ICON_SIZE + 8;
    }

    let leftIconColor;
    if (error) {
      leftIconColor = colors.error;
    } else if (this.state.focused) {
      leftIconColor = colors.primary;
    } else {
      leftIconColor = colors.light;
    }

    const leftIconProps = {
      size: 24,
      color: leftIconColor,
      name: leftIconName || "",
    };

    const leftIconStyle: ImageStyle = {
      position: "absolute",
      marginTop:
        type === "solid"
          ? MINIMIZED_LABEL_FONT_SIZE + 4
          : leftIconMode === "outset"
          ? 16
          : 0,
      marginLeft: leftIconMode === "inset" && type === "solid" ? 16 : 0,
    };

    const labelStyle = {
      ...typography.subtitle1,
      top: type === "solid" ? 16 : 0,
      left:
        leftIconName && leftIconMode === "inset"
          ? ICON_SIZE + (type === "solid" ? 16 : 12)
          : 0,
      transform: [
        {
          // Move label to top
          translateY: this.state.labeled.interpolate({
            inputRange: [0, 1],
            outputRange: [
              type === "solid"
                ? OUTLINE_MINIMIZED_LABEL_Y_OFFSET
                : MINIMIZED_LABEL_Y_OFFSET,
              0,
            ],
          }),
        },
        {
          // Make label smaller
          scale: this.state.labeled.interpolate({
            inputRange: [0, 1],
            outputRange: [
              MINIMIZED_LABEL_FONT_SIZE / MAXIMIZED_LABEL_FONT_SIZE,
              1,
            ],
          }),
        },
        {
          // Offset label scale since RN doesn't support transform origin
          translateX: this.state.labeled.interpolate({
            inputRange: [0, 1],
            outputRange: [
              -(1 - MINIMIZED_LABEL_FONT_SIZE / MAXIMIZED_LABEL_FONT_SIZE) *
                (this.state.labelLayout.width / 2),
              0,
            ],
          }),
        },
      ],
    };

    const { textStyles } = extractStyles(style);
    const inputStyles = applyStyles(
      [
        styles.input,
        inputStyle,
        type === "solid" ? { marginHorizontal: 12 } : {},
      ],
      textStyles
    );

    const {
      backgroundColor: bgColor,
      padding,
      paddingTop,
      paddingBottom,
      paddingLeft,
      paddingRight,
      borderRadius,
      borderWidth,
      borderTopWidth,
      borderRightWidth,
      borderBottomWidth,
      borderLeftWidth,
      borderColor: borderCol,
      ...styleProp
    } = StyleSheet.flatten(style || {}) as ViewStyle & { height?: number };

    return (
      <View style={[styles.container, styleProp]}>
        {leftIconName && leftIconMode === "outset" ? (
          <Icon {...leftIconProps} style={leftIconStyle} />
        ) : null}
        <View
          style={applyStyles([containerStyle], {
            height: style?.height,
            backgroundColor: bgColor,
            padding,
            paddingTop,
            paddingBottom,
            paddingLeft,
            paddingRight,
            borderRadius,
            borderWidth,
            borderTopWidth,
            borderRightWidth,
            borderBottomWidth,
            borderLeftWidth,
            borderColor: borderCol,
          })}
        >
          {type === "underline" ? (
            // When type === 'flat', render an underline
            <Animated.View
              style={[
                styles.underline,
                {
                  backgroundColor:
                    bgColor ||
                    (error
                      ? colors.error
                      : this.state.focused
                      ? activeColor
                      : underlineColor),
                  // Underlines is thinner when input is not focused
                  transform: [{ scaleY: this.state.focused ? 1 : 0.5 }],
                },
              ]}
            />
          ) : null}

          {label ? (
            // Position colored placeholder and gray placeholder on top of each other and crossfade them
            // This gives the effect of animating the color, but allows us to use native driver
            <View
              pointerEvents="none"
              style={[
                StyleSheet.absoluteFill,
                {
                  opacity:
                    // Hide the label in minimized state until we measure its width
                    this.state.value || this.state.focused
                      ? this.state.labelLayout.measured
                        ? 1
                        : 0
                      : 1,
                },
              ]}
            >
              <AnimatedText
                onLayout={(e: LayoutChangeEvent) =>
                  this.setState({
                    labelLayout: {
                      width: e.nativeEvent.layout.width,
                      measured: true,
                    },
                  })
                }
                style={[
                  styles.placeholder,
                  type === "solid" ? { paddingHorizontal: 12 } : {},
                  labelStyle,
                  {
                    color: placeholderColor,
                    opacity: this.state.labeled.interpolate({
                      inputRange: [0, 1],
                      outputRange: [hasActiveOutline ? 1 : 0, 0],
                    }),
                  },
                ]}
                numberOfLines={1}
              >
                {label}
              </AnimatedText>
              <AnimatedText
                style={[
                  styles.placeholder,
                  type === "solid" ? { paddingHorizontal: 12 } : {},
                  labelStyle,
                  {
                    color: placeholderColor,
                    opacity: hasActiveOutline ? this.state.labeled : 1,
                  },
                ]}
                numberOfLines={1}
              >
                {label}
              </AnimatedText>
            </View>
          ) : null}

          {leftIconName && leftIconMode === "inset" ? (
            <View
              style={{
                justifyContent: type === "solid" ? "center" : undefined,
              }}
            >
              <Icon {...leftIconProps} style={leftIconStyle} />
            </View>
          ) : null}

          {render({
            ref: (c: NativeTextInput) => {
              this._root = c;
            },
            onChange: this._handleChangeText,
            placeholder: label
              ? this.state.placeholder
              : this.props.placeholder,
            placeholderTextColor: placeholderColor,
            editable: !disabled,
            selectionColor: activeColor,
            multiline,
            numberOfLines,
            onFocus: this._handleFocus,
            onBlur: this._handleBlur,
            underlineColorAndroid: "transparent",
            style: inputStyles,
            ...rest,
            value: this.state.value,
          })}
        </View>
        {rightIconName ? (
          <Icon
            name={rightIconName}
            size={ICON_SIZE}
            color={colors.light}
            style={{
              position: "absolute",
              right: 16,
              marginTop: type === "solid" ? MINIMIZED_LABEL_FONT_SIZE + 4 : 16,
            }}
          />
        ) : null}

        {assistiveText ? (
          <Text
            style={[
              {
                color: error ? colors.error : colors.light,
                marginTop: 8,
                marginLeft: assistiveTextLeftMargin,
              },
            ]}
          >
            {assistiveText}
          </Text>
        ) : null}
      </View>
    );
  }
Example #25
Source File: DatePicker.tsx    From react-native-jigsaw with MIT License 4 votes vote down vote up
DatePicker: React.FC<Props> = ({
  Icon,
  style,
  theme: { colors, typography, roundness, disabledOpacity },
  date,
  onDateChange = () => {},
  defaultValue,
  disabled = false,
  mode = "date",
  format,
  type = "underline",
  leftIconName,
  rightIconName,
  leftIconMode = "inset",
  label,
  placeholder,
  ...props
}) => {
  const [value, setValue] = React.useState<any>(date || defaultValue);

  React.useEffect(() => {
    if (defaultValue != null) {
      setValue(defaultValue);
    }
  }, [defaultValue]);

  const [pickerVisible, setPickerVisible] = React.useState(false);
  const [labeled] = React.useState<Animated.Value>(
    new Animated.Value(date ? 0 : 1)
  );
  const [placeholder1, setPlaceholder1] = React.useState("");
  const [focused, setFocused] = React.useState<boolean>(false);
  const [labelLayout, setLabelLayout] = React.useState<{
    measured: Boolean;
    width: number;
  }>({ measured: false, width: 0 });

  const getValidDate = (): Date => {
    if (!value) {
      return new Date();
    }
    return typeof value?.getMonth === "function" ? value : new Date();
  };

  const formatDate = (): string => {
    if (!value) return "";
    let newDate = getValidDate();

    if (format) return dateFormat(newDate, format);

    if (mode === "time") {
      return `${newDate.toLocaleTimeString()}`;
    }

    if (mode === "datetime") {
      return `${newDate.toLocaleString()}`;
    }

    return `${
      MONTHS[newDate.getMonth()]
    } ${newDate.getDate()}, ${newDate.getFullYear()}`;
  };

  const toggleVisibility = async () => {
    setPickerVisible(!pickerVisible);
    focused ? _handleBlur() : _handleFocus();
  };
  const insets = useSafeAreaInsets();

  // const _restoreLabel = () =>
  //   Animated.timing(labeled, {
  //     toValue: 1,
  //     duration: FOCUS_ANIMATION_DURATION,
  //     useNativeDriver: true,
  //   }).start();

  // const _minmizeLabel = () =>
  //   Animated.timing(labeled, {
  //     toValue: 0,
  //     duration: BLUR_ANIMATION_DURATION,
  //     useNativeDriver: true,
  //   }).start();

  // const _showPlaceholder = () =>
  //   setTimeout(() => setPlaceholder1(placeholder || ""), 50);

  const _hidePlaceholder = () => {
    setPlaceholder1("");
  };

  React.useEffect(() => {
    setValue(date);
  }, [date]);

  React.useEffect(() => {
    if (value || focused || placeholder1) {
      // _minmizeLabel();
      Animated.timing(labeled, {
        toValue: 0,
        duration: BLUR_ANIMATION_DURATION,
        useNativeDriver: true,
      }).start();
    } else {
      // _restoreLabel();
      Animated.timing(labeled, {
        toValue: 1,
        duration: FOCUS_ANIMATION_DURATION,
        useNativeDriver: true,
      }).start();
    }
  }, [value, focused, placeholder1, labeled]);

  React.useEffect(() => {
    const _showPlaceholder = () =>
      setTimeout(() => setPlaceholder1(placeholder || ""), 50);
    if (focused || !label) {
      _showPlaceholder();
    } else {
      _hidePlaceholder();
    }
    return () => {
      clearTimeout(_showPlaceholder());
    };
  }, [focused, label, placeholder]);

  const _handleFocus = () => {
    if (disabled) {
      return;
    }

    setFocused(true);
  };

  const _handleBlur = () => {
    if (disabled) {
      return;
    }
    setFocused(false);
  };

  const MINIMIZED_LABEL_Y_OFFSET = -(typography.caption.lineHeight + 4);
  const OUTLINE_MINIMIZED_LABEL_Y_OFFSET = -(16 * 0.5 + 4);
  const MAXIMIZED_LABEL_FONT_SIZE = typography.subtitle1.fontSize;
  const MINIMIZED_LABEL_FONT_SIZE = typography.caption.fontSize;

  const hasActiveOutline = focused;

  let inputTextColor,
    activeColor,
    underlineColor,
    borderColor,
    placeholderColor,
    containerStyle: StyleProp<ViewStyle>,
    backgroundColor,
    inputStyle: StyleProp<TextStyle>;

  inputTextColor = colors.strong;
  if (disabled) {
    activeColor = colors.light;
    placeholderColor = colors.light;
    borderColor = "transparent";
    underlineColor = "transparent";
    backgroundColor = colors.divider;
  } else {
    activeColor = colors.primary;
    placeholderColor = borderColor = colors.light;
    underlineColor = colors.light;
    backgroundColor = colors.background;
  }

  const { lineHeight, ...subtitle1 } = typography.subtitle1;

  inputStyle = {
    paddingVertical: 0,
    color: inputTextColor,
    paddingLeft:
      leftIconName && leftIconMode === "inset"
        ? ICON_SIZE + (type === "solid" ? 16 : 12)
        : 0,
    paddingRight: rightIconName ? ICON_SIZE + 16 + 4 : 12,
    ...subtitle1,
    height: lineHeight,
  };

  if (type === "underline") {
    containerStyle = {
      borderTopLeftRadius: roundness,
      borderTopRightRadius: roundness,
      paddingBottom: 12,
      marginTop: 16,
    };
  } else {
    containerStyle = {
      borderRadius: roundness,
      borderColor: hasActiveOutline ? activeColor : borderColor,
      borderWidth: 1,
      paddingTop: labeled ? 16 * 1.5 : 16,
      paddingBottom: labeled ? 16 * 0.5 : 16,
      opacity: disabled ? disabledOpacity : 1,
      backgroundColor,
    };

    inputStyle.paddingHorizontal = 12;
  }

  if (leftIconName && leftIconMode === "outset") {
    containerStyle.marginLeft = ICON_SIZE + 8;
  }

  let leftIconColor;
  if (focused) {
    leftIconColor = colors.primary;
  } else {
    leftIconColor = colors.light;
  }

  const leftIconProps = {
    size: 24,
    color: leftIconColor,
    name: leftIconName || "",
  };

  const leftIconStyle: ImageStyle = {
    position: "absolute",
    marginTop:
      type === "solid"
        ? leftIconMode === "inset"
          ? MINIMIZED_LABEL_FONT_SIZE + 4
          : 16
        : leftIconMode === "outset"
        ? 16
        : 0,
  };

  const labelStyle = {
    ...typography.subtitle1,
    top: type === "solid" ? 16 : 0,
    left:
      leftIconName && leftIconMode === "inset"
        ? ICON_SIZE + (type === "solid" ? 16 : 12)
        : 0,
    transform: [
      {
        // Move label to top
        translateY: labeled.interpolate({
          inputRange: [0, 1],
          outputRange: [
            type === "solid"
              ? OUTLINE_MINIMIZED_LABEL_Y_OFFSET
              : MINIMIZED_LABEL_Y_OFFSET,
            0,
          ],
        }),
      },
      {
        // Make label smaller
        scale: labeled.interpolate({
          inputRange: [0, 1],
          outputRange: [
            MINIMIZED_LABEL_FONT_SIZE / MAXIMIZED_LABEL_FONT_SIZE,
            1,
          ],
        }),
      },
      {
        // Offset label scale since RN doesn't support transform origin
        translateX: labeled.interpolate({
          inputRange: [0, 1],
          outputRange: [
            -(1 - MINIMIZED_LABEL_FONT_SIZE / MAXIMIZED_LABEL_FONT_SIZE) *
              (labelLayout.width / 2),
            0,
          ],
        }),
      },
    ],
  };

  const inputStyles = [
    styles.input,
    inputStyle,
    type === "solid" ? { marginHorizontal: 12 } : {},
  ];

  // const render = (props) => <NativeTextInput {...props} />;

  return (
    <View style={[styles.container, style]}>
      <Touchable disabled={disabled} onPress={toggleVisibility}>
        <View pointerEvents="none">
          <View style={[styles.container, style]}>
            {leftIconName && leftIconMode === "outset" ? (
              <Icon {...leftIconProps} style={leftIconStyle} />
            ) : null}
            <View
              style={[containerStyle, style ? { height: style.height } : {}]}
            >
              {type === "underline" ? (
                // When type === 'flat', render an underline
                <Animated.View
                  style={[
                    styles.underline,
                    {
                      backgroundColor: focused ? activeColor : underlineColor,
                      // Underlines is thinner when input is not focused
                      transform: [{ scaleY: focused ? 1 : 0.5 }],
                    },
                  ]}
                />
              ) : null}

              {label ? (
                // Position colored placeholder and gray placeholder on top of each other and crossfade them
                // This gives the effect of animating the color, but allows us to use native driver
                <View
                  pointerEvents="none"
                  style={[
                    StyleSheet.absoluteFill,
                    {
                      opacity:
                        // Hide the label in minimized state until we measure its width
                        date || focused ? (labelLayout.measured ? 1 : 0) : 1,
                    },
                  ]}
                >
                  <AnimatedText
                    onLayout={(e: LayoutChangeEvent) =>
                      setLabelLayout({
                        width: e.nativeEvent.layout.width,
                        measured: true,
                      })
                    }
                    style={[
                      styles.placeholder,
                      type === "solid" ? { paddingHorizontal: 12 } : {},
                      labelStyle,
                      {
                        color: colors.light,
                        opacity: labeled.interpolate({
                          inputRange: [0, 1],
                          outputRange: [hasActiveOutline ? 1 : 0, 0],
                        }),
                      },
                    ]}
                    numberOfLines={1}
                  >
                    {label}
                  </AnimatedText>
                  <AnimatedText
                    style={[
                      styles.placeholder,
                      type === "solid" ? { paddingHorizontal: 12 } : {},
                      labelStyle,
                      {
                        color: placeholderColor,
                        opacity: hasActiveOutline ? labeled : 1,
                      },
                    ]}
                    numberOfLines={1}
                  >
                    {label}
                  </AnimatedText>
                </View>
              ) : null}

              {leftIconName && leftIconMode === "inset" ? (
                <Icon
                  {...leftIconProps}
                  style={{
                    ...leftIconStyle,
                    marginLeft: type === "solid" ? 16 : 0,
                  }}
                />
              ) : null}

              <NativeTextInput
                value={formatDate()}
                placeholder={label ? placeholder1 : placeholder}
                editable={!disabled}
                placeholderTextColor={placeholderColor}
                selectionColor={activeColor}
                onFocus={_handleFocus}
                onBlur={_handleBlur}
                underlineColorAndroid={"transparent"}
                style={inputStyles}
                {...props}
              />
            </View>
            {rightIconName ? (
              <Icon
                name={rightIconName}
                size={ICON_SIZE}
                color={colors.light}
                style={{
                  position: "absolute",
                  right: 16,
                  marginTop:
                    type === "solid" ? MINIMIZED_LABEL_FONT_SIZE + 4 : 16,
                }}
              />
            ) : null}
          </View>
        </View>
      </Touchable>
      {pickerVisible && (
        <Portal>
          <View
            style={[
              styles.picker,
              {
                backgroundColor: colors.divider,
              },
            ]}
          >
            <View
              style={[
                styles.pickerContainer,
                {
                  paddingTop: insets.top,
                  paddingBottom: insets.bottom,
                  paddingLeft: insets.left,
                  paddingRight: insets.right,
                },
              ]}
            >
              <DateTimePicker
                value={getValidDate()}
                mode={mode}
                isVisible={pickerVisible}
                toggleVisibility={toggleVisibility}
                onChange={(_event: any, data: any) => {
                  toggleVisibility();
                  setValue(data);
                  onDateChange(data);
                }}
              />
            </View>
          </View>
        </Portal>
      )}
    </View>
  );
}
Example #26
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 #27
Source File: index.tsx    From react-native-expandable-list-view with MIT License 4 votes vote down vote up
ExpandableListView: React.FC<Props> = ({data,innerItemLabelStyle,renderItemSeparator,renderInnerItemSeparator,onInnerItemClick,onItemClick,defaultLoaderStyles,itemSeparatorStyle,itemLabelStyle,itemImageIndicatorStyle,itemContainerStyle,innerItemSeparatorStyle,innerItemContainerStyle,customLoader,customChevron,animated=true,chevronColor, ExpandableListViewStyles}) => {
  const [state, dispatch] = useReducer(reducer, initialState);
  const CustomLoader = customLoader;
  useEffect(() => {
    if (state.selectedIndex >= 0) {
      if (state.animatedValues[state.selectedIndex] !== undefined) {
        if (state.selectedIndex !== state.lastSelectedIndex) {
          if (
            state.lastSelectedIndex >= 0 &&
            state.lastSelectedIndex < state.data.length
          ) {
            Animated.parallel([
              Animated.timing(state.animatedValues[state.lastSelectedIndex], {
                useNativeDriver: false,
                duration: 300,
                easing: Easing.linear,
                toValue: 0,
              }),
              Animated.timing(
                state.rotateValueHolder[state.lastSelectedIndex],
                {
                  toValue: 0,
                  duration: 300,
                  easing: Easing.linear,
                  useNativeDriver: true,
                },
              ),
            ]).start();
          }
          Animated.parallel([
            Animated.timing(state.animatedValues[state.selectedIndex], {
              useNativeDriver: false,
              duration: 300,
              easing: Easing.linear,
              toValue: state.height[state.selectedIndex],
            }),
            Animated.timing(state.rotateValueHolder[state.selectedIndex], {
              toValue: 1,
              duration: 300,
              easing: Easing.linear,
              useNativeDriver: true,
            }),
          ]).start();
        } else {
          Animated.parallel([
            Animated.timing(state.animatedValues[state.selectedIndex], {
              useNativeDriver: false,
              duration: 300,
              easing: Easing.linear,
              toValue:
                state.opened &&
                state.height !== undefined &&
                state.height[state.selectedIndex] !== undefined
                  ? state.height[state.selectedIndex]
                  : 0,
            }),
            Animated.timing(state.rotateValueHolder[state.selectedIndex], {
              toValue: state.opened ? 1 : 0,
              duration: 300,
              easing: Easing.linear,
              useNativeDriver: true,
            }),
          ]).start();
        }
        dispatch({type: 'set', lastSelectedIndex: state.selectedIndex});
      }
    } else {
      if (
        state.isMounted.length === state.data.length &&
        state.data.length > 0
      ) {
        Animated.timing(state.opacityValues, {
          toValue: 1,
          duration: 300,
          easing: Easing.linear,
          useNativeDriver: true,
        }).start();
      }
    }
  }, [
    state.data,
    state.height,
    state.opened,
    state.isMounted,
    state.opacityValues,
    state.animatedValues,
    state.rotateValueHolder,
    state.selectedIndex,
    state.lastSelectedIndex,
  ]);

  useEffect(() => {
    async function reset() {
      await dispatch({type: 'reset'});
      await dispatch({type: 'set', data: data});
    }
    reset();
  }, [data]);

  function handleLayout(evt: LayoutChangeEvent, index: number) {
    if (!state.isMounted[index] && evt.nativeEvent.layout.height !== 0) {
      let h = state.height;
      h[index] = evt.nativeEvent.layout.height;
      let m = state.isMounted;
      m[index] = true;
      let newAnimatedValues: Array<Animated.Value> = [...state.animatedValues];
      let newRotateValueHolder: Array<Animated.Value> = [
        ...state.rotateValueHolder,
      ];
      newAnimatedValues.push(new Animated.Value(0));
      newRotateValueHolder.push(new Animated.Value(0));

      dispatch({
        type: 'set',
        animatedValues: newAnimatedValues,
        rotateValueHolder: newRotateValueHolder,
        height: h,
        isMounted: m,
      });
    }
  }

  function updateLayout(updatedIndex: number) {
    dispatch({
      type: 'set',
      opened: updatedIndex === state.selectedIndex ? !state.opened : true,
      selectedIndex: updatedIndex,
    });

    if (onItemClick) {
      return onItemClick({index: updatedIndex});
    }
    return;
  }

  const List = useMemo(() => Animated.FlatList, []);

  function renderInnerItem(itemO: any, headerItem: Item, headerIndex: number) {
    let {item}: {item: InnerItem} = itemO;
    let {index}: {index: number} = itemO;

    let CustomComponent = item.customInnerItem;

    let container = {
      ...styles.content,
      ...innerItemContainerStyle,
      height: undefined,
    };
    innerItemLabelStyle = {
      ...styles.text,
      ...innerItemLabelStyle,
      height: undefined,
    };

    innerItemSeparatorStyle = {
      ...styles.innerItemSeparator,
      ...innerItemSeparatorStyle,
    };

    return (
      <>
        <TouchableOpacity
          activeOpacity={0.6}
          key={Math.random()}
          style={container}
          onPress={() =>
            onInnerItemClick &&
            onInnerItemClick({
              innerItemIndex: index,
              item: headerItem,
              itemIndex: headerIndex,
            })
          }>
          {CustomComponent !== undefined ? (
            CustomComponent
          ) : (
            <Text style={innerItemLabelStyle}>{item.name}</Text>
          )}
        </TouchableOpacity>
        {renderInnerItemSeparator !== undefined &&
          renderInnerItemSeparator &&
          index < headerItem.subCategory.length - 1 && (
            <View style={innerItemSeparatorStyle} />
          )}
      </>
    );
  }

  function renderItem({item, index}: ExpandableListItem) {

    itemContainerStyle = {
      ...styles.header,
      ...itemContainerStyle,
      height: undefined,
    };
    itemLabelStyle = {
      ...styles.headerText,
      ...itemLabelStyle,
    };
    itemImageIndicatorStyle = {
      height: 15,
      width: 15,
      marginHorizontal: 5,
      ...itemImageIndicatorStyle,
    };

    itemSeparatorStyle = {...styles.headerSeparator, ...itemSeparatorStyle};

    let CustomComponent = item.customItem;
    return (
      <Animated.View
        style={{
          height: undefined,
        }}>
        <TouchableOpacity
          activeOpacity={0.6}
          onPress={() => updateLayout(index)}
          style={itemContainerStyle}>
          {CustomComponent !== undefined ? (
            CustomComponent
          ) : (
            <>
              <Animated.Image
                source={
                  customChevron !== undefined
                    ? customChevron
                    : chevronColor !== undefined &&
                      chevronColor === 'white'
                    ? white_chevron
                    : black_chevron
                }
                resizeMethod="scale"
                resizeMode="contain"
                style={[
                  itemImageIndicatorStyle,
                  animated === undefined ||
                  (animated !== undefined && animated)
                    ? state.rotateValueHolder[index] !== undefined && {
                        transform: [
                          {
                            rotate: state.rotateValueHolder[index].interpolate({
                              inputRange: [0, 1],
                              outputRange: ['0deg', '90deg'],
                            }),
                          },
                        ],
                      }
                    : {
                        transform: [
                          {
                            rotate:
                              state.opened && index === state.selectedIndex
                                ? '90deg'
                                : '0deg',
                          },
                        ],
                      },
                ]}
              />

              <Text style={itemLabelStyle}>{item.categoryName}</Text>
            </>
          )}
        </TouchableOpacity>

        <Animated.View
          style={[
            animated === undefined ||
            (animated !== undefined && animated)
              ? // eslint-disable-next-line react-native/no-inline-styles
                {
                  height: !state.isMounted[index]
                    ? undefined
                    : state.animatedValues[index],
                  overflow: 'hidden',
                }
              : // eslint-disable-next-line react-native/no-inline-styles
                {
                  display:
                    state.opened && index === state.selectedIndex
                      ? 'flex'
                      : 'none',
                  overflow: 'hidden',
                },
          ]}
          onLayout={(evt: any) => handleLayout(evt, index)}>
          <FlatList
            style={{height: undefined}}
            contentContainerStyle={{height: undefined}}
            updateCellsBatchingPeriod={50}
            initialNumToRender={50}
            windowSize={50}
            maxToRenderPerBatch={50}
            keyExtractor={() => Math.random().toString()}
            listKey={String(Math.random())}
            data={item.subCategory}
            renderItem={(innerItem: any) =>
              renderInnerItem(innerItem, item, index)
            }
          />
        </Animated.View>

        {renderItemSeparator !== undefined &&
          renderItemSeparator &&
          (!state.opened || state.selectedIndex !== index) &&
          index < state.data.length - 1 && <View style={itemSeparatorStyle} />}
      </Animated.View>
    );
  }

  return (
    <>
    {animated && data.length >0 && state.isMounted[data.length -1] === undefined && (CustomLoader !== undefined ? CustomLoader : <ActivityIndicator style={defaultLoaderStyles} color="#94bfda" size="large" />)}

    <Animated.View
      style={[
        // eslint-disable-next-line react-native/no-inline-styles
        {
          opacity:
            animated === undefined ||
            (animated !== undefined && animated)
              ? state.isMounted.length === state.data.length &&
                data.length > 0
                ? state.opacityValues
                : 0
              : 1,
        },
        {...ExpandableListViewStyles},
        {height: animated && data.length >0 && state.isMounted[data.length -1] === undefined ? 0 : ExpandableListViewStyles?.height !== undefined ? ExpandableListViewStyles?.height : 'auto'},
      ]}>


      <List
        updateCellsBatchingPeriod={50}
        initialNumToRender={50}
        windowSize={50}
        maxToRenderPerBatch={50}
        keyExtractor={(_: any, itemIndex: number) => itemIndex.toString()}
        data={state.data}
        renderItem={(item: ExpandableListItem) => renderItem(item)}
      />
    </Animated.View>
    </>
  );
}
Example #28
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 #29
Source File: ExpansionPanel.tsx    From react-native-design-kit with MIT License 4 votes vote down vote up
export default function ExpansionPanel({
  visible = false,
  title,
  titleStyle,
  iconStartRotation = '-90deg',
  iconEndRotation = '0deg',
  animationDuration = 250,
  icon,
  subtitle,
  subtitleStyle,
  containerStyle,
  contentContainerStyle,
  children,
  onPress,
}: ExpansionPanelProps) {
  const width = useRef<number>();
  const animation = useRef(new Animated.Value(visible ? 1 : 0)).current;

  const handleLayout = useCallback((event: LayoutChangeEvent) => {
    width.current = event.nativeEvent.layout.width;
  }, []);

  const handleRenderPanel = useMemo(
    () => (
      <Touchable
        testID="panel"
        touchableType="normal"
        onLayout={handleLayout}
        style={StyleSheet.flatten([
          styles.container,
          containerStyle,
          styles.fixedContainer,
        ])}
        onPress={onPress}>
        <Animated.View
          style={StyleSheet.flatten([
            styles.iconContainer,
            {
              transform: [
                {
                  rotateZ: animation.interpolate({
                    inputRange: [0, 1],
                    outputRange: [iconStartRotation, iconEndRotation],
                  }),
                },
              ],
            },
          ])}>
          {icon || <Icon name="chevron-down" />}
        </Animated.View>
        <View style={styles.sectionTitle}>
          <View style={styles.titleContainer}>
            <Text style={titleStyle}>{title}</Text>
          </View>
          {subtitle ? (
            <View style={styles.subtitleContainer}>
              <Text
                style={StyleSheet.flatten([styles.subtitle, subtitleStyle])}>
                {subtitle}
              </Text>
            </View>
          ) : null}
        </View>
      </Touchable>
    ),
    [
      icon,
      iconStartRotation,
      iconEndRotation,
      titleStyle,
      title,
      subtitle,
      subtitleStyle,
      containerStyle,
      animation,
      onPress,
      handleLayout,
    ],
  );

  const handleRenderContent = useMemo(
    () => (
      <Collapse visible={visible} animationDuration={animationDuration}>
        <View
          style={StyleSheet.flatten([
            styles.contentContainer,
            contentContainerStyle,
            {width: width.current},
          ])}>
          {children}
        </View>
      </Collapse>
    ),
    [
      width.current,
      visible,
      animationDuration,
      contentContainerStyle,
      children,
    ],
  );

  const handleRunAnimation = useCallback(
    () =>
      Animated.timing(animation, {
        toValue: visible ? 1 : 0,
        duration: animationDuration,
        useNativeDriver: true,
      }).start(),
    [animation, visible, animationDuration],
  );

  useDidUpdate(handleRunAnimation, [handleRunAnimation]);

  return (
    <>
      {handleRenderPanel}
      {handleRenderContent}
    </>
  );
}