unicorn-demo-app 6.25.0 → 6.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unicorn-demo-app",
3
- "version": "6.25.0",
3
+ "version": "6.26.0",
4
4
  "main": "src/index.js",
5
5
  "author": "Ethan Sharabi <ethan.shar@gmail.com>",
6
6
  "license": "MIT",
@@ -25,6 +25,13 @@ interface RadioGroupOptions {
25
25
  interface BooleanGroupOptions {
26
26
  spread?: boolean;
27
27
  afterValueChanged?: () => void;
28
+ state?: boolean;
29
+ setState?: React.Dispatch<React.SetStateAction<boolean>>;
30
+ }
31
+
32
+ interface SegmentsExtraOptions {
33
+ state?: string;
34
+ setState?: React.Dispatch<React.SetStateAction<any /** no suitable solution for enum */>>;
28
35
  }
29
36
 
30
37
  export function renderHeader(title: string, others?: TextProps) {
@@ -35,19 +42,29 @@ export function renderHeader(title: string, others?: TextProps) {
35
42
  );
36
43
  }
37
44
 
38
- export function renderBooleanOption(title: string, key: string, {spread, afterValueChanged}: BooleanGroupOptions = {spread: true}) {
45
+ export function renderBooleanOption(title: string,
46
+ key: string,
47
+ {spread, afterValueChanged, state, setState}: BooleanGroupOptions = {spread: true}) {
39
48
  // @ts-ignore
40
- const value = this.state[key];
49
+ const value = state ?? this.state[key];
41
50
  return (
42
51
  <View row centerV spread={spread} marginB-s4 key={key}>
43
- <Text $textDefault flex={spread} marginR-s4={!spread}>{title}</Text>
52
+ <Text $textDefault flex={spread} marginR-s4={!spread}>
53
+ {title}
54
+ </Text>
44
55
  <Switch
45
56
  useCustomTheme
46
57
  key={key}
47
58
  testID={key}
48
59
  value={value}
49
- // @ts-ignore
50
- onValueChange={value => this.setState({[key]: value}, afterValueChanged)}
60
+ onValueChange={value => {
61
+ if (setState) {
62
+ setState(value);
63
+ } else {
64
+ // @ts-ignore
65
+ this.setState({[key]: value}, afterValueChanged);
66
+ }
67
+ }}
51
68
  />
52
69
  </View>
53
70
  );
@@ -130,7 +147,9 @@ export function renderColorOption(title: string,
130
147
  const value = this.state[key];
131
148
  return (
132
149
  <View marginV-s2>
133
- <Text text70M $textDefault>{title}</Text>
150
+ <Text text70M $textDefault>
151
+ {title}
152
+ </Text>
134
153
  <ColorPalette
135
154
  value={value}
136
155
  colors={colors}
@@ -171,19 +190,34 @@ export function renderSliderOption(title: string,
171
190
  );
172
191
  }
173
192
 
174
- export function renderMultipleSegmentOptions(title: string, key: string, options: (SegmentedControlItemProps & {value: any})[]) {
193
+ export function renderMultipleSegmentOptions(title: string,
194
+ key: string,
195
+ options: (SegmentedControlItemProps & {value: any})[],
196
+ {state, setState}: SegmentsExtraOptions = {}) {
175
197
  // @ts-ignore
176
- const value = this.state[key];
198
+ const value = state ?? this.state[key];
177
199
  const index = _.findIndex(options, {value});
178
200
 
179
201
  return (
180
202
  <View row centerV spread marginB-s4 key={key}>
181
- {!!title && <Text $textDefault marginR-s2>{title}</Text>}
203
+ {!!title && (
204
+ <Text $textDefault marginR-s2>
205
+ {title}
206
+ </Text>
207
+ )}
182
208
  <SegmentedControl
183
209
  initialIndex={index}
184
210
  segments={options}
185
211
  // @ts-ignore
186
- onChangeIndex={index => this.setState({[key]: options[index].value})}
212
+ onChangeIndex={index => {
213
+ const value = options[index].value;
214
+ if (setState) {
215
+ setState(value);
216
+ } else {
217
+ // @ts-ignore
218
+ this.setState({[key]: value});
219
+ }
220
+ }}
187
221
  />
188
222
  </View>
189
223
  );
@@ -54,6 +54,7 @@ export const navigationData = {
54
54
  {title: 'Color Picker', tags: 'color picker control', screen: 'unicorn.components.ColorPickerScreen'},
55
55
  {title: 'Color Swatch', tags: 'color swatch and palette', screen: 'unicorn.components.ColorSwatchScreen'},
56
56
  {title: 'TextField', tags: 'text input field form', screen: 'unicorn.components.TextFieldScreen'},
57
+ {title: 'NumberInput', tags: 'number input', screen: 'unicorn.components.NumberInputScreen'},
57
58
  {title: 'Picker', tags: 'picker form', screen: 'unicorn.components.PickerScreen'},
58
59
  {title: 'DateTimePicker', tags: 'date time picker form', screen: 'unicorn.components.DateTimePickerScreen'},
59
60
  {title: 'RadioButton', tags: 'radio button group controls', screen: 'unicorn.components.RadioButtonScreen'},
@@ -0,0 +1,230 @@
1
+ import React, {useState, useCallback, useRef, useMemo} from 'react';
2
+ import {StyleSheet, TouchableWithoutFeedback, Keyboard as RNKeyboard} from 'react-native';
3
+ import {gestureHandlerRootHOC} from 'react-native-gesture-handler';
4
+ import {
5
+ Text,
6
+ Spacings,
7
+ NumberInput,
8
+ NumberInputData,
9
+ View,
10
+ Typography,
11
+ Constants,
12
+ Incubator
13
+ } from 'react-native-ui-lib';
14
+ import {renderBooleanOption, renderMultipleSegmentOptions} from '../ExampleScreenPresenter';
15
+
16
+ enum ExampleTypeEnum {
17
+ PRICE = 'price',
18
+ PERCENTAGE = 'percentage',
19
+ ANY_NUMBER = 'number'
20
+ }
21
+
22
+ type ExampleType = ExampleTypeEnum | `${ExampleTypeEnum}`;
23
+
24
+ const VALIDATION_MESSAGE = 'Please enter a valid number';
25
+ const MINIMUM_PRICE = 5000;
26
+ const MINIMUM_PRICE_VALIDATION_MESSAGE = `Make sure your number is above ${MINIMUM_PRICE}`;
27
+ const DISCOUNT_PERCENTAGE = {min: 1, max: 80};
28
+ // eslint-disable-next-line max-len
29
+ const DISCOUNT_PERCENTAGE_VALIDATION_MESSAGE = `Make sure your number is between ${DISCOUNT_PERCENTAGE.min} and ${DISCOUNT_PERCENTAGE.max}`;
30
+
31
+ const NumberInputScreen = () => {
32
+ const currentData = useRef<NumberInputData>();
33
+ const [text, setText] = useState<string>('');
34
+ const [showLabel, setShowLabel] = useState<boolean>(true);
35
+ const [exampleType, setExampleType] = useState<ExampleType>('price');
36
+
37
+ const processInput = useCallback(() => {
38
+ let newText = '';
39
+ if (currentData.current) {
40
+ switch (currentData.current.type) {
41
+ case 'valid':
42
+ newText = currentData.current.formattedNumber;
43
+ break;
44
+ case 'empty':
45
+ newText = 'Empty';
46
+ break;
47
+ case 'error':
48
+ newText = `Error: value '${currentData.current.userInput}' is invalid`;
49
+ break;
50
+ }
51
+ }
52
+
53
+ setText(newText);
54
+ }, []);
55
+
56
+ const onChangeNumber = useCallback((data: NumberInputData) => {
57
+ currentData.current = data;
58
+ processInput();
59
+ },
60
+ [processInput]);
61
+
62
+ const label = useMemo(() => {
63
+ if (showLabel) {
64
+ switch (exampleType) {
65
+ case 'price':
66
+ default:
67
+ return 'Enter price';
68
+ case 'percentage':
69
+ return 'Enter discount percentage';
70
+ case 'number':
71
+ return 'Enter any number';
72
+ }
73
+ }
74
+ }, [showLabel, exampleType]);
75
+
76
+ const placeholder = useMemo(() => {
77
+ switch (exampleType) {
78
+ case 'price':
79
+ default:
80
+ return 'Price';
81
+ case 'percentage':
82
+ return 'Discount';
83
+ case 'number':
84
+ return 'Any number';
85
+ }
86
+ }, [exampleType]);
87
+
88
+ const fractionDigits = useMemo(() => {
89
+ switch (exampleType) {
90
+ case 'price':
91
+ case 'number':
92
+ default:
93
+ return undefined;
94
+ case 'percentage':
95
+ return 0;
96
+ }
97
+ }, [exampleType]);
98
+
99
+ const leadingText = useMemo(() => {
100
+ switch (exampleType) {
101
+ case 'price':
102
+ return '$';
103
+ case 'percentage':
104
+ case 'number':
105
+ default:
106
+ return undefined;
107
+ }
108
+ }, [exampleType]);
109
+
110
+ const trailingText = useMemo(() => {
111
+ switch (exampleType) {
112
+ case 'percentage':
113
+ return '%';
114
+ case 'price':
115
+ case 'number':
116
+ default:
117
+ return undefined;
118
+ }
119
+ }, [exampleType]);
120
+
121
+ const isValid = useCallback(() => {
122
+ return currentData.current?.type === 'valid';
123
+ }, []);
124
+
125
+ const isAboveMinimumPrice = useCallback(() => {
126
+ if (currentData.current?.type === 'valid') {
127
+ return currentData.current.number > MINIMUM_PRICE;
128
+ }
129
+ }, []);
130
+
131
+ const isWithinDiscountPercentage = useCallback(() => {
132
+ if (currentData.current?.type === 'valid') {
133
+ return (
134
+ currentData.current.number >= DISCOUNT_PERCENTAGE.min && currentData.current.number <= DISCOUNT_PERCENTAGE.max
135
+ );
136
+ }
137
+ }, []);
138
+
139
+ const validate = useMemo((): Incubator.TextFieldProps['validate'] => {
140
+ switch (exampleType) {
141
+ case 'price':
142
+ return [isValid, isAboveMinimumPrice];
143
+ case 'percentage':
144
+ return [isValid, isWithinDiscountPercentage];
145
+ default:
146
+ return isValid;
147
+ }
148
+ }, [exampleType, isValid, isAboveMinimumPrice, isWithinDiscountPercentage]);
149
+
150
+ const validationMessage = useMemo((): Incubator.TextFieldProps['validationMessage'] => {
151
+ switch (exampleType) {
152
+ case 'price':
153
+ return [VALIDATION_MESSAGE, MINIMUM_PRICE_VALIDATION_MESSAGE];
154
+ case 'percentage':
155
+ return [VALIDATION_MESSAGE, DISCOUNT_PERCENTAGE_VALIDATION_MESSAGE];
156
+ default:
157
+ return VALIDATION_MESSAGE;
158
+ }
159
+ }, [exampleType]);
160
+
161
+ return (
162
+ <TouchableWithoutFeedback onPress={RNKeyboard.dismiss}>
163
+ <View flex centerH>
164
+ <Text text40 margin-s10>
165
+ Number Input
166
+ </Text>
167
+ {renderBooleanOption('Show label', 'showLabel', {spread: false, state: showLabel, setState: setShowLabel})}
168
+ {renderMultipleSegmentOptions('',
169
+ 'exampleType',
170
+ [
171
+ {label: 'Price', value: ExampleTypeEnum.PRICE},
172
+ {label: 'Percentage', value: ExampleTypeEnum.PERCENTAGE},
173
+ {label: 'Number', value: ExampleTypeEnum.ANY_NUMBER}
174
+ ],
175
+ {state: exampleType, setState: setExampleType})}
176
+
177
+ <View flex center>
178
+ <NumberInput
179
+ key={exampleType}
180
+ // initialNumber={100}
181
+ label={label}
182
+ labelStyle={styles.label}
183
+ placeholder={placeholder}
184
+ fractionDigits={fractionDigits}
185
+ onChangeNumber={onChangeNumber}
186
+ leadingText={leadingText}
187
+ leadingTextStyle={leadingText && [styles.infoText, {marginLeft: Spacings.s4}]}
188
+ trailingText={trailingText}
189
+ trailingTextStyle={trailingText && [styles.infoText, {marginRight: Spacings.s4}]}
190
+ style={[
191
+ styles.mainText,
192
+ !leadingText && {marginLeft: Spacings.s4},
193
+ !trailingText && {marginRight: Spacings.s4}
194
+ ]}
195
+ containerStyle={styles.containerStyle}
196
+ validate={validate}
197
+ validationMessage={validationMessage}
198
+ validationMessageStyle={Typography.text80M}
199
+ validateOnChange
200
+ centered
201
+ />
202
+ <Text marginT-s5>{text}</Text>
203
+ </View>
204
+ </View>
205
+ </TouchableWithoutFeedback>
206
+ );
207
+ };
208
+
209
+ export default gestureHandlerRootHOC(NumberInputScreen);
210
+
211
+ const styles = StyleSheet.create({
212
+ containerStyle: {
213
+ marginBottom: 30,
214
+ marginLeft: Spacings.s5,
215
+ marginRight: Spacings.s5
216
+ },
217
+ mainText: {
218
+ height: 36,
219
+ marginVertical: Spacings.s1,
220
+ ...Typography.text30M
221
+ },
222
+ infoText: {
223
+ marginTop: Constants.isIOS ? Spacings.s2 : 0,
224
+ ...Typography.text50M
225
+ },
226
+ label: {
227
+ marginBottom: Spacings.s1,
228
+ ...Typography.text80M
229
+ }
230
+ });
@@ -1,37 +1,52 @@
1
1
  import _ from 'lodash';
2
- import React, {useState, useCallback} from 'react';
3
- import {Alert} from 'react-native';
4
- import {Text, View, SectionsWheelPicker, SegmentedControl, Button, Incubator} from 'react-native-ui-lib';
2
+ import React, {useState, useCallback, useMemo} from 'react';
3
+ import {Alert, StyleSheet} from 'react-native';
4
+ import {
5
+ Text,
6
+ View,
7
+ SectionsWheelPicker,
8
+ SegmentedControl,
9
+ Button,
10
+ Incubator,
11
+ Constants,
12
+ Switch,
13
+ Colors
14
+ } from 'react-native-ui-lib';
5
15
 
6
16
  const {WheelPicker} = Incubator;
17
+
18
+ const DAYS = _.times(10, i => i);
19
+ const HOURS = _.times(24, i => i);
20
+ const MINUTES = _.times(60, i => i);
21
+
7
22
  const SectionsWheelPickerScreen = () => {
8
23
  const [numOfSections, setNumOfSections] = useState(1);
9
-
24
+ const [disableRTL, setDisableRTL] = useState(false);
10
25
  const [selectedDays, setSelectedDays] = useState(0);
11
26
  const [selectedHours, setSelectedHours] = useState(0);
12
27
  const [selectedMinutes, setSelectedMinutes] = useState(0);
13
28
 
14
- const days = _.times(10, i => i);
15
- const hours = _.times(24, i => i);
16
- const minutes = _.times(60, i => i);
29
+ const shouldDisableRTL = useMemo(() => {
30
+ return Constants.isRTL && disableRTL;
31
+ }, [disableRTL]);
17
32
 
18
33
  const getItems = useCallback(values => {
19
34
  return _.map(values, item => ({label: '' + item, value: item}));
20
35
  }, []);
21
36
 
22
- const onDaysChange = (item: number | string) => {
37
+ const onDaysChange = useCallback((item: number | string) => {
23
38
  setSelectedDays(item as number);
24
- };
39
+ }, []);
25
40
 
26
- const onHoursChange = (item: number | string) => {
41
+ const onHoursChange = useCallback((item: number | string) => {
27
42
  setSelectedHours(item as number);
28
- };
43
+ }, []);
29
44
 
30
- const onMinutesChange = (item: number | string) => {
45
+ const onMinutesChange = useCallback((item: number | string) => {
31
46
  setSelectedMinutes(item as number);
32
- };
47
+ }, []);
33
48
 
34
- const onSavePress = () => {
49
+ const onSavePress = useCallback(() => {
35
50
  const days = selectedDays === 1 ? 'day' : 'days';
36
51
  const hours = selectedHours === 1 ? 'hour' : 'hours';
37
52
  const minutes = selectedMinutes === 1 ? 'minute' : 'minutes';
@@ -52,67 +67,132 @@ const SectionsWheelPickerScreen = () => {
52
67
  : numOfSections === 2
53
68
  ? Alert.alert('Your chosen duration is:\n' + selectedDays + ' ' + days + ' and ' + selectedHours + ' ' + hours)
54
69
  : Alert.alert('Your chosen duration is:\n' + selectedDays + ' ' + days);
55
- };
70
+ }, [numOfSections, selectedDays, selectedHours, selectedMinutes]);
56
71
 
57
- const onResetPress = () => {
72
+ const onResetPress = useCallback(() => {
58
73
  setSelectedDays(0);
59
74
  setSelectedHours(0);
60
75
  setSelectedMinutes(0);
61
- };
62
-
63
- const sections: Incubator.WheelPickerProps[] = [
64
- {
65
- items: getItems(days),
66
- onChange: onDaysChange,
67
- initialValue: selectedDays,
68
- label: 'Days',
69
- align: numOfSections === 1 ? WheelPicker.alignments.CENTER : WheelPicker.alignments.RIGHT,
70
- style: {flex: 1}
71
- },
72
- {
73
- items: getItems(hours),
74
- onChange: onHoursChange,
75
- initialValue: selectedHours,
76
- label: 'Hrs',
77
- align: numOfSections === 2 ? WheelPicker.alignments.LEFT : WheelPicker.alignments.CENTER,
78
- style: numOfSections === 2 ? {flex: 1} : undefined
79
- },
80
- {
81
- items: getItems(minutes),
82
- onChange: onMinutesChange,
83
- initialValue: selectedMinutes,
84
- label: 'Mins',
85
- align: WheelPicker.alignments.LEFT,
86
- style: {flex: 1}
87
- }
88
- ];
89
-
90
- const sectionsToPresent = _.slice(sections, 0, numOfSections);
91
-
92
- const onChangeIndex = (index: number) => {
76
+ }, []);
77
+
78
+ const sections: Incubator.WheelPickerProps[] = useMemo(() => {
79
+ return [
80
+ {
81
+ items: getItems(DAYS),
82
+ onChange: onDaysChange,
83
+ initialValue: selectedDays,
84
+ label: Constants.isRTL ? 'ימים' : 'Days',
85
+ align:
86
+ numOfSections === 1
87
+ ? WheelPicker.alignments.CENTER
88
+ : shouldDisableRTL
89
+ ? WheelPicker.alignments.LEFT
90
+ : WheelPicker.alignments.RIGHT,
91
+ style: {
92
+ flex: 1,
93
+ flexDirection: numOfSections !== 1 && Constants.isRTL && !disableRTL ? 'row-reverse' : undefined
94
+ }
95
+ },
96
+ {
97
+ items: getItems(HOURS),
98
+ onChange: onHoursChange,
99
+ initialValue: selectedHours,
100
+ label: Constants.isRTL ? 'שעות' : 'Hrs',
101
+ align:
102
+ numOfSections === 2
103
+ ? shouldDisableRTL
104
+ ? WheelPicker.alignments.RIGHT
105
+ : WheelPicker.alignments.LEFT
106
+ : WheelPicker.alignments.CENTER,
107
+ style: numOfSections === 2 ? {flex: 1, flexDirection: shouldDisableRTL ? 'row-reverse' : 'row'} : undefined
108
+ },
109
+ {
110
+ items: getItems(MINUTES),
111
+ onChange: onMinutesChange,
112
+ initialValue: selectedMinutes,
113
+ label: Constants.isRTL ? 'דקות' : 'Mins',
114
+ align: shouldDisableRTL ? WheelPicker.alignments.RIGHT : WheelPicker.alignments.LEFT,
115
+ style: {flex: 1, flexDirection: shouldDisableRTL ? 'row-reverse' : 'row'}
116
+ }
117
+ ];
118
+ }, [
119
+ getItems,
120
+ disableRTL,
121
+ selectedDays,
122
+ selectedHours,
123
+ selectedMinutes,
124
+ onDaysChange,
125
+ onHoursChange,
126
+ onMinutesChange,
127
+ numOfSections,
128
+ shouldDisableRTL
129
+ ]);
130
+
131
+ const sectionsToPresent = useMemo(() => _.slice(sections, 0, numOfSections), [numOfSections, sections]);
132
+
133
+ const timeSections = useMemo(() => {
134
+ return [
135
+ {
136
+ items: getItems(_.times(24, i => i + 1))
137
+ },
138
+ {
139
+ items: getItems(_.times(12, i => {
140
+ if (i < 2) {
141
+ return `0${i * 5}`;
142
+ }
143
+ return i * 5;
144
+ }))
145
+ }
146
+ ];
147
+ }, [getItems]);
148
+
149
+ const onChangeIndex = useCallback((index: number) => {
93
150
  return setNumOfSections(index + 1);
94
- };
151
+ }, []);
152
+
153
+ const updateDisableRTLValue = useCallback((value: boolean) => {
154
+ setDisableRTL(value);
155
+ }, []);
95
156
 
96
157
  return (
97
158
  <View>
98
159
  <Text text40 marginL-10 marginT-20>
99
160
  Sections Wheel Picker
100
161
  </Text>
101
- <View centerH marginT-40>
162
+ <View row center style={styles.bottomDivider}>
163
+ <Text margin-s5> Disable RTL</Text>
164
+ <Switch value={shouldDisableRTL} onValueChange={updateDisableRTLValue}/>
165
+ </View>
166
+ <View centerH marginT-20>
167
+ <Text text60 marginB-20>
168
+ Pick a duration
169
+ </Text>
102
170
  <SegmentedControl
103
171
  segments={[{label: '1 section'}, {label: '2 sections'}, {label: '3 sections'}]}
104
172
  onChangeIndex={onChangeIndex}
105
173
  throttleTime={400}
106
174
  />
107
- <Text text50 marginV-20>
108
- Pick a duration
175
+ </View>
176
+ <SectionsWheelPicker numberOfVisibleRows={4} disableRTL={disableRTL} sections={sectionsToPresent}/>
177
+ <View paddingB-20 center spread row style={styles.bottomDivider}>
178
+ <Button marginR-40 link label={'Save'} onPress={onSavePress}/>
179
+ <Button label={'Reset'} link onPress={onResetPress}/>
180
+ </View>
181
+ <View>
182
+ <Text center text60 marginV-20>
183
+ Pick a time
109
184
  </Text>
185
+ <SectionsWheelPicker disableRTL={disableRTL} sections={timeSections}/>
110
186
  </View>
111
- <SectionsWheelPicker sections={sectionsToPresent}/>
112
- <Button marginH-150 marginT-40 label={'Save'} onPress={onSavePress}/>
113
- <Button marginH-150 marginT-15 label={'Reset'} onPress={onResetPress}/>
114
187
  </View>
115
188
  );
116
189
  };
117
190
 
118
191
  export default SectionsWheelPickerScreen;
192
+
193
+ const styles = StyleSheet.create({
194
+ bottomDivider: {
195
+ borderBottomColor: Colors.$outlineDefault,
196
+ borderBottomWidth: 4
197
+ }
198
+ });
@@ -31,6 +31,7 @@ export function registerScreens(registrar) {
31
31
  registrar('unicorn.components.KeyboardAwareScrollViewScreen', () => require('./KeyboardAwareScrollViewScreen').default);
32
32
  registrar('unicorn.components.MaskedInputScreen', () => require('./MaskedInputScreen').default);
33
33
  registrar('unicorn.components.MarqueeScreen', () => require('./MarqueeScreen').default);
34
+ registrar('unicorn.components.NumberInputScreen', () => require('./NumberInputScreen').default);
34
35
  registrar('unicorn.components.OverlaysScreen', () => require('./OverlaysScreen').default);
35
36
  registrar('unicorn.components.PageControlScreen', () => require('./PageControlScreen').default);
36
37
  registrar('unicorn.components.PanDismissibleScreen', () => require('./PanDismissibleScreen').default);
@@ -277,12 +277,7 @@ export default class TextFieldScreen extends Component {
277
277
  Centered
278
278
  </Text>
279
279
 
280
- <TextField
281
- label="PIN"
282
- placeholder="XXXX"
283
- labelStyle={{alignSelf: 'center'}}
284
- containerStyle={{alignSelf: 'center'}}
285
- />
280
+ <TextField label="PIN" placeholder="XXXX" centered/>
286
281
  </View>
287
282
  <KeyboardAwareInsetsView/>
288
283
  </ScrollView>