Skip to content

Commit d9dd487

Browse files
committed
feat(Picker): Add new component.
1 parent 5f4f7f4 commit d9dd487

File tree

9 files changed

+398
-1
lines changed

9 files changed

+398
-1
lines changed

example/examples/src/routes.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,4 +427,12 @@ export const stackPageData: Routes[] = [
427427
description: '用于展示页码、请求数据等。',
428428
},
429429
},
430+
{
431+
name: 'Picker',
432+
component: require('./routes/Picker').default,
433+
params: {
434+
title: 'Picker 选择器',
435+
description: 'Picker 解决 ios 与 android 和用户交互方式不同问题',
436+
},
437+
},
430438
];

example/examples/src/routes/Input/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ export default class InputView extends Component<InputProps> {
2626
<Input
2727
style={styles.input}
2828
placeholder="请输入"
29-
disabled={true}
29+
clear
30+
// disabled={true}
3031
onChangeText={(value: string) => {
3132
this.setState({value});
3233
}}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React from 'react';
2+
import {Text, View} from 'react-native';
3+
import {Picker} from '@uiw/react-native';
4+
import {ComProps} from '../../routes';
5+
import Layout, {Container} from '../../Layout';
6+
const {Header, Body, Footer} = Layout;
7+
8+
export interface BadgeViewProps extends ComProps {}
9+
10+
export default class BadgeView extends React.Component<BadgeViewProps> {
11+
render() {
12+
const {route, navigation} = this.props;
13+
const description = route.params.description;
14+
const title = route.params.title;
15+
return (
16+
<Container scrollEnabled={false}>
17+
<Layout>
18+
<Header title={title} description={description} />
19+
<Body scrollEnabled={false}>
20+
<View
21+
style={{
22+
flex: 1,
23+
flexDirection: 'row',
24+
backgroundColor: '#fff',
25+
marginTop: 20,
26+
}}>
27+
<View style={{width: '50%'}}>
28+
<Picker
29+
date={[
30+
{label: '1'},
31+
{label: '2'},
32+
{label: '3'},
33+
{label: '4'},
34+
{label: '5'},
35+
{label: '6'},
36+
]}
37+
value={17}
38+
/>
39+
</View>
40+
<View style={{width: '50%'}}>
41+
<Picker
42+
onChange={val => {
43+
console.log('val: ', val);
44+
}}
45+
date={[
46+
{label: '1'},
47+
{label: '2'},
48+
{label: '3'},
49+
{label: '4'},
50+
{label: '5'},
51+
{label: '6'},
52+
]}
53+
value={17}
54+
/>
55+
</View>
56+
</View>
57+
</Body>
58+
<Footer />
59+
</Layout>
60+
</Container>
61+
);
62+
}
63+
}

packages/core/src/Picker/README.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
Picker 选择器
2+
---
3+
4+
解决 ios 与 android 和用户交互方式不同问题.
5+
6+
### 基础示例
7+
8+
```jsx
9+
import { View } from 'react-native';
10+
import { Picker } from '@uiw/react-native';
11+
12+
function Demo() {
13+
return (
14+
<View style={{ flex: 1 }}>
15+
<Picker
16+
date={[
17+
{label: '1'},
18+
{label: '2'},
19+
{label: '3'},
20+
{label: '4'},
21+
{label: '5'},
22+
]}
23+
/>
24+
</View>
25+
)
26+
}
27+
```
28+
29+
### 使用自定义元素
30+
31+
```jsx
32+
import { View, Text } from 'react-native';
33+
import { Picker } from '@uiw/react-native';
34+
35+
function Demo() {
36+
return (
37+
<View style={{ flex: 1 }}>
38+
<Picker
39+
date={[
40+
{label: '1'},
41+
{label: '2'},
42+
{label: '3'},
43+
{label: '4'},
44+
{label: '5'},
45+
{label: '5',render: (label, record, index)=><Text>123</Text>},
46+
]}
47+
/>
48+
</View>
49+
)
50+
}
51+
```
52+
53+
54+
55+
### Props
56+
57+
```ts
58+
import { StyleProp, TextStyle, ViewStyle } from 'react-native';
59+
60+
export interface PickerDate {
61+
label?: string,
62+
render?: (key: string, record: PickerDate, index: number)=>React.ReactNode,
63+
[key: string]: any
64+
}
65+
66+
export interface PickerProps {
67+
/** 显示几行, 默认 3 */
68+
lines?: number,
69+
/** 指定需要显示的 key, 默认使用 data 的 label 属性 */
70+
key?: string,
71+
/** 需要渲染的数据 */
72+
date?: Array<PickerDate>,
73+
/** item 容器样式 */
74+
containerStyle?: {
75+
/** 激活的容器样式 */
76+
actived?: StyleProp<ViewStyle>,
77+
/** 未激活的容器样式 */
78+
unactived?: StyleProp<ViewStyle>,
79+
},
80+
/** 容器的文本样式 */
81+
textStyle?: {
82+
/** 激活的文本样式 */
83+
actived?: StyleProp<TextStyle>,
84+
/** 未激活的文本样式 */
85+
unactived?: StyleProp<TextStyle>,
86+
},
87+
/** 选中当前项的下标 */
88+
value?: number,
89+
/** value 改变时触发 */
90+
onChange?: (value: number)=>unknown,
91+
}
92+
```

packages/core/src/Picker/index.tsx

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import React, { useState, useEffect, useRef, useMemo } from 'react';
2+
import {
3+
View,
4+
StyleSheet,
5+
StyleProp,
6+
TextStyle,
7+
ViewStyle,
8+
LayoutChangeEvent,
9+
TouchableOpacity,
10+
Text,
11+
ScrollView,
12+
Animated,
13+
NativeSyntheticEvent,
14+
NativeScrollEvent,
15+
} from 'react-native';
16+
17+
export interface PickerDate {
18+
label?: string;
19+
render?: (key: string, record: PickerDate, index: number) => React.ReactNode;
20+
[key: string]: any;
21+
}
22+
23+
export interface PickerProps {
24+
/** 显示几行, 默认 3 */
25+
lines?: number;
26+
/** 指定需要显示的 key, 默认使用 data 的 label 属性 */
27+
key?: string;
28+
/** 需要渲染的数据 */
29+
date?: Array<PickerDate>;
30+
/** item 容器样式 */
31+
containerStyle?: {
32+
/** 激活的容器样式 */
33+
actived?: StyleProp<ViewStyle>;
34+
/** 未激活的容器样式 */
35+
unactived?: StyleProp<ViewStyle>;
36+
};
37+
/** 容器的文本样式 */
38+
textStyle?: {
39+
/** 激活的文本样式 */
40+
actived?: StyleProp<TextStyle>;
41+
/** 未激活的文本样式 */
42+
unactived?: StyleProp<TextStyle>;
43+
};
44+
/** 选中当前项的下标 */
45+
value?: number;
46+
/** value 改变时触发 */
47+
onChange?: (value: number) => unknown;
48+
}
49+
50+
const Picker = (props: PickerProps) => {
51+
const {
52+
lines = 3,
53+
key = 'label',
54+
date = new Array<PickerDate>(),
55+
containerStyle = {},
56+
textStyle = {},
57+
value = 0,
58+
onChange,
59+
} = props;
60+
const scrollView = useRef<ScrollView>();
61+
const ItemHeights = useRef<Array<number>>([]).current;
62+
const onPressORonScroll = useRef<'onPress' | 'onScroll'>('onScroll');
63+
const timer = useRef<NodeJS.Timeout>();
64+
const saveY = useRef<number>(0);
65+
const Y = useRef(new Animated.Value(0)).current;
66+
const currentY = useRef<number>(0);
67+
const isTouchEnd = useRef<boolean>(false);
68+
const isScroll = useRef<boolean>(false);
69+
const [current, setCurrent] = useState(0);
70+
useEffect(() => {
71+
onChange?.(current);
72+
}, [current]);
73+
useEffect(() => {
74+
if (value !== current) {
75+
const leng = value > date.length - 1 ? date.length - 1 : value;
76+
location((style.containerHeight as number) * (leng + 1), leng);
77+
setCurrent(leng);
78+
}
79+
}, [value]);
80+
const style = useMemo(() => {
81+
const containerUn = StyleSheet.flatten([styles.container, containerStyle.unactived]);
82+
const containerAc = StyleSheet.flatten([styles.container, styles.border, containerStyle.actived]);
83+
const textUn = StyleSheet.flatten([styles.textStyle, textStyle.unactived]);
84+
const textAc = StyleSheet.flatten([styles.textStyle, styles.acTextStyle, textStyle.unactived]);
85+
const containerHeight = containerUn.height || 50;
86+
return {
87+
containerAc,
88+
containerUn,
89+
textUn,
90+
textAc,
91+
containerHeight,
92+
};
93+
}, [containerStyle, textStyle]);
94+
const location = (scrollY: number, index: number) => {
95+
scrollView.current?.scrollTo({ x: 0, y: scrollY - (style.containerHeight as number), animated: true });
96+
currentY.current = index;
97+
};
98+
const scrollYEnd = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
99+
if (onPressORonScroll.current === 'onScroll') {
100+
return;
101+
}
102+
const scrollY = event.nativeEvent.contentOffset.y;
103+
const currentHeight = ItemHeights[currentY.current] - ItemHeights[0];
104+
if (scrollY - 2 <= currentHeight && scrollY + 2 >= currentHeight) {
105+
setCurrent(currentY.current);
106+
}
107+
onPressORonScroll.current = 'onScroll';
108+
};
109+
const setScrollY = () => {
110+
if (onPressORonScroll.current === 'onPress') {
111+
return false;
112+
}
113+
isTouchEnd.current = true;
114+
if (!isTouchEnd.current || !isScroll.current) {
115+
return false;
116+
}
117+
if (saveY.current <= ItemHeights[0] / 1.1) {
118+
scrollView.current?.scrollTo({ x: 0, y: 0, animated: true });
119+
setCurrent(0);
120+
return false;
121+
}
122+
const spot = saveY.current / ItemHeights[0] + '';
123+
const integer = Number(spot.substr(0, spot.indexOf('.')));
124+
const decimal = Number(spot[spot.indexOf('.') + 1]);
125+
const itemIndex = decimal >= 9 ? integer + 1 : integer;
126+
scrollView.current?.scrollTo({ x: 0, y: ItemHeights[itemIndex] - ItemHeights[0], animated: true });
127+
setCurrent(itemIndex);
128+
isScroll.current = false;
129+
isTouchEnd.current = false;
130+
};
131+
const getItemHeight = (event: LayoutChangeEvent) => {
132+
const { height } = event.nativeEvent.layout;
133+
ItemHeights?.push(height * ItemHeights.length + height);
134+
};
135+
return (
136+
<View style={{ paddingVertical: 10, height: (style.containerHeight as number) * lines + 10 }}>
137+
<ScrollView
138+
showsVerticalScrollIndicator={false}
139+
style={{ marginTop: -1 }}
140+
ref={scrollView as React.LegacyRef<ScrollView>}
141+
onMomentumScrollEnd={scrollYEnd}
142+
scrollEventThrottle={16}
143+
onTouchEnd={setScrollY}
144+
onScroll={Animated.event([{ nativeEvent: { contentOffset: { y: Y } } }], {
145+
listener: (event: NativeSyntheticEvent<NativeScrollEvent>) => {
146+
if (onPressORonScroll.current === 'onPress' || !isTouchEnd.current) {
147+
return false;
148+
}
149+
isScroll.current = false;
150+
saveY.current = event.nativeEvent.contentOffset.y;
151+
if (timer.current) {
152+
clearTimeout(timer.current!);
153+
timer.current = undefined;
154+
}
155+
timer.current = setTimeout(() => {
156+
isScroll.current = true;
157+
setScrollY();
158+
clearTimeout(timer.current!);
159+
timer.current = undefined;
160+
}, 160);
161+
},
162+
useNativeDriver: false,
163+
})}
164+
>
165+
{date.map((item, index) => (
166+
<TouchableOpacity
167+
onLayout={getItemHeight}
168+
key={index}
169+
activeOpacity={1}
170+
onPress={() => {
171+
onPressORonScroll.current = 'onPress';
172+
location(ItemHeights![index], index);
173+
}}
174+
>
175+
{React.isValidElement(item.render?.(item[key], item, index)) ? (
176+
item.render?.(item[key], item, index)
177+
) : (
178+
<View style={style.containerUn}>
179+
<Text style={current === index ? style.textAc : style.textUn}>{item[key]}</Text>
180+
</View>
181+
)}
182+
</TouchableOpacity>
183+
))}
184+
{new Array(lines - 1).fill('').map((item, index) => (
185+
<View key={index} style={style.containerUn} />
186+
))}
187+
{/* style.containerHeight as number * lines + 10 */}
188+
</ScrollView>
189+
<View style={[style.containerAc, { top: (-style.containerHeight as number) * lines + 10 }]} />
190+
<View style={[style.containerAc, { top: (-style.containerHeight as number) * (lines - 1) + 10 }]} />
191+
</View>
192+
);
193+
};
194+
195+
const styles = StyleSheet.create({
196+
container: {
197+
height: 50,
198+
justifyContent: 'center',
199+
alignItems: 'center',
200+
},
201+
border: {
202+
backgroundColor: '#E6E6E6',
203+
height: 1,
204+
position: 'relative',
205+
zIndex: 999,
206+
},
207+
textStyle: {
208+
fontSize: 20,
209+
color: '#000',
210+
},
211+
acTextStyle: {
212+
color: '#fd8a00',
213+
},
214+
});
215+
216+
export default Picker;

0 commit comments

Comments
 (0)