1
1
import _ from 'lodash' ;
2
- import React , { useRef , useState , useCallback , useMemo } from 'react' ;
2
+ import React , { useRef , useCallback } from 'react' ;
3
3
import { StyleSheet , StyleProp , ViewStyle , LayoutChangeEvent } from 'react-native' ;
4
- import Reanimated , { EasingNode , Easing as _Easing } from 'react-native-reanimated' ;
4
+ import Reanimated , {
5
+ Easing ,
6
+ useAnimatedReaction ,
7
+ useAnimatedStyle ,
8
+ useSharedValue ,
9
+ withTiming ,
10
+ runOnJS
11
+ } from 'react-native-reanimated' ;
5
12
import { Colors , BorderRadiuses , Spacings } from '../../style' ;
6
13
import { asBaseComponent } from '../../commons/new' ;
7
14
import View from '../view' ;
8
15
import Segment , { SegmentedControlItemProps as SegmentProps } from './segment' ;
9
16
import { Constants } from 'helpers' ;
10
17
11
- const { interpolate : _interpolate , interpolateNode} = Reanimated ;
12
- const interpolate = interpolateNode || _interpolate ;
13
- const Easing = EasingNode || _Easing ;
14
18
const BORDER_WIDTH = 1 ;
19
+ const TIMING_CONFIG = {
20
+ duration : 300 ,
21
+ easing : Easing . bezier ( 0.33 , 1 , 0.68 , 1 )
22
+ } ;
15
23
16
24
export type SegmentedControlItemProps = SegmentProps ;
17
25
export type SegmentedControlProps = {
@@ -59,6 +67,10 @@ export type SegmentedControlProps = {
59
67
* Should the icon be on right of the label
60
68
*/
61
69
iconOnRight ?: boolean ;
70
+ /**
71
+ * Trailing throttle time of changing index in ms.
72
+ */
73
+ throttleTime ?: number ;
62
74
/**
63
75
* Additional spacing styles for the container
64
76
*/
@@ -85,63 +97,61 @@ const SegmentedControl = (props: SegmentedControlProps) => {
85
97
inactiveColor = Colors . grey20 ,
86
98
outlineColor = activeColor ,
87
99
outlineWidth = BORDER_WIDTH ,
100
+ throttleTime = 0 ,
88
101
testID
89
102
} = props ;
90
- const [ selectedSegment , setSelectedSegment ] = useState ( - 1 ) ;
91
-
92
- const segmentsStyle = useRef ( [ ] as { x : number ; width : number } [ ] ) ;
93
- const segmentedControlHeight = useRef ( 0 ) ;
94
- const indexRef = useRef ( 0 ) ;
103
+ const animatedSelectedIndex = useSharedValue ( initialIndex ) ;
104
+ const segmentsStyle = useSharedValue ( [ ] as { x : number ; width : number } [ ] ) ;
105
+ const segmentedControlHeight = useSharedValue ( 0 ) ;
95
106
const segmentsCounter = useRef ( 0 ) ;
96
- const animatedValue = useRef ( new Reanimated . Value ( initialIndex ) ) ;
97
107
108
+ // eslint-disable-next-line react-hooks/exhaustive-deps
98
109
const changeIndex = useCallback ( _ . throttle ( ( ) => {
99
- onChangeIndex ?.( indexRef . current ) ;
110
+ onChangeIndex ?.( animatedSelectedIndex . value ) ;
100
111
} ,
101
- 400 ,
112
+ throttleTime ,
102
113
{ trailing : true , leading : false } ) ,
103
- [ ] ) ;
114
+ [ throttleTime ] ) ;
104
115
105
- const onSegmentPress = useCallback ( ( index : number ) => {
106
- if ( selectedSegment !== index ) {
107
- setSelectedSegment ( index ) ;
108
- indexRef . current = index ;
109
-
110
- Reanimated . timing ( animatedValue . current , {
111
- toValue : index ,
112
- duration : 300 ,
113
- easing : Easing . bezier ( 0.33 , 1 , 0.68 , 1 )
114
- } ) . start ( changeIndex ) ;
116
+ useAnimatedReaction ( ( ) => {
117
+ return animatedSelectedIndex . value ;
118
+ } ,
119
+ ( selected , previous ) => {
120
+ if ( selected !== - 1 && previous !== null && selected !== previous ) {
121
+ onChangeIndex && runOnJS ( changeIndex ) ( ) ;
115
122
}
116
123
} ,
117
- [ onChangeIndex , selectedSegment ] ) ;
124
+ [ ] ) ;
125
+
126
+ const onSegmentPress = useCallback ( ( index : number ) => {
127
+ animatedSelectedIndex . value = index ;
128
+ // eslint-disable-next-line react-hooks/exhaustive-deps
129
+ } , [ ] ) ;
118
130
119
131
const onLayout = useCallback ( ( index : number , event : LayoutChangeEvent ) => {
120
132
const { x, width, height} = event . nativeEvent . layout ;
121
- segmentsStyle . current [ index ] = { x, width} ;
122
- segmentedControlHeight . current = height - 2 * BORDER_WIDTH ;
133
+ segmentsStyle . value [ index ] = { x, width} ;
134
+ segmentedControlHeight . value = height - 2 * BORDER_WIDTH ;
123
135
segmentsCounter . current ++ ;
124
136
125
- return segmentsCounter . current === segments ?. length && setSelectedSegment ( initialIndex ) ;
137
+ if ( segmentsCounter . current === segments ?. length ) {
138
+ animatedSelectedIndex . value = initialIndex ;
139
+ segmentsStyle . value = [ ...segmentsStyle . value ] ;
140
+ }
126
141
} ,
142
+ // eslint-disable-next-line react-hooks/exhaustive-deps
127
143
[ initialIndex , segments ?. length ] ) ;
128
144
129
- const animatedStyle = useMemo ( ( ) => {
130
- if ( segmentsCounter . current === segments ?. length ) {
131
- const inset = interpolate ( animatedValue . current , {
132
- inputRange : _ . times ( segmentsCounter . current ) ,
133
- outputRange : _ . map ( segmentsStyle . current , segment => segment . x )
134
- } ) ;
135
-
136
- const width = interpolate ( animatedValue . current , {
137
- inputRange : _ . times ( segmentsCounter . current ) ,
138
- outputRange : _ . map ( segmentsStyle . current , segment => segment . width - 2 * BORDER_WIDTH )
139
- } ) ;
140
-
141
- return [ { width} , Constants . isRTL ? { right : inset } : { left : inset } ] ;
145
+ const animatedStyle = useAnimatedStyle ( ( ) => {
146
+ if ( segmentsStyle . value . length !== 0 ) {
147
+ const inset = withTiming ( segmentsStyle . value [ animatedSelectedIndex . value ] . x , TIMING_CONFIG ) ;
148
+ const width = withTiming ( segmentsStyle . value [ animatedSelectedIndex . value ] . width - 2 * BORDER_WIDTH ,
149
+ TIMING_CONFIG ) ;
150
+ const height = segmentedControlHeight . value ;
151
+ return Constants . isRTL ? { width, right : inset , height} : { width, left : inset , height} ;
142
152
}
143
- return undefined ;
144
- } , [ segmentsCounter . current , segments ?. length ] ) ;
153
+ return { } ;
154
+ } ) ;
145
155
146
156
const renderSegments = ( ) =>
147
157
_ . map ( segments , ( _value , index ) => {
@@ -151,7 +161,7 @@ const SegmentedControl = (props: SegmentedControlProps) => {
151
161
onLayout = { onLayout }
152
162
index = { index }
153
163
onPress = { onSegmentPress }
154
- isSelected = { selectedSegment === index }
164
+ selectedIndex = { animatedSelectedIndex }
155
165
activeColor = { activeColor }
156
166
inactiveColor = { inactiveColor }
157
167
{ ...segments ?. [ index ] }
@@ -163,21 +173,18 @@ const SegmentedControl = (props: SegmentedControlProps) => {
163
173
return (
164
174
< View style = { containerStyle } testID = { testID } >
165
175
< View row center style = { [ styles . container , style , { borderRadius, backgroundColor} ] } >
166
- { animatedStyle && (
167
- < Reanimated . View
168
- style = { [
169
- styles . selectedSegment ,
170
- animatedStyle ,
171
- {
172
- borderColor : outlineColor ,
173
- borderRadius,
174
- backgroundColor : activeBackgroundColor ,
175
- borderWidth : outlineWidth ,
176
- height : segmentedControlHeight . current
177
- }
178
- ] }
179
- />
180
- ) }
176
+ < Reanimated . View
177
+ style = { [
178
+ styles . selectedSegment ,
179
+ {
180
+ borderColor : outlineColor ,
181
+ borderRadius,
182
+ backgroundColor : activeBackgroundColor ,
183
+ borderWidth : outlineWidth
184
+ } ,
185
+ animatedStyle
186
+ ] }
187
+ />
181
188
{ renderSegments ( ) }
182
189
</ View >
183
190
</ View >
0 commit comments