Skip to content

Infra/ migrate SegmentedControl to reanimated v2 #1567

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Oct 14, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ const SectionsWheelPickerScreen = () => {
<SegmentedControl
segments={[{label: '1 section'}, {label: '2 sections'}, {label: '3 sections'}]}
onChangeIndex={onChangeIndex}
throttleTime={400}
/>
<Text text50 marginV-20>
Pick a duration
Expand Down
4 changes: 4 additions & 0 deletions generatedTypes/src/components/segmentedControl/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ export declare type SegmentedControlProps = {
* Should the icon be on right of the label
*/
iconOnRight?: boolean;
/**
* Trailing throttle time of changing index in ms.
*/
throttleTime?: number;
/**
* Additional spacing styles for the container
*/
Expand Down
4 changes: 2 additions & 2 deletions generatedTypes/src/components/segmentedControl/segment.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export declare type SegmentProps = SegmentedControlItemProps & {
/**
* Callback for when segment has pressed.
*/
onPress: (index: number) => void;
onPress?: (index: number) => void;
/**
* The index of the segment.
*/
Expand All @@ -61,7 +61,7 @@ declare const _default: React.ComponentClass<SegmentedControlItemProps & {
/**
* Callback for when segment has pressed.
*/
onPress: (index: number) => void;
onPress?: ((index: number) => void) | undefined;
/**
* The index of the segment.
*/
Expand Down
78 changes: 42 additions & 36 deletions src/components/segmentedControl/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import _ from 'lodash';
import React, {useRef, useState, useCallback, useMemo} from 'react';
import React, {useRef, useState, useCallback} from 'react';
import {StyleSheet, StyleProp, ViewStyle, LayoutChangeEvent} from 'react-native';
import Reanimated, {EasingNode, Easing as _Easing} from 'react-native-reanimated';
import Reanimated, {
Easing,
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
withTiming,
runOnJS
} from 'react-native-reanimated';
import {Colors, BorderRadiuses, Spacings} from '../../style';
import {asBaseComponent} from '../../commons/new';
import View from '../view';
import Segment, {SegmentedControlItemProps as SegmentProps} from './segment';
import {Constants} from 'helpers';

const {interpolate: _interpolate, interpolateNode} = Reanimated;
const interpolate = interpolateNode || _interpolate;
const Easing = EasingNode || _Easing;
const BORDER_WIDTH = 1;
const TIMING_CONFIG = {
duration: 300,
easing: Easing.bezier(0.33, 1, 0.68, 1)
};

export type SegmentedControlItemProps = SegmentProps;
export type SegmentedControlProps = {
Expand Down Expand Up @@ -59,6 +67,10 @@ export type SegmentedControlProps = {
* Should the icon be on right of the label
*/
iconOnRight?: boolean;
/**
* Trailing throttle time of changing index in ms.
*/
throttleTime?: number;
/**
* Additional spacing styles for the container
*/
Expand All @@ -85,36 +97,37 @@ const SegmentedControl = (props: SegmentedControlProps) => {
inactiveColor = Colors.grey20,
outlineColor = activeColor,
outlineWidth = BORDER_WIDTH,
throttleTime,
testID
} = props;
const [selectedSegment, setSelectedSegment] = useState(-1);

const animatedSelectedIndex = useSharedValue(selectedSegment);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that you have a shared value for the selectedIndex, you can implement this component a little differently.
Right now, you keep both animatedSelectedIndex, selectedSegment and indexRef. Each one of them represent the same value which goes against the single source of truth rule..

I'd suggest to try and unify them all to a single source and use only the shareValue.
It can help with performance, since you won't need to update the state anymore and clean the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see why the indexRef is unnecessary, but the selectedSegment is necessary for triggering a render after the onLayout (and after any onPress).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's go over it together..
Let me know when you have time.
Maybe I'm wrong, but I think it's possible to make some change and to make it work only with animatedSelectedIndex

const segmentsStyle = useRef([] as {x: number; width: number}[]);
const segmentedControlHeight = useRef(0);
const indexRef = useRef(0);
const segmentsCounter = useRef(0);
const animatedValue = useRef(new Reanimated.Value(initialIndex));
const delay = throttleTime || 0;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of having the delay const you can set a default value to throttleTime when you destruct it from props


const changeIndex = useCallback(_.throttle(() => {
onChangeIndex?.(indexRef.current);
onChangeIndex?.(animatedSelectedIndex.value);
},
400,
delay,
{trailing: true, leading: false}),
[]);

const onSegmentPress = useCallback((index: number) => {
if (selectedSegment !== index) {
setSelectedSegment(index);
indexRef.current = index;

Reanimated.timing(animatedValue.current, {
toValue: index,
duration: 300,
easing: Easing.bezier(0.33, 1, 0.68, 1)
}).start(changeIndex);
useAnimatedReaction(() => {
return animatedSelectedIndex.value;
},
(selected, previous) => {
if (selected !== -1 && selected !== previous) {
onChangeIndex && runOnJS(changeIndex)();
}
},
[onChangeIndex, selectedSegment]);
[]);

const onSegmentPress = useCallback((index: number) => {
setSelectedSegment(index);
animatedSelectedIndex.value = index;
}, []);

const onLayout = useCallback((index: number, event: LayoutChangeEvent) => {
const {x, width, height} = event.nativeEvent.layout;
Expand All @@ -126,32 +139,25 @@ const SegmentedControl = (props: SegmentedControlProps) => {
},
[initialIndex, segments?.length]);

const animatedStyle = useMemo(() => {
const animatedStyle = useAnimatedStyle(() => {
if (segmentsCounter.current === segments?.length) {
const inset = interpolate(animatedValue.current, {
inputRange: _.times(segmentsCounter.current),
outputRange: _.map(segmentsStyle.current, segment => segment.x)
});

const width = interpolate(animatedValue.current, {
inputRange: _.times(segmentsCounter.current),
outputRange: _.map(segmentsStyle.current, segment => segment.width - 2 * BORDER_WIDTH)
});

return [{width}, Constants.isRTL ? {right: inset} : {left: inset}];
const inset = withTiming(segmentsStyle.current[selectedSegment].x, TIMING_CONFIG);
const width = withTiming(segmentsStyle.current[selectedSegment].width - 2 * BORDER_WIDTH, TIMING_CONFIG);
return Constants.isRTL ? {width, right: inset} : {width, left: inset};
}
return undefined;
}, [segmentsCounter.current, segments?.length]);
return {};
}, [segmentsCounter.current, segments?.length, selectedSegment]);

const renderSegments = () =>
_.map(segments, (_value, index) => {
const isSelected = selectedSegment === index;
return (
<Segment
key={index}
onLayout={onLayout}
index={index}
onPress={onSegmentPress}
isSelected={selectedSegment === index}
isSelected={isSelected}
activeColor={activeColor}
inactiveColor={inactiveColor}
{...segments?.[index]}
Expand Down
6 changes: 3 additions & 3 deletions src/components/segmentedControl/segment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export type SegmentProps = SegmentedControlItemProps & {
/**
* Callback for when segment has pressed.
*/
onPress: (index: number) => void;
onPress?: (index: number) => void;
/**
* The index of the segment.
*/
Expand Down Expand Up @@ -81,8 +81,8 @@ const Segment = React.memo((props: SegmentProps) => {
}, [iconSource, segmentedColor, iconStyle]);

const onSegmentPress = useCallback(() => {
onPress(index);
}, [index, onPress]);
!isSelected && onPress?.(index);
}, [index, onPress, isSelected]);

const segmentOnLayout = useCallback((event: LayoutChangeEvent) => {
onLayout?.(index, event);
Expand Down