Skip to content

Commit 0fa79e5

Browse files
ethansharmendyEdri
andauthored
New custom WheelPicker (#740)
* create a customizable WheelPicker * Reset PlaygroundScreen changes * Keep working on WheelPicker (incubator) * Fix TS issues * export WheelPicker in incubator index * Use Flatlist for WheelPicker for better performance when having big lists * Separate to two examples: months and years * Feat/basic ui (#1038) * Removed 'wheel` animation to match the design guidlines + added default colors and font * added text configuration props * on selecting item - wheel will scroll to the right row * added Fader and extract separators * Calculate current active index * added onChange event * Increase visible rows to 5 * change separator color to be visible * added demo code * update reanimated version * types changes * type update * extract logic to it's own type * refactor item center calculation to be based on the offset * update demo screen according to the new api * pass style to item * added unittests to listMiddleIndex calculations * made demo more clear * Update src/incubator/WheelPicker/index.tsx Co-authored-by: Ethan Sharabi <[email protected]> * PR fixes - naming * PR fixes - naming * PR fix - optional chaining Co-authored-by: Ethan Sharabi <[email protected]> Co-authored-by: Mendy Edri <[email protected]>
1 parent 0f768d4 commit 0fa79e5

File tree

12 files changed

+379
-2
lines changed

12 files changed

+379
-2
lines changed

demo/src/screens/MenuStructure.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,8 @@ export const navigationData = {
149149
title: 'Incubator (Experimental)',
150150
screens: [
151151
{title: 'Native TouchableOpacity', tags: 'touchable native', screen: 'unicorn.incubator.TouchableOpacityScreen'},
152-
{title: '(New) TextField', tags: 'text field input', screen: 'unicorn.components.IncubatorTextFieldScreen'}
152+
{title: '(New) TextField', tags: 'text field input', screen: 'unicorn.components.IncubatorTextFieldScreen'},
153+
{title: 'WheelPicker (Incubator)', tags: 'wheel picker spinner experimental', screen: 'unicorn.incubator.WheelPickerScreen'}
153154
]
154155
},
155156
Inspirations: {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React, {useCallback} from 'react';
2+
import {View, Text, Incubator, Colors, Typography} from 'react-native-ui-lib';
3+
import _ from 'lodash';
4+
import {ItemProps} from 'src/incubator/WheelPicker/Item';
5+
6+
// Months
7+
const months = [
8+
'January',
9+
'February',
10+
'March',
11+
'April',
12+
'May',
13+
'June',
14+
'July',
15+
'August',
16+
'September',
17+
'October',
18+
'November',
19+
'December'
20+
];
21+
22+
// Years
23+
const years = _.times(2020, i => i);
24+
25+
export default () => {
26+
27+
const onChange = useCallback((index: number, item?: ItemProps) => {
28+
console.log(item, index);
29+
}, []);
30+
31+
return (
32+
<View flex padding-page>
33+
<Text h1>Wheel Picker</Text>
34+
<View flex centerV centerH paddingT-page>
35+
<Text h3>Months</Text>
36+
<Incubator.WheelPicker
37+
onChange={onChange}
38+
activeTextColor={Colors.primary}
39+
inactiveTextColor={Colors.grey20}
40+
items={_.map(months, i => ({text: i, value: i}))}
41+
textStyle={{...Typography.text60R}}
42+
/>
43+
44+
<Text h3 marginT-s5>Years</Text>
45+
<Incubator.WheelPicker onChange={onChange} items={_.map(years, i => ({text: '' + i, value: i}))} />
46+
</View>
47+
</View>
48+
);
49+
};

demo/src/screens/incubatorScreens/index.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,9 @@ import {gestureHandlerRootHOC} from 'react-native-gesture-handler';
22

33
export function registerScreens(registrar) {
44
registrar('unicorn.incubator.TouchableOpacityScreen', () =>
5-
gestureHandlerRootHOC(require('./TouchableOpacityScreen').default));
5+
gestureHandlerRootHOC(require('./TouchableOpacityScreen').default)
6+
);
7+
registrar('unicorn.incubator.WheelPickerScreen', () =>
8+
gestureHandlerRootHOC(require('./WheelPickerScreen').default)
9+
);
610
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/// <reference types="react" />
2+
import { TextStyle } from 'react-native';
3+
export interface ItemProps {
4+
text: string;
5+
value: string | number;
6+
}
7+
interface InternalProps extends ItemProps {
8+
index: number;
9+
offset: any;
10+
itemHeight: number;
11+
activeColor?: string;
12+
inactiveColor?: string;
13+
style?: TextStyle;
14+
onSelect: (index: number) => void;
15+
}
16+
declare const _default: ({ index, text, itemHeight, onSelect, offset, activeColor, inactiveColor, style }: InternalProps) => JSX.Element;
17+
export default _default;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
declare type ItemType = {
2+
itemHeight: number;
3+
listSize: number;
4+
};
5+
declare const _default: ({ itemHeight, listSize }: ItemType) => (offset: number) => number;
6+
export default _default;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/// <reference types="react" />
2+
import { TextStyle } from 'react-native';
3+
import { ItemProps } from './Item';
4+
export interface WheelPickerProps {
5+
/**
6+
* Data source for WheelPicker
7+
*/
8+
items?: ItemProps[];
9+
/**
10+
* Describe the height of each item in the WheelPicker
11+
*/
12+
itemHeight?: number;
13+
/**
14+
* Text color for the focused row
15+
*/
16+
activeTextColor?: string;
17+
/**
18+
* Text color for other, non-focused rows
19+
*/
20+
inactiveTextColor?: string;
21+
/**
22+
* Row text style
23+
*/
24+
textStyle?: TextStyle;
25+
/**
26+
* Event, on active row change
27+
*/
28+
onChange: (index: number, item?: ItemProps) => void;
29+
}
30+
declare const WheelPicker: ({ items, itemHeight, activeTextColor, inactiveTextColor, textStyle, onChange: onChangeEvent }: WheelPickerProps) => JSX.Element;
31+
export default WheelPicker;

generatedTypes/incubator/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { default as TabController } from './TabController';
22
export { default as TextField } from './TextField';
33
export { default as TouchableOpacity } from './TouchableOpacity';
4+
export { default as WheelPicker } from './WheelPicker';

src/incubator/WheelPicker/Item.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import React, {useCallback, useMemo} from 'react';
2+
import Animated, {interpolateColors} from 'react-native-reanimated';
3+
import Text from '../../components/text';
4+
import TouchableOpacity from '../../components/touchableOpacity';
5+
import {TextStyle} from 'react-native';
6+
import {Colors} from '../../../src/style';
7+
8+
const AnimatedTouchableOpacity = Animated.createAnimatedComponent(
9+
TouchableOpacity
10+
);
11+
const AnimatedText = Animated.createAnimatedComponent(Text);
12+
13+
export interface ItemProps {
14+
text: string;
15+
value: string | number;
16+
}
17+
18+
interface InternalProps extends ItemProps {
19+
index: number;
20+
offset: any;
21+
itemHeight: number;
22+
activeColor?: string;
23+
inactiveColor?: string;
24+
style?: TextStyle;
25+
onSelect: (index: number) => void;
26+
}
27+
28+
export default ({
29+
index,
30+
text,
31+
itemHeight,
32+
onSelect,
33+
offset,
34+
activeColor = Colors.primary,
35+
inactiveColor = Colors.grey20,
36+
style
37+
}: InternalProps) => {
38+
39+
const selectItem = useCallback(() => onSelect(index), [index]);
40+
const itemOffset = index * itemHeight;
41+
42+
const color = useMemo(() => {
43+
return interpolateColors(offset, {
44+
inputRange: [
45+
itemOffset - itemHeight,
46+
itemOffset,
47+
itemOffset + itemHeight
48+
],
49+
outputColorRange: [inactiveColor, activeColor, inactiveColor]
50+
});
51+
}, [itemHeight]);
52+
53+
return (
54+
<AnimatedTouchableOpacity
55+
activeOpacity={1}
56+
style={{
57+
height: itemHeight
58+
}}
59+
key={index}
60+
center
61+
onPress={selectItem}
62+
index={index}
63+
>
64+
<AnimatedText text60R style={{color, ...style}}>
65+
{text}
66+
</AnimatedText>
67+
</AnimatedTouchableOpacity>
68+
);
69+
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import useMiddleIndex from '../helpers/useListMiddleIndex';
2+
3+
describe('Finds list\'s middle index', () => {
4+
5+
it('When list is at offset 0, it should return the index of the first item', () => {
6+
const sut = useMiddleIndex({itemHeight: 50, listSize: 10});
7+
const offset = 0;
8+
expect(sut(offset)).toEqual(0);
9+
});
10+
11+
it('When list is at offset 100, it means we are at passed on 2 items', () => {
12+
const sut = useMiddleIndex({itemHeight: 50, listSize: 10});
13+
const offset = 100;
14+
expect(sut(offset)).toEqual(2);
15+
});
16+
17+
it('Make sure calculation changes on the middle of the item height', () => {
18+
const sut = useMiddleIndex({itemHeight: 50, listSize: 10});
19+
let offset = 24;
20+
expect(sut(offset)).toEqual(0);
21+
22+
offset = 26;
23+
expect(sut(offset)).toEqual(1);
24+
});
25+
26+
it('Make sure calculation does not exceeds the number of items', () => {
27+
const sut = useMiddleIndex({itemHeight: 50, listSize: 10});
28+
let offset = 501;
29+
expect(sut(offset)).toEqual(9);
30+
31+
offset = 600;
32+
expect(sut(offset)).toEqual(9);
33+
});
34+
35+
it('Make sure calculation does not less then 0', () => {
36+
const sut = useMiddleIndex({itemHeight: 50, listSize: 10});
37+
const offset = -100;
38+
expect(sut(offset)).toEqual(0);
39+
});
40+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
type ItemType = {
2+
itemHeight: number;
3+
listSize: number;
4+
}
5+
6+
export default ({itemHeight, listSize}: ItemType) => {
7+
const valueInRange = (value: number, min: number, max: number): number => {
8+
if (value < min || value === -0) {
9+
return min;
10+
}
11+
if (value > max) {
12+
return max;
13+
}
14+
return value;
15+
};
16+
17+
const middleIndex = (offset: number): number => {
18+
const calculatedIndex = Math.round(offset / itemHeight);
19+
return valueInRange(calculatedIndex, 0, listSize - 1);
20+
};
21+
22+
return middleIndex
23+
};

0 commit comments

Comments
 (0)