diff --git a/.changeset/rich-kings-vanish.md b/.changeset/rich-kings-vanish.md new file mode 100644 index 0000000000..0e7fb01d71 --- /dev/null +++ b/.changeset/rich-kings-vanish.md @@ -0,0 +1,5 @@ +--- +'@td-design/react-native': patch +--- + +使用useSafeState代替useState diff --git a/.changeset/smart-phones-stare.md b/.changeset/smart-phones-stare.md new file mode 100644 index 0000000000..4930b0c447 --- /dev/null +++ b/.changeset/smart-phones-stare.md @@ -0,0 +1,8 @@ +--- +'@td-design/react-native-skeleton': minor +'@td-design/react-native-rating': minor +'@td-design/react-native-tabs': minor +'@td-design/svgicon-cli': minor +--- + +perf: 优化多个组件 diff --git a/packages/hooks/src/useGetState/index.ts b/packages/hooks/src/useGetState/index.ts index 4bf6d511e8..3e18c51dbb 100644 --- a/packages/hooks/src/useGetState/index.ts +++ b/packages/hooks/src/useGetState/index.ts @@ -10,7 +10,7 @@ function useGetState(initialState: S | (() => S)): [S, Dispatch(): [ S | undefined, Dispatch>, - GetStateAction + GetStateAction, ]; function useGetState(initialState?: S) { const [state, setState] = useSafeState(initialState); diff --git a/packages/lego/src/bar-line/index.tsx b/packages/lego/src/bar-line/index.tsx index 205090695d..8ac491064e 100644 --- a/packages/lego/src/bar-line/index.tsx +++ b/packages/lego/src/bar-line/index.tsx @@ -199,16 +199,15 @@ function BarLine( border-radius: 7px; "> ${params[0]?.seriesName}:${params[0]?.data?.value || params[0]?.data} ${ - barUnit ?? params[0]?.data?.unit ?? '' - } + barUnit ?? params[0]?.data?.unit ?? '' + }
@@ -245,23 +244,21 @@ function BarLine(
${params[0]?.seriesName}:${params[0]?.data?.value || params[0]?.data} ${ - barUnit ?? params[0]?.data?.unit ?? '' - } + barUnit ?? params[0]?.data?.unit ?? '' + }
diff --git a/packages/lego/src/cylinder-shadow-bar/index.tsx b/packages/lego/src/cylinder-shadow-bar/index.tsx index cd1d84f175..4dd8c11db8 100644 --- a/packages/lego/src/cylinder-shadow-bar/index.tsx +++ b/packages/lego/src/cylinder-shadow-bar/index.tsx @@ -107,8 +107,8 @@ export default forwardRef( border-radius: 7px; ">
${params[0]?.seriesName}:${params[0]?.data?.value || params[0]?.data} ${ - unit ?? params[0]?.data?.unit ?? '' - } + unit ?? params[0]?.data?.unit ?? '' + } `; diff --git a/packages/lego/src/hooks/useBaseChartConfig.ts b/packages/lego/src/hooks/useBaseChartConfig.ts index 368da8b607..03eb71852e 100644 --- a/packages/lego/src/hooks/useBaseChartConfig.ts +++ b/packages/lego/src/hooks/useBaseChartConfig.ts @@ -63,9 +63,8 @@ export default function useBaseChartConfig(inModal = false, unit?: string) {
diff --git a/packages/lego/src/horizontal-bar/index.tsx b/packages/lego/src/horizontal-bar/index.tsx index 9375abaae6..7c94f7d008 100644 --- a/packages/lego/src/horizontal-bar/index.tsx +++ b/packages/lego/src/horizontal-bar/index.tsx @@ -83,15 +83,14 @@ export default forwardRef(
${params[0]?.seriesName}:${params[0]?.data?.value || params[0]?.data} ${ - unit ?? params[0]?.data?.unit ?? '' - } + unit ?? params[0]?.data?.unit ?? '' + } `; diff --git a/packages/lego/src/multi-horizontal-bar/index.tsx b/packages/lego/src/multi-horizontal-bar/index.tsx index 2ba7edb9f2..9022985c0c 100644 --- a/packages/lego/src/multi-horizontal-bar/index.tsx +++ b/packages/lego/src/multi-horizontal-bar/index.tsx @@ -97,9 +97,8 @@ export default forwardRef(
diff --git a/packages/lego/src/slice-bar/index.tsx b/packages/lego/src/slice-bar/index.tsx index 61146ce530..173373b901 100644 --- a/packages/lego/src/slice-bar/index.tsx +++ b/packages/lego/src/slice-bar/index.tsx @@ -96,15 +96,14 @@ export default forwardRef(
${params[0]?.seriesName}:${params[0]?.data?.value || params[0]?.data} ${ - unit ?? params[0]?.data?.unit ?? '' - } + unit ?? params[0]?.data?.unit ?? '' + } `; diff --git a/packages/react-native-rating/src/SwipeRating.tsx b/packages/react-native-rating/src/SwipeRating.tsx index bc715339ea..612e8cb3cd 100644 --- a/packages/react-native-rating/src/SwipeRating.tsx +++ b/packages/react-native-rating/src/SwipeRating.tsx @@ -1,4 +1,4 @@ -import React, { forwardRef } from 'react'; +import React, { forwardRef, useMemo } from 'react'; import { StyleSheet } from 'react-native'; import { PanGestureHandler } from 'react-native-gesture-handler'; import Animated from 'react-native-reanimated'; @@ -37,13 +37,15 @@ const SwipeRating = forwardRef( ratingFillColor, }); - const renderRatings = () => { - return Array(count) - .fill('') - .map((_, index) => ( - - )); - }; + const Ratings = useMemo( + () => + Array(count) + .fill('') + .map((_, index) => ( + + )), + [count, ratingBgColor, size, strokeColor] + ); const styles = StyleSheet.create({ content: { flexDirection: 'row', alignItems: 'center', width: count * size }, @@ -56,7 +58,7 @@ const SwipeRating = forwardRef( - {renderRatings()} + {Ratings} diff --git a/packages/react-native-rating/src/TapRating.tsx b/packages/react-native-rating/src/TapRating.tsx index a4ec737842..421949ed52 100644 --- a/packages/react-native-rating/src/TapRating.tsx +++ b/packages/react-native-rating/src/TapRating.tsx @@ -1,4 +1,4 @@ -import React, { forwardRef } from 'react'; +import React, { forwardRef, useMemo } from 'react'; import { StyleSheet } from 'react-native'; import { Flex, helpers, Text, Theme, useTheme } from '@td-design/react-native'; @@ -45,6 +45,22 @@ const TapRating = forwardRef( }, }); + const Ratings = useMemo( + () => + Array(count) + .fill('') + .map((_, index) => ( + = index + 1} + onSelectStarInPosition={handleSelect} + {...{ size, disabled, starStyle, selectedColor, unselectedColor, outRangeScale, activeOpacity }} + /> + )), + [count, disabled, outRangeScale, position, selectedColor, size, starStyle, unselectedColor, activeOpacity] + ); + return ( {showReview && ( @@ -53,17 +69,7 @@ const TapRating = forwardRef( )} - {Array(count) - .fill('') - .map((_, index) => ( - = index + 1} - onSelectStarInPosition={handleSelect} - {...{ size, disabled, starStyle, selectedColor, unselectedColor, outRangeScale, activeOpacity }} - /> - ))} + {Ratings} ); diff --git a/packages/react-native-skeleton/src/StaticBone.tsx b/packages/react-native-skeleton/src/StaticBone.tsx index db9f1fe467..93b8748319 100644 --- a/packages/react-native-skeleton/src/StaticBone.tsx +++ b/packages/react-native-skeleton/src/StaticBone.tsx @@ -12,6 +12,8 @@ export const StaticBone: FC = ({ boneStyle, animationType, bone backgroundColor: interpolateColor(animation.value, [0, 1], [boneColor!, highlightColor!]), }; }); + const styles = animationType === 'none' ? animatedStyle : [boneStyle, animatedStyle]; + return ; }; diff --git a/packages/react-native-skeleton/src/index.md b/packages/react-native-skeleton/src/index.md index 2d35d631b7..e914be1a4b 100644 --- a/packages/react-native-skeleton/src/index.md +++ b/packages/react-native-skeleton/src/index.md @@ -116,3 +116,31 @@ export type AnimationDirection = | undefined; ``` + +_关于`styles`属性的说明:_ + +- `styles`属性是一个数组,数组的每一项是一个`ViewStyle`对象,用于描述骨架屏的样式,数组的每一项对应骨架屏的一行,数组的长度决定了骨架屏的行数。 +- `styles`的样式最好跟里面元素的样式保持一致或者近似,否则会出现骨架效果跟实际效果不一致的情况。比如: + +```tsx + + + hello world + + + hello world + +; + +const styles = StyleSheet.create({ + box1: { + width: 200, + height: 50, + }, + box2: { + width: 300, + height: 120, + marginTop: 20, + }, +}); +``` diff --git a/packages/react-native-skeleton/src/index.tsx b/packages/react-native-skeleton/src/index.tsx index 643ecc4cee..fc953697b5 100644 --- a/packages/react-native-skeleton/src/index.tsx +++ b/packages/react-native-skeleton/src/index.tsx @@ -1,4 +1,4 @@ -import React, { FC, ReactNode, useCallback } from 'react'; +import React, { FC, useEffect, useMemo } from 'react'; import { ReactElement } from 'react'; import { LayoutChangeEvent, ViewStyle } from 'react-native'; import Animated, { Easing, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated'; @@ -8,9 +8,37 @@ import { useSafeState } from '@td-design/rn-hooks'; import { calc } from './helper'; import { ShiverBone } from './ShiverBone'; import { StaticBone } from './StaticBone'; -import { SkeletonProps } from './type'; +import { AnimationType, SkeletonProps } from './type'; const DEFAULT_BORDER_RADIUS = 4; + +const getBoneStyles = ( + style: ViewStyle, + size: { width: number; height: number }, + animationType: AnimationType, + boneColor: string +) => { + const boneWidth = + (typeof style.width === 'string' && style.width.includes('%') ? calc(size.width, style.width) : style.width) ?? 0; + + const boneHeight = + (typeof style.height === 'string' && style.height.includes('%') ? calc(size.height, style.height) : style.height) ?? + 0; + + const boneStyle = { + width: boneWidth, + height: boneHeight, + borderRadius: style.borderRadius || DEFAULT_BORDER_RADIUS, + ...style, + }; + + if (animationType !== 'pulse') { + boneStyle.overflow = 'hidden'; + boneStyle.backgroundColor = style.backgroundColor || boneColor; + } + return boneStyle; +}; + const Skeleton: FC = ({ containerStyle, easing = Easing.bezierFn(0.5, 0, 0.25, 1), @@ -24,63 +52,45 @@ const Skeleton: FC = ({ children, }) => { const [size, setSize] = useSafeState({ width: 0, height: 0 }); - const loadingValue = useSharedValue(+loading); const animationValue = useSharedValue(0); - const shiverValue = useSharedValue(animationType === 'shiver' ? 1 : 0); - const onLayout = useCallback((e: LayoutChangeEvent) => { + const onLayout = (e: LayoutChangeEvent) => { const { width, height } = e.nativeEvent.layout; setSize({ width, height }); - }, []); + }; - if (loadingValue.value === 1) { - if (shiverValue.value === 1) { - animationValue.value = withRepeat(withTiming(1, { duration, easing }), -1, false); - } else { - animationValue.value = withRepeat(withTiming(1, { duration: duration / 2, easing }), -1, true); - } - } + useEffect(() => { + // 重置动画 + animationValue.value = 0; - const getBoneStyles = (style: ViewStyle) => { - const { backgroundColor, borderRadius } = style; - const boneWidth = - (typeof style.width === 'string' && style.width.includes('%') ? calc(size.width, style.width) : style.width) ?? 0; - const boneHeight = - (typeof style.height === 'string' && style.height.includes('%') - ? calc(size.height, style.height) - : style.height) ?? 0; - - const boneStyle = { - width: boneWidth, - height: boneHeight, - borderRadius: borderRadius || DEFAULT_BORDER_RADIUS, - ...style, - }; - if (animationType !== 'pulse') { - boneStyle.overflow = 'hidden'; - boneStyle.backgroundColor = backgroundColor || boneColor; + if (loading) { + if (animationType === 'shiver') { + animationValue.value = withRepeat(withTiming(1, { duration, easing }), -1, false); + } else { + animationValue.value = withRepeat(withTiming(1, { duration: duration / 2, easing }), -1, true); + } } - return boneStyle; - }; + }, [loading, animationType]); - const getBones = (styles: ViewStyle[] = [], children: ReactNode, prefix = ''): ReactNode => { + const Bones = useMemo(() => { if (styles.length > 0) { return styles.map((style, i) => { - const boneStyle = getBoneStyles(style); - const _prefix = prefix ? `${prefix}_${i}` : `${i}`; + const boneStyle = getBoneStyles(style, size, animationType, boneColor); + if (animationType === 'pulse' || animationType === 'none') { return ( ); } + return ( = ({ ); }); } + return React.Children.map(children, (child, i) => { const style = (child as ReactElement).props.style || {}; - const boneStyle = getBoneStyles(style); + const boneStyle = getBoneStyles(style, size, animationType, boneColor); + if (animationType === 'pulse' || animationType === 'none') { return ( = ({ /> ); }); - }; + }, [styles, size, animationType, animationDirection, boneColor, highlightColor, animationValue]); return ( - {loading ? getBones(styles, children) : children} + {loading ? Bones : children} ); }; diff --git a/packages/react-native-tabs/src/AnimatedPagerView.tsx b/packages/react-native-tabs/src/AnimatedPagerView.tsx deleted file mode 100644 index 013dd49ada..0000000000 --- a/packages/react-native-tabs/src/AnimatedPagerView.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, { DependencyList, forwardRef, PropsWithChildren } from 'react'; -import PagerView from 'react-native-pager-view'; -import Animated, { runOnJS, useEvent, useHandler } from 'react-native-reanimated'; - -import { AnimatedPagerViewProps } from './type'; -import usePagerView from './usePagerView'; - -const AnimatedPagerView = Animated.createAnimatedComponent(PagerView); - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type Obj = Record; - -export function usePagerScrollHandler( - handlers: Record void>, - dependencies?: DependencyList -) { - const { context, doDependenciesDiffer } = useHandler(handlers, dependencies); - const subscribeForEvents = ['onPageScroll']; - - return useEvent( - event => { - 'worklet'; - const { onPageScroll } = handlers; - if (onPageScroll && event.eventName.endsWith('onPageScroll')) { - onPageScroll(event, context); - } - }, - subscribeForEvents, - doDependenciesDiffer - ); -} - -export default forwardRef>( - ({ scrollEnabled, overdrag, keyboardDismissMode, children, onPageSelected }, ref) => { - const { onPageScroll: _onPageScroll, page, onPageScrollStateChanged } = usePagerView.useModel(); - - const handler = usePagerScrollHandler({ - onPageScroll: (e: { offset: number; position: number }) => { - 'worklet'; - runOnJS(_onPageScroll)(e); - }, - }); - - return ( - - {children} - - ); - } -); diff --git a/packages/react-native-tabs/src/SceneView.tsx b/packages/react-native-tabs/src/SceneView.tsx deleted file mode 100644 index dfd76ddfc1..0000000000 --- a/packages/react-native-tabs/src/SceneView.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { useEffect } from 'react'; -import { StyleSheet } from 'react-native'; - -import { Box } from '@td-design/react-native'; -import { useSafeState } from '@td-design/rn-hooks'; - -import { SceneViewProps } from './type'; -import usePagerView from './usePagerView'; - -export default function SceneView({ index, lazy, layout, children }: SceneViewProps) { - const { addEnterListener, page } = usePagerView.useModel(); - - const [isLoading, setLoading] = useSafeState(Math.abs(page - index) > 0); - const focused = page === index; - - // Always render the route when it becomes focused - if (isLoading && Math.abs(page - index) <= 0) { - setLoading(false); - } - - useEffect(() => { - const handleEnter = (value: number) => { - if (value === index) { - setLoading(prev => !prev); - } - }; - - let unsubscribe: (() => void) | undefined; - let timer: ReturnType; - - if (lazy && isLoading) { - unsubscribe = addEnterListener(handleEnter); - } else if (isLoading) { - timer = setTimeout(() => { - setLoading(false); - }, 0); - } - - return () => { - unsubscribe?.(); - clearTimeout(timer); - }; - }, []); - - return ( - - {focused || layout.width ? children({ loading: isLoading }) : null} - - ); -} diff --git a/packages/react-native-tabs/src/ScrollBar.tsx b/packages/react-native-tabs/src/ScrollBar.tsx index 2872ad83bc..f3c723c031 100644 --- a/packages/react-native-tabs/src/ScrollBar.tsx +++ b/packages/react-native-tabs/src/ScrollBar.tsx @@ -1,7 +1,7 @@ -import React, { PropsWithChildren, ReactElement, useEffect, useRef } from 'react'; -import { LayoutChangeEvent, LayoutRectangle, ScrollView } from 'react-native'; +import React, { PropsWithChildren, useEffect, useRef } from 'react'; +import { LayoutChangeEvent, LayoutRectangle, ScrollView, StyleSheet } from 'react-native'; -import { useMemoizedFn, useSafeState } from '@td-design/rn-hooks'; +import { useSafeState } from '@td-design/rn-hooks'; interface ScrollBarProps { height: number; @@ -9,52 +9,64 @@ interface ScrollBarProps { } export default function ScrollBar({ height, page, children }: PropsWithChildren) { - const scrollRef = useRef(null); - - // 记录 Tab 布局数据 const [tabLayouts, setTabLayouts] = useSafeState([]); - const onTabsLayout = useMemoizedFn((layouts: LayoutRectangle[]) => { + + // 保存每个Tab的布局 + const handleTabLayout = (layouts: LayoutRectangle[]) => { setTabLayouts(layouts); - }); + }; - // 记录 ScrollView 的内容宽度 + // 保存ScrollView的宽度 const [contentWidth, setContentWidth] = useSafeState(0); - const onContentSizeChange = useMemoizedFn((w: number) => { - setContentWidth(w); - }); + const handleContentChange = (width: number) => { + setContentWidth(width); + }; - // 记录 ScrollBar 的宽度 + // 保存滚动条的宽度 const [scrollBarWidth, setScrollBarWidth] = useSafeState(0); - const onLayout = useMemoizedFn((e: LayoutChangeEvent) => { + const handleScrollBarLayout = (e: LayoutChangeEvent) => { setScrollBarWidth(e.nativeEvent.layout.width); - }); + }; + + const scrollViewRef = useRef(null); useEffect(() => { if (tabLayouts.length - 1 < page || contentWidth === 0 || scrollBarWidth === 0) return; - // 获得选中的 Tab 布局数据 + // 当前选中的Tab的布局 const tabLayout = tabLayouts[page]; - // 计算 Tab 中心到 ScrollBar 中心的 x 轴距离 - const dx = tabLayout.x + tabLayout.width / 2 - scrollBarWidth / 2; - // 计算出 ScrollView 的最大可滚动距离,ScrollView 的可滚动范围是 [0, maxScrollX] + + // 当前选中的Tab的中心点 + const tabCenter = tabLayout.x + tabLayout.width / 2 - scrollBarWidth / 2; + + // 计算ScrollView的最大可滚动距离[0, maxScrollX] const maxScrollX = contentWidth - scrollBarWidth; - // 计算出 ScrollView 应该滚动到的 x 坐标,它必须大于等于 0 并且小于等于 maxScrollX - const x = Math.min(Math.max(0, dx), maxScrollX); - scrollRef.current?.scrollTo({ x }); + + // 计算ScrollView应该滚动的x坐标位置,它必须在[0, maxScrollX]之间 + const scrollX = Math.min(Math.max(0, tabCenter), maxScrollX); + + // 滚动ScrollView + scrollViewRef.current?.scrollTo({ x: scrollX, animated: true }); }, [page, tabLayouts, contentWidth, scrollBarWidth]); return ( - {React.cloneElement(children as ReactElement, { onTabsLayout, height })} + {React.cloneElement(children as React.ReactElement, { onTabsLayout: handleTabLayout })} ); } + +const styles = StyleSheet.create({ + scrollbar: { + flexGrow: 0, + }, +}); diff --git a/packages/react-native-tabs/src/TabBar.tsx b/packages/react-native-tabs/src/TabBar.tsx index d7065f31b3..58ee8a6dfc 100644 --- a/packages/react-native-tabs/src/TabBar.tsx +++ b/packages/react-native-tabs/src/TabBar.tsx @@ -1,123 +1,188 @@ -import React, { useMemo, useRef } from 'react'; -import { LayoutRectangle } from 'react-native'; -import { Extrapolate, interpolate, useAnimatedStyle } from 'react-native-reanimated'; +import React, { useEffect, useMemo, useRef } from 'react'; +import { + Animated, + LayoutChangeEvent, + LayoutRectangle, + Platform, + StyleProp, + StyleSheet, + TextStyle, + ViewStyle, +} from 'react-native'; -import { Flex, helpers, Theme, useTheme } from '@td-design/react-native'; +import { Flex, helpers } from '@td-design/react-native'; import { useMemoizedFn, useSafeState } from '@td-design/rn-hooks'; import TabBarIndicator from './TabBarIndicator'; import TabBarItem from './TabBarItem'; -import { TabBarProps } from './type'; -const { px, ONE_PIXEL } = helpers; +const { ONE_PIXEL, deviceWidth } = helpers; + +export interface TabBarProps { + tabs: string[]; + height?: number; + onTabPress: (index: number) => void; + onTabsLayout?: (layouts: LayoutRectangle[]) => void; + page: number; + position: Animated.Value; + offset: Animated.Value; + isIdle: boolean; + showIndicator?: boolean; + scrollState: 'idle' | 'dragging' | 'settling'; + tabStyle?: StyleProp; + tabItemStyle?: StyleProp; + labelStyle?: StyleProp; + indicatorStyle?: StyleProp; +} export default function TabBar({ tabs, - page, - height, onTabPress, onTabsLayout, - showIndicator, - scrollX, + height, + position, + offset, + page, isIdle, + scrollState, + showIndicator = true, tabStyle, tabItemStyle, labelStyle, indicatorStyle, }: TabBarProps) { - const theme = useTheme(); - - // 给indicatorStyle赋初始值 - indicatorStyle = useMemo( - () => ({ - height: px(4), - borderRadius: px(2), - color: theme.colors.primary200, - ...indicatorStyle, - }), - [indicatorStyle, theme.colors.primary200] - ); + const layouts = useRef([]); + const indicatorWidth = getIndicatorWidth(indicatorStyle); - const layouts = useRef([]).current; - const inputRange = useMemo(() => tabs.map((_, index) => index), [tabs]); + const inputRange = useMemo(() => tabs.map((_, i) => i), [tabs]); + const [outputRange, setOutputRange] = useSafeState(inputRange.map(() => 0)); - const [tabWidths, setTabWidths] = useSafeState(inputRange.map(() => 0)); - const [scrollRange, setScrollRange] = useSafeState(inputRange.map(() => 0)); + const offsetPosition = Animated.add(position, offset); - // 保存每个 Tab 的布局信息 - const handleTabLayout = useMemoizedFn((index: number, layout: LayoutRectangle) => { - layouts[index] = layout; + const scrollX = offsetPosition.interpolate({ + inputRange, + outputRange, + extrapolate: 'clamp', + }); - const length = layouts.filter(layout => layout.width > 0).length; + const lastPage = useLastPage(page, isIdle); + const interactive = useInteractive(scrollState); + + const handleTabPress = useMemoizedFn((index: number) => { + if (isIdle) { + onTabPress(index); + } + }); + + const handleTabLayout = useMemoizedFn((e: LayoutChangeEvent, index: number) => { + layouts.current[index] = e.nativeEvent.layout; + + const length = layouts.current.filter(layout => layout.width > 0).length; if (length !== tabs.length) return; - const widths: number[] = []; const range: number[] = []; for (let index = 0; index < length; index++) { - const { x, width } = layouts[index]; - // 我们希望指示器和所选 Tab 垂直居中对齐 - // 那么指示器的 x 轴偏移量就是 Tab 的 center.x - 指示器的 center.x - const tabCenterX = x + width / 2; - const indicatorCenterX = width / 2; - range.push(tabCenterX - indicatorCenterX); - widths.push(width); - } - setTabWidths(widths); - setScrollRange(range); - onTabsLayout?.(layouts); - }); + const layout = layouts.current[index]; - const handleTabPress = useMemoizedFn((index: number) => { - if (isIdle) { - onTabPress?.(index); + // 指示器要和当前Tab垂直居中对齐 + const tabCenterX = layout.x + layout.width / 2; + const indicatorX = tabCenterX - indicatorWidth / 2; + range.push(indicatorX); } + + setOutputRange(range); + onTabsLayout?.(layouts.current); }); return ( a + b, 0)} + minWidth={deviceWidth} height={height} - flex={1} justifyContent={'space-evenly'} - alignItems="center" + alignItems={'center'} backgroundColor={'white'} - borderBottomWidth={ONE_PIXEL} borderBottomColor={'border'} + borderBottomWidth={ONE_PIXEL} style={tabStyle} > {tabs.map((tab, index) => { - const enhanced = index === page; - const inputRange = [index - 1, index, index + 1]; + const enhanced = interactive || index === page || index === lastPage; - const animatedStyles = useAnimatedStyle(() => { - const scale = interpolate(scrollX.value, inputRange, [1, enhanced ? 1.2 : 1, 1], Extrapolate.CLAMP); - const opacity = interpolate(scrollX.value, inputRange, [0.8, enhanced ? 1 : 0.8, 0.8], Extrapolate.CLAMP); + let scale = offsetPosition.interpolate({ + inputRange: [index - 1, index, index + 1], + outputRange: [1, enhanced ? 1.2 : 1, 1], + extrapolate: 'clamp', + }); - return { - opacity, - transform: [{ scale }], - }; + let opacity = offsetPosition.interpolate({ + inputRange: [index - 1, index, index + 1], + outputRange: [0.8, enhanced ? 1 : 0.8, 0.8], + extrapolate: 'clamp', }); + + if (Platform.OS === 'ios' && Math.abs(page - lastPage) > 1 && index === lastPage) { + scale = offsetPosition.interpolate({ + inputRange: [page - 1, page, page + 1], + outputRange: [1.2, 1, 1.2], + extrapolate: 'clamp', + }); + + opacity = offsetPosition.interpolate({ + inputRange: [page - 1, page, page + 1], + outputRange: [1, 0.8, 1], + extrapolate: 'clamp', + }); + } + return ( handleTabPress(index)} - onLayout={event => handleTabLayout(index, event.nativeEvent.layout)} - style={[tabItemStyle, animatedStyles]} - labelStyle={[labelStyle]} + onLayout={e => handleTabLayout(e, index)} + style={tabItemStyle} + labelStyle={[labelStyle, { opacity, transform: [{ scale }] }]} /> ); })} - {showIndicator && ( - - )} + {showIndicator && } ); } + +const useLastPage = (page: number, isIdle: boolean) => { + const lastPage = useRef(0); + + useEffect(() => { + if (isIdle) { + lastPage.current = page; + } + }, [page, isIdle]); + + return lastPage.current; +}; + +const useInteractive = (scrollState: 'idle' | 'dragging' | 'settling') => { + const interactive = useRef(false); + const scrollStateRef = useRef(scrollState); + + useEffect(() => { + scrollStateRef.current = scrollState; + }, [scrollState]); + + if (scrollState === 'dragging') { + interactive.current = true; + } else if (scrollState === 'idle' && (Platform.OS === 'android' || scrollStateRef.current === 'settling')) { + interactive.current = false; + } + + return interactive.current; +}; + +function getIndicatorWidth(style?: StyleProp) { + const flattenedStyle = StyleSheet.flatten([{ width: 24 }, style]); + if (typeof flattenedStyle.width === 'number') { + return flattenedStyle.width; + } + return 24; +} diff --git a/packages/react-native-tabs/src/TabBarIndicator.tsx b/packages/react-native-tabs/src/TabBarIndicator.tsx index 513d6bf89f..05153da501 100644 --- a/packages/react-native-tabs/src/TabBarIndicator.tsx +++ b/packages/react-native-tabs/src/TabBarIndicator.tsx @@ -1,31 +1,38 @@ -import React from 'react'; -import { StyleSheet } from 'react-native'; -import Animated, { Extrapolate, interpolate, useAnimatedStyle } from 'react-native-reanimated'; +import React, { memo } from 'react'; +import { Animated, StyleProp, StyleSheet, ViewStyle } from 'react-native'; -import { TabBarIndicatorProps } from './type'; +import { Theme, useTheme } from '@td-design/react-native'; -export default function TabBarIndicator({ style, scrollX, inputRange, scrollRange, tabWidths }: TabBarIndicatorProps) { - const styles = StyleSheet.create({ - indicator: { - position: 'absolute', - left: 0, - bottom: 0, - width: 36, - height: style.height, - borderRadius: style.borderRadius, - backgroundColor: style.color, - }, - }); +function TabBarIndicator({ + style, + scrollX, +}: { + style: StyleProp; + scrollX: Animated.AnimatedInterpolation; +}) { + const theme = useTheme(); - const animatedStyles = useAnimatedStyle(() => { - const translateX = interpolate(scrollX.value, inputRange, scrollRange, Extrapolate.CLAMP); - const width = interpolate(scrollX.value, inputRange, tabWidths, Extrapolate.CLAMP); + return ( + + ); +} - return { - width, - transform: [{ translateX }], - }; - }); +export default memo(TabBarIndicator); - return ; -} +const styles = StyleSheet.create({ + indicator: { + position: 'absolute', + left: 0, + bottom: 0, + height: 4, + borderRadius: 2, + }, +}); diff --git a/packages/react-native-tabs/src/TabBarItem.tsx b/packages/react-native-tabs/src/TabBarItem.tsx index ee50166952..73835a2eea 100644 --- a/packages/react-native-tabs/src/TabBarItem.tsx +++ b/packages/react-native-tabs/src/TabBarItem.tsx @@ -1,37 +1,39 @@ -import React from 'react'; -import { Pressable } from 'react-native'; -import Animated from 'react-native-reanimated'; +import React, { memo } from 'react'; +import { Animated, Pressable, StyleProp, TextStyle, ViewProps, ViewStyle } from 'react-native'; import { helpers, Theme, useTheme } from '@td-design/react-native'; -import { TabBarItemProps } from './type'; - -const AnimatedPressable = Animated.createAnimatedComponent(Pressable); +interface TabBarItemProps { + title: string; + onPress?: () => void; + onLayout: ViewProps['onLayout']; + style?: StyleProp; + labelStyle?: Animated.WithAnimatedObject | Animated.WithAnimatedArray>; +} -export default function TabBarItem({ title, style, labelStyle, onPress, onLayout }: TabBarItemProps) { +const TabBarItem = ({ style, labelStyle, title, onLayout, onPress }: TabBarItemProps) => { const theme = useTheme(); - const renderText = () => { - if (typeof title === 'string') - return ( - - {title} - - ); - - return title(); - }; - return ( - - {renderText()} - + + {title} + + ); -} +}; + +export default memo(TabBarItem); diff --git a/packages/react-native-tabs/src/createShareModel.tsx b/packages/react-native-tabs/src/createShareModel.tsx deleted file mode 100644 index 068be3ca29..0000000000 --- a/packages/react-native-tabs/src/createShareModel.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { createContext, PropsWithChildren, ReactNode, useContext, useMemo } from 'react'; - -const EMPTY = Symbol('EMPTY'); - -type UseHook = ((props: Props) => Value) | (() => Value); -type ConsumerProps = { - children: (value: Value) => ReactNode; -}; - -/** - * 创建局部共享数据 - * @param useHook 自定义hooks - * @returns - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function createShareModel(useHook: UseHook) { - const Context = createContext(EMPTY); - const hookName = useHook.name || 'useHook'; - Context.displayName = `${hookName}Context`; - - const ShareModelProvider = ({ initialState, children }: PropsWithChildren<{ initialState: Props }>) => { - const value = useHook(initialState); - - // eslint-disable-next-line react-hooks/exhaustive-deps - return useMemo(() => {children}, [value]); - }; - - const useShareModel = () => { - const value = useContext(Context); - if (value === EMPTY) { - throw new Error('Component must be wrapped within Provider'); - } - return value; - }; - - const ShareModelConsumer = (props: ConsumerProps) => { - return ( - - {value => { - if (value === EMPTY) { - throw new Error('Component must be wrapped with '); - } - return props.children(value); - }} - - ); - }; - - return { - Provider: ShareModelProvider, - Consumer: ShareModelConsumer, - useModel: useShareModel, - }; -} diff --git a/packages/react-native-tabs/src/index.md b/packages/react-native-tabs/src/index.md index 987ee63994..78109419fb 100644 --- a/packages/react-native-tabs/src/index.md +++ b/packages/react-native-tabs/src/index.md @@ -22,9 +22,9 @@ yarn add react-native-pager-view @td-design/react-native-tabs ```tsx | pure const routes = [ - { key: 'first', title: 'First', component: }, - { key: 'second', title: 'Second', component: }, - { key: 'third', title: 'Third', component: }, + { title: 'First', component: }, + { title: 'Second', component: }, + { title: 'Third', component: }, ]; return ( @@ -46,9 +46,9 @@ return ( ```tsx | pure const routes = [ - { key: 'first', title: 'First', component: }, - { key: 'second', title: 'Second', component: }, - { key: 'third', title: 'Third', component: }, + { title: 'First', component: }, + { title: 'Second', component: }, + { title: 'Third', component: }, ]; return ( @@ -66,37 +66,13 @@ return ( /> -### 3. 自定义选项卡文本 +### 3. 自定义选项卡文本样式 ```tsx | pure const routes = [ - { key: 'first', title: () => First, component: }, - { key: 'second', title: () => Second, component: }, - { key: 'third', title: () => Third, component: }, -]; - -return ( - - - -); -``` - -
- -
- -### 4. 自定义选项卡文本样式 - -```tsx | pure -const routes = [ - { key: 'first', title: 'First', component: }, - { key: 'second', title: 'Second', component: }, - { key: 'third', title: 'Third', component: }, + { title: 'First', component: }, + { title: 'Second', component: }, + { title: 'Third', component: }, ]; return ( @@ -114,13 +90,13 @@ return ( /> -### 5. 自定义指示器样式 +### 4. 自定义指示器样式 ```tsx | pure const routes = [ - { key: 'first', title: 'First', component: }, - { key: 'second', title: 'Second', component: }, - { key: 'third', title: 'Third', component: }, + { title: 'First', component: }, + { title: 'Second', component: }, + { title: 'Third', component: }, ]; return ( @@ -138,13 +114,13 @@ return ( /> -### 6. 隐藏指示器 +### 5. 隐藏指示器 ```tsx | pure const routes = [ - { key: 'first', title: 'First', component: }, - { key: 'second', title: 'Second', component: }, - { key: 'third', title: 'Third', component: }, + { title: 'First', component: }, + { title: 'Second', component: }, + { title: 'Third', component: }, ]; return ( @@ -162,17 +138,17 @@ return ( /> -### 7. 很多个选项卡 +### 6. 很多个选项卡 ```tsx | pure const routes = [ - { key: 'first', title: 'First', component: }, - { key: 'second', title: 'Second', component: }, - { key: 'third', title: 'Third', component: }, - { key: 'forth', title: 'Forth', component: }, - { key: 'fifth', title: 'Fifth', component: }, - { key: 'sixth', title: 'Sixth', component: }, - { key: 'seventh', title: 'Seventh', component: }, + { title: 'First', component: }, + { title: 'Second', component: }, + { title: 'Third', component: }, + { title: 'Forth', component: }, + { title: 'Fifth', component: }, + { title: 'Sixth', component: }, + { title: 'Seventh', component: }, ]; return ( @@ -182,61 +158,13 @@ return ( ); ``` -### 8. 懒加载 - -```tsx | pure -const routes = [ - { key: 'first', title: 'First', component: }, - { key: 'second', title: 'Second', component: }, - { key: 'third', title: 'Third', component: }, -]; - -return ( - - - -); -``` - -
- -
- -### 9. 懒加载 + 自定义占位组件 +### 7. 默认切换到第二个选项卡 ```tsx | pure const routes = [ - { key: 'first', title: 'First', component: }, - { key: 'second', title: 'Second', component: }, - { key: 'third', title: 'Third', component: }, -]; - -return ( - - } /> - -); -``` - -
- -
- -### 10. 默认切换到第二个选项卡 - -```tsx | pure -const routes = [ - { key: 'first', title: 'First', component: }, - { key: 'second', title: 'Second', component: }, - { key: 'third', title: 'Third', component: }, + { title: 'First', component: }, + { title: 'Second', component: }, + { title: 'Third', component: }, ]; return ( @@ -256,33 +184,23 @@ return ( ## API -| 属性 | 必填 | 说明 | 类型 | 默认值 | -| --- | --- | --- | --- | --- | -| scenes | `true` | 选项卡面板配置 | `TabScene[]` | | -| onChange | `false` | 选择某个选项卡标签 | `(key: string) => void` | | -| initialPage | `false` | 默认切换到第几个选项卡 | `number` | `0` | -| height | `false` | 选项卡高度 | `boolean` | `48` | -| scrollEnabled | `false` | 启用手势控制左右滑动 | `boolean` | `true` | -| overdrag | `false` | 到第一页或者最后一页之后还是否允许继续拖动 | `boolean` | `true` | -| keyboardDismissMode | `false` | 关闭键盘模式 | `none` \| `on-drag` | `on-drag` | -| showIndicator | `false` | 是否显示指示器 | `boolean` | `true` | -| lazy | `false` | 是否懒加载其他页面 | `boolean` | `false` | -| renderLazyPlaceholder | `false` | 懒加载时的占位提示组件 | `() => ReactNode` | `() => null` | -| tabStyle | `false` | 选项卡样式 | `ViewStyle` | | -| tabItemStyle | `false` | 选项卡标签样式 | `ViewStyle` | | -| labelStyle | `false` | 标签文字样式 | `TextStyle` | | -| indicatorStyle | `false` | 指示器样式 | `IndicatorStyle` | | +| 属性 | 必填 | 说明 | 类型 | 默认值 | +| ------------------- | ------- | ------------------------------------------ | ------------------- | --------- | +| scenes | `true` | 选项卡面板配置 | `TabScene[]` | | +| initialPage | `false` | 默认切换到第几个选项卡 | `number` | `0` | +| height | `false` | 选项卡高度 | `boolean` | `48` | +| scrollEnabled | `false` | 启用手势控制左右滑动 | `boolean` | `true` | +| overdrag | `false` | 到第一页或者最后一页之后还是否允许继续拖动 | `boolean` | `true` | +| keyboardDismissMode | `false` | 关闭键盘模式 | `none` \| `on-drag` | `on-drag` | +| showIndicator | `false` | 是否显示指示器 | `boolean` | `true` | +| tabStyle | `false` | 选项卡样式 | `ViewStyle` | | +| tabItemStyle | `false` | 选项卡标签样式 | `ViewStyle` | | +| labelStyle | `false` | 标签文字样式 | `TextStyle` | | +| indicatorStyle | `false` | 指示器样式 | `ViewStyle` | | ```ts interface TabScene { - key: string; title: ReactNode; component: JSX.Element; } - -interface IndicatorStyle { - height?: number; - borderRadius?: number; - color?: string; -} ``` diff --git a/packages/react-native-tabs/src/index.tsx b/packages/react-native-tabs/src/index.tsx index e543519fe8..e9c73217f0 100644 --- a/packages/react-native-tabs/src/index.tsx +++ b/packages/react-native-tabs/src/index.tsx @@ -1,93 +1,103 @@ -import React from 'react'; -import { LayoutChangeEvent } from 'react-native'; -import { PagerViewOnPageSelectedEvent } from 'react-native-pager-view'; +import React, { cloneElement } from 'react'; +import { Animated, StyleProp, TextStyle, ViewStyle } from 'react-native'; +import PagerView from 'react-native-pager-view'; import { Box, helpers } from '@td-design/react-native'; -import { useMemoizedFn, useSafeState } from '@td-design/rn-hooks'; -import AnimatedPagerView from './AnimatedPagerView'; -import SceneView from './SceneView'; import ScrollBar from './ScrollBar'; import TabBar from './TabBar'; -import { TabsProps } from './type'; import usePagerView from './usePagerView'; const { px } = helpers; +const AnimatedPagerView = Animated.createAnimatedComponent(PagerView); -export default function ({ initialPage = 0, ...props }: TabsProps) { - const [layout, setLayout] = useSafeState({ width: 0, height: 0 }); +type Tab = { + title: string; + component: JSX.Element; +}; - const handleLayout = useMemoizedFn((e: LayoutChangeEvent) => { - const { width, height } = e.nativeEvent.layout; - if (layout.height !== height || layout.width !== width) { - setLayout({ width, height }); - } - }); - - return ( - - - - - - ); +export interface TabsProps { + scenes: Tab[]; + initialPage?: number; + /** 标签栏的高度。 默认为48 */ + height?: number; + /** 是否支持手势滚动。 */ + scrollEnabled?: boolean; + /** 是否显示指示器。 默认为true */ + showIndicator?: boolean; + /** 到第一页或者最后一页之后还是否允许继续拖动。 默认为true */ + overdrag?: boolean; + /** 键盘关闭模式。 默认为滚动时关闭 */ + keyboardDismissMode?: 'none' | 'on-drag'; + tabStyle?: StyleProp; + tabItemStyle?: StyleProp; + labelStyle?: StyleProp; + indicatorStyle?: StyleProp; } -function TabView({ - scenes, - onChange, +export default function Tabs({ + initialPage = 0, + scenes = [], + height = px(48), + showIndicator = true, scrollEnabled = true, overdrag = true, keyboardDismissMode = 'on-drag', - height = px(48), - showIndicator = true, - lazy = false, - layout, - renderLazyPlaceholder = () => null, tabStyle, tabItemStyle, labelStyle, indicatorStyle, }: TabsProps) { - const { pagerRef, setPage, page, scrollX, isIdle, onPageSelected } = usePagerView.useModel(); + const { + pagerViewRef, + setPage, + page, + position, + offset, + isIdle, + scrollState, + onPageScroll, + onPageSelected, + onPageScrollStateChanged, + } = usePagerView(initialPage); + + const titles = scenes.map(tab => tab.title); - const handlePageSelected = useMemoizedFn((e: PagerViewOnPageSelectedEvent) => { - const page = e.nativeEvent.position; - onChange?.(scenes[page].key); - onPageSelected(page); - }); return ( - <> + + {/* 可以滚动的TabBar */} item.title)} + tabs={titles} onTabPress={setPage} - scrollX={scrollX} - isIdle={isIdle} page={page} + position={position} + offset={offset} + isIdle={isIdle} + scrollState={scrollState} + showIndicator={showIndicator} tabStyle={tabStyle} tabItemStyle={tabItemStyle} labelStyle={labelStyle} indicatorStyle={indicatorStyle} - showIndicator={showIndicator} /> + + {/* PagerView的内容 */} - {scenes.map((item, i) => ( - - {({ loading }) => { - if (loading) return renderLazyPlaceholder(); - return item.component; - }} - - ))} + {scenes.map(({ title, component }) => cloneElement(component, { key: title }))} - + ); } diff --git a/packages/react-native-tabs/src/type.ts b/packages/react-native-tabs/src/type.ts deleted file mode 100644 index 23b4bf61da..0000000000 --- a/packages/react-native-tabs/src/type.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { LayoutRectangle, StyleProp, TextStyle, ViewProps, ViewStyle } from 'react-native'; -import { PagerViewOnPageSelectedEvent } from 'react-native-pager-view'; -import { SharedValue } from 'react-native-reanimated'; - -type TabLabel = string | (() => React.ReactNode); - -export interface TabScene { - key: string; - title: TabLabel; - component: JSX.Element; -} - -type Layout = { width: number; height: number }; -export type Listener = (value: number) => void; - -export interface TabsProps - extends Omit, - Pick { - /** 所有的页面 */ - scenes: TabScene[]; - /** 翻页之后的回调 */ - onChange?: (key: string) => void; - /** 标签栏的高度。 默认为48 */ - height?: number; - /** 是否显示指示器。 默认为true */ - showIndicator?: boolean; - /** 是否懒加载其他页面。 默认为false */ - lazy?: boolean; - /** 懒加载时的占位提示组件 */ - renderLazyPlaceholder?: () => React.ReactNode; - /** 默认切换到第几个选项卡 */ - initialPage?: number; - layout?: Layout; -} - -export interface AnimatedPagerViewProps { - /** 是否支持滚动翻页。 默认为 true */ - scrollEnabled?: boolean; - /** 到第一页或者最后一页之后还是否允许继续拖动。 默认为true */ - overdrag?: boolean; - /** 键盘关闭模式。 默认为滚动时关闭 */ - keyboardDismissMode?: 'none' | 'on-drag'; - onPageSelected: (e: PagerViewOnPageSelectedEvent) => void; -} - -export interface TabBarProps { - tabs: TabLabel[]; - onTabPress: (index: number) => void; - onTabsLayout?: (layouts: LayoutRectangle[]) => void; - height?: number; - page: number; - scrollX: SharedValue; - isIdle: boolean; - spacing?: number; - showIndicator: boolean; - tabStyle?: StyleProp; - tabItemStyle?: StyleProp; - labelStyle?: StyleProp; - indicatorStyle?: IndicatorStyle; -} - -export interface TabBarItemProps { - title: TabLabel; - onPress?: () => void; - onLayout: ViewProps['onLayout']; - style?: StyleProp; - labelStyle?: StyleProp; -} - -export interface IndicatorStyle { - height?: number; - borderRadius?: number; - color?: string; -} - -export interface TabBarIndicatorProps { - style: IndicatorStyle; - scrollX: SharedValue; - inputRange: number[]; - scrollRange: number[]; - tabWidths: number[]; -} - -export interface SceneViewProps { - index: number; - lazy: boolean; - layout: Layout; - children: (props: { loading: boolean }) => React.ReactNode; -} diff --git a/packages/react-native-tabs/src/usePagerView.ts b/packages/react-native-tabs/src/usePagerView.ts index b8284fc592..083b6fb04a 100644 --- a/packages/react-native-tabs/src/usePagerView.ts +++ b/packages/react-native-tabs/src/usePagerView.ts @@ -1,89 +1,80 @@ -import { useRef } from 'react'; -import { Platform } from 'react-native'; -import PagerView, { PageScrollStateChangedNativeEvent } from 'react-native-pager-view'; -import { useDerivedValue, useSharedValue } from 'react-native-reanimated'; +import { useMemo, useRef } from 'react'; +import { Animated, Platform } from 'react-native'; +import PagerView, { + PagerViewOnPageScrollEventData, + PagerViewOnPageSelectedEvent, + PageScrollStateChangedNativeEvent, +} from 'react-native-pager-view'; import { useMemoizedFn, useSafeState } from '@td-design/rn-hooks'; -import { createShareModel } from './createShareModel'; -import { Listener } from './type'; - -function usePagerView(initialPage: number) { - const listenersRef = useRef([]); - const pagerRef = useRef(null); +export default function usePagerView(initialPage: number) { + const pagerViewRef = useRef(null); const [activePage, setActivePage] = useSafeState(initialPage); const [isIdle, setIdle] = useSafeState(true); - const setPage = (page: number, animated = true) => { + const setPage = useMemoizedFn((page: number, animated = true) => { if (animated) { - requestAnimationFrame(() => pagerRef.current?.setPage(page)); + pagerViewRef.current?.setPage(page); } else { - requestAnimationFrame(() => pagerRef.current?.setPageWithoutAnimation(page)); + pagerViewRef.current?.setPageWithoutAnimation(page); } + setActivePage(page); if (activePage !== page) { setIdle(false); } - }; - - const offset = useSharedValue(0); - const position = useSharedValue(initialPage); - - const scrollX = useDerivedValue(() => offset.value + position.value, [offset, position]); - - const onPageScroll = (e: { offset: number; position: number }) => { - offset.value = e.offset; - position.value = e.position; - }; + }); + + const offset = useRef(new Animated.Value(initialPage)).current; + const position = useRef(new Animated.Value(0)).current; + + const onPageScroll = useMemo( + () => + Animated.event( + [ + { + nativeEvent: { + offset, + position, + }, + }, + ], + { + listener: ({ nativeEvent: { position, offset } }) => { + console.log('onPageScroll', 'position', position, 'offset', offset); + }, + useNativeDriver: true, + } + ), + [offset, position] + ); - const onPageSelected = (page: number) => { - setActivePage(page); + const onPageSelected = useMemoizedFn((e: PagerViewOnPageSelectedEvent) => { + setActivePage(e.nativeEvent.position); if (Platform.OS === 'ios') { setIdle(true); } - }; - - const onPageScrollStateChanged = ({ nativeEvent: { pageScrollState } }: PageScrollStateChangedNativeEvent) => { - switch (pageScrollState) { - case 'idle': - setIdle(true); - break; + }); - case 'dragging': - const next = activePage + (offset.value > 0 ? Math.ceil(offset.value) : Math.floor(offset.value)); - if (next !== activePage) { - listenersRef.current.forEach(listener => listener(next)); - } - break; + const [scrollState, setScrollState] = useSafeState<'idle' | 'dragging' | 'settling'>('idle'); - default: - break; - } - }; - - const addEnterListener = (listener: Listener) => { - listenersRef.current.push(listener); - - return () => { - const index = listenersRef.current.indexOf(listener); - if (index > -1) { - listenersRef.current.splice(index, 1); - } - }; - }; + const onPageScrollStateChanged = useMemoizedFn((e: PageScrollStateChangedNativeEvent) => { + setScrollState(e.nativeEvent.pageScrollState); + setIdle(e.nativeEvent.pageScrollState === 'idle'); + }); return { - pagerRef, + pagerViewRef, page: activePage, isIdle, - scrollX, - onPageScroll: useMemoizedFn(onPageScroll), - setPage: useMemoizedFn(setPage), - onPageSelected: useMemoizedFn(onPageSelected), - onPageScrollStateChanged: useMemoizedFn(onPageScrollStateChanged), - addEnterListener: useMemoizedFn(addEnterListener), + scrollState, + position, + offset, + setPage, + onPageScroll, + onPageSelected, + onPageScrollStateChanged, }; } - -export default createShareModel(usePagerView); diff --git a/packages/react-native/src/accordion/index.tsx b/packages/react-native/src/accordion/index.tsx index 91d8cc3faf..5deb33d374 100644 --- a/packages/react-native/src/accordion/index.tsx +++ b/packages/react-native/src/accordion/index.tsx @@ -1,8 +1,9 @@ -import React, { FC, useMemo, useState } from 'react'; +import React, { FC, useMemo } from 'react'; import { FlatList } from 'react-native'; import Animated from 'react-native-reanimated'; import { useTheme } from '@shopify/restyle'; +import { useSafeState } from '@td-design/rn-hooks'; import Box from '../box'; import helpers from '../helpers'; @@ -24,7 +25,7 @@ const Accordion: FC = ({ headerStyle, contentStyle, }) => { - const [currentIndex, setCurrentIndex] = useState(); + const [currentIndex, setCurrentIndex] = useSafeState(); return ( { - const [textWithTail, setTextWithTail] = useState(text); + const [textWithTail, setTextWithTail] = useSafeState(text); useEffect(() => { if (animated) { @@ -35,7 +37,7 @@ const AnimatedNotice: FC { progress.value = withTiming( diff --git a/packages/react-native/src/notice-bar/index.tsx b/packages/react-native/src/notice-bar/index.tsx index ad069b31e2..3bc1c4a819 100644 --- a/packages/react-native/src/notice-bar/index.tsx +++ b/packages/react-native/src/notice-bar/index.tsx @@ -1,8 +1,9 @@ -import React, { FC, PropsWithChildren, useState } from 'react'; +import React, { FC, PropsWithChildren } from 'react'; import { LayoutChangeEvent } from 'react-native'; import Animated, { FadeOutRight } from 'react-native-reanimated'; import { useTheme } from '@shopify/restyle'; +import { useSafeState } from '@td-design/rn-hooks'; import Box from '../box'; import Flex from '../flex'; @@ -27,8 +28,8 @@ const NoticeBar: FC = props => { activeOpacity = 0.6, } = props; - const [visible, setVisible] = useState(true); - const [height, setHeight] = useState(0); + const [visible, setVisible] = useSafeState(true); + const [height, setHeight] = useSafeState(0); const handleContentLayout = (e: LayoutChangeEvent) => { setHeight(e.nativeEvent.layout.height); diff --git a/packages/svgicon-cli/src/templates/Icon.tsx.template b/packages/svgicon-cli/src/templates/Icon.tsx.template index a4e5ea9e33..14831fcd2f 100644 --- a/packages/svgicon-cli/src/templates/Icon.tsx.template +++ b/packages/svgicon-cli/src/templates/Icon.tsx.template @@ -1,7 +1,6 @@ -/* tslint:disable */ /* eslint-disable */ -import React, { FC } from 'react'; +import React from 'react'; import { ViewProps } from 'react-native'; #svgComponents# #imports# @@ -16,7 +15,7 @@ export interface SvgIconProps extends GProps, ViewProps { color?: string | string[]; } -let SvgIcon: FC = ({ name, ...rest }) => { +let SvgIcon: React.FC = ({ name, ...rest }) => { switch (name) { #cases# default: @@ -24,6 +23,4 @@ let SvgIcon: FC = ({ name, ...rest }) => { } }; -SvgIcon = React.memo ? React.memo(SvgIcon) : SvgIcon; - -export default SvgIcon; +export default React.memo(SvgIcon); diff --git a/packages/svgicon-cli/src/templates/LocalSingleIcon.tsx.template b/packages/svgicon-cli/src/templates/LocalSingleIcon.tsx.template index ee67c765be..0c1bc1a63f 100644 --- a/packages/svgicon-cli/src/templates/LocalSingleIcon.tsx.template +++ b/packages/svgicon-cli/src/templates/LocalSingleIcon.tsx.template @@ -1,7 +1,6 @@ -/* tslint:disable */ /* eslint-disable */ -import React, { FC } from 'react'; +import React from 'react'; import { ViewProps } from 'react-native'; #svgComponents# #helper# @@ -14,7 +13,7 @@ export interface SvgIconProps extends GProps, ViewProps { color?: string | string[]; } -let #componentName#: FC = ({ size, width = size, height = size, color, ...rest }) => { +let #componentName#: React.FC = ({ size, width = size, height = size, color, ...rest }) => { #xml# return (#iconContent# ); @@ -24,6 +23,4 @@ let #componentName#: FC = ({ size, width = size, height = size, co size: px(#size#), }; -#componentName# = React.memo ? React.memo(#componentName#) : #componentName#; - -export default #componentName#; +export default React.memo(#componentName#); diff --git a/packages/svgicon-cli/src/templates/helper.ts.template b/packages/svgicon-cli/src/templates/helper.ts.template index 60d6d713c5..1e0ec70481 100644 --- a/packages/svgicon-cli/src/templates/helper.ts.template +++ b/packages/svgicon-cli/src/templates/helper.ts.template @@ -1,4 +1,3 @@ -/* tslint:disable */ /* eslint-disable */ export const getIconColor = (color: string | string[] | undefined, index: number, defaultColor: string) => {