Skip to content

Commit 78347bc

Browse files
authored
Feat/new pan view with reanimated 2 (#1404)
* New PanView - somewhat working * Small refactor * Add worklet * Fix typings * Do not add GestureHandlerRootView by default * Move to Incubator * Move to Incubator (2) * Add chevronDown to assets and use it from there * Rename to PanView... * Improve docs * Add TODO * Increase damping * Reorder imports * Use Object.assign * Move GestureHandlerRootView to the screen * Fix tests (import) * Remove translationLock * Use typeof instead of static types * Remove unused and unecessary styles * Move default value of directions * Add springBack * Allow returning to the original location * Rename const
1 parent 02ef6b2 commit 78347bc

File tree

14 files changed

+1339
-1
lines changed

14 files changed

+1339
-1
lines changed

demo/src/configurations.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {Assets, Colors, Typography, Spacings, Incubator} from 'react-native-ui-lib'; // eslint-disable-line
22

33
Assets.loadAssetsGroup('icons.demo', {
4+
chevronDown: require('./assets/icons/chevronDown.png'),
45
add: require('./assets/icons/add.png'),
56
camera: require('./assets/icons/cameraSelected.png'),
67
close: require('./assets/icons/close.png'),

demo/src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,9 @@ module.exports = {
225225
get IncubatorTextFieldScreen() {
226226
return require('./screens/incubatorScreens/IncubatorTextFieldScreen').default;
227227
},
228+
get PanViewScreen() {
229+
return require('./screens/incubatorScreens/PanViewScreen').default;
230+
},
228231
// realExamples
229232
get AppleMusic() {
230233
return require('./screens/realExamples/AppleMusic').default;

demo/src/screens/MenuStructure.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@ export const navigationData = {
156156
screens: [
157157
{title: 'Native TouchableOpacity', tags: 'touchable native', screen: 'unicorn.incubator.TouchableOpacityScreen'},
158158
{title: '(New) TextField', tags: 'text field input', screen: 'unicorn.components.IncubatorTextFieldScreen'},
159-
{title: 'WheelPicker (Incubator)', tags: 'wheel picker spinner experimental', screen: 'unicorn.incubator.WheelPickerScreen'}
159+
{title: 'WheelPicker (Incubator)', tags: 'wheel picker spinner experimental', screen: 'unicorn.incubator.WheelPickerScreen'},
160+
{title: 'Pan View', tags: 'pan swipe drag', screen: 'unicorn.incubator.PanViewScreen'}
160161
]
161162
},
162163
Inspirations: {
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import React, {Component} from 'react';
2+
import {StyleSheet, ScrollView} from 'react-native';
3+
import {GestureHandlerRootView, FlatList} from 'react-native-gesture-handler';
4+
import {
5+
Assets,
6+
View,
7+
Text,
8+
Colors,
9+
Incubator,
10+
Card,
11+
Constants,
12+
Modal,
13+
BorderRadiuses,
14+
Image,
15+
TouchableOpacity
16+
} from 'react-native-ui-lib';
17+
const {PanView} = Incubator;
18+
19+
interface Item {
20+
value: string;
21+
label: string;
22+
}
23+
24+
const overlayBackgroundColor = Colors.rgba(Colors.black, 0.2);
25+
const colors: Item[] = [
26+
{value: Colors.red10, label: 'Red10'},
27+
{value: Colors.red30, label: 'Red30'},
28+
{value: Colors.red50, label: 'Red50'},
29+
{value: Colors.red70, label: 'Red70'},
30+
{value: Colors.blue10, label: 'Blue10'},
31+
{value: Colors.blue30, label: 'Blue30'},
32+
{value: Colors.blue50, label: 'Blue50'},
33+
{value: Colors.blue70, label: 'Blue70'},
34+
{value: Colors.purple10, label: 'Purple10'},
35+
{value: Colors.purple30, label: 'Purple30'},
36+
{value: Colors.purple50, label: 'Purple50'},
37+
{value: Colors.purple70, label: 'Purple70'},
38+
{value: Colors.green10, label: 'Green10'},
39+
{value: Colors.green30, label: 'Green30'},
40+
{value: Colors.green50, label: 'Green50'},
41+
{value: Colors.green70, label: 'Green70'},
42+
{value: Colors.yellow10, label: 'Yellow10'},
43+
{value: Colors.yellow30, label: 'Yellow30'},
44+
{value: Colors.yellow50, label: 'Yellow50'},
45+
{value: Colors.yellow70, label: 'Yellow70'}
46+
];
47+
48+
class PanViewScreen extends Component {
49+
state = {
50+
showToast: false,
51+
showDialog: false
52+
};
53+
54+
onDialogDismissed = () => {
55+
this.setState({showDialog: false});
56+
};
57+
58+
keyExtractor = (item: Item) => {
59+
return item.value;
60+
};
61+
62+
renderVerticalItem = ({item}: {item: Item}) => {
63+
return (
64+
<Text text50 margin-20 color={item.value}>
65+
{item.label}
66+
</Text>
67+
);
68+
};
69+
70+
renderDialog = () => {
71+
const Container = Constants.isAndroid ? GestureHandlerRootView : React.Fragment;
72+
const containerProps = Constants.isAndroid ? {style: styles.gestureHandler} : {};
73+
return (
74+
<View flex>
75+
<Modal
76+
transparent
77+
onBackgroundPress={this.onDialogDismissed}
78+
overlayBackgroundColor={overlayBackgroundColor}
79+
visible
80+
>
81+
<Container {...containerProps}>
82+
<PanView
83+
directions={[PanView.directions.DOWN]}
84+
dismissible
85+
springBack
86+
// threshold={{y: 10}}
87+
containerStyle={styles.panView}
88+
onDismiss={this.onDialogDismissed}
89+
>
90+
<View style={styles.dialog}>
91+
<Text text60 margin-s2>
92+
Title (swipe here)
93+
</Text>
94+
<View height={1} bg-grey40/>
95+
<FlatList
96+
showsVerticalScrollIndicator={false}
97+
style={styles.verticalScroll}
98+
data={colors}
99+
renderItem={this.renderVerticalItem}
100+
keyExtractor={this.keyExtractor}
101+
/>
102+
</View>
103+
</PanView>
104+
</Container>
105+
</Modal>
106+
</View>
107+
);
108+
};
109+
110+
onToastDismissed = () => {
111+
this.setState({showToast: false});
112+
};
113+
114+
renderToast = () => {
115+
return (
116+
<PanView
117+
directions={[PanView.directions.LEFT, PanView.directions.DOWN, PanView.directions.RIGHT]}
118+
dismissible
119+
springBack
120+
directionLock
121+
threshold={{y: 10}}
122+
containerStyle={styles.panView}
123+
onDismiss={this.onToastDismissed}
124+
>
125+
<TouchableOpacity center style={styles.toast} onPress={this.onToastDismissed}>
126+
<Text>Swipe or click to dismiss</Text>
127+
</TouchableOpacity>
128+
</PanView>
129+
);
130+
};
131+
132+
renderCard = (key: string, name: string) => {
133+
// @ts-expect-error
134+
const value = this.state[key];
135+
const text = value ? `I'm still showing or being dismissed` : `Click me (${name})`;
136+
const onPress = value ? undefined : () => this.setState({[key]: true});
137+
return (
138+
<Card margin-page onPress={onPress}>
139+
<View padding-15>
140+
<Text text30 grey30>
141+
{text}
142+
</Text>
143+
</View>
144+
</Card>
145+
);
146+
};
147+
148+
render() {
149+
const {showToast, showDialog} = this.state;
150+
const Container = showDialog ? View : GestureHandlerRootView;
151+
return (
152+
<Container style={[styles.root, styles.gestureHandler]}>
153+
<View marginL-page height={50} centerV>
154+
<Text text50>New Pan View</Text>
155+
</View>
156+
<ScrollView>
157+
{this.renderCard('showToast', 'toast')}
158+
{this.renderCard('showDialog', 'dialog')}
159+
<View height={Constants.screenHeight} centerH>
160+
<Text text50 marginB-s2>
161+
Scrollable
162+
</Text>
163+
<Image source={Assets.icons.demo.chevronDown}/>
164+
</View>
165+
</ScrollView>
166+
{showToast && this.renderToast()}
167+
{showDialog && this.renderDialog()}
168+
</Container>
169+
);
170+
}
171+
}
172+
173+
export default PanViewScreen;
174+
175+
const styles = StyleSheet.create({
176+
root: {
177+
backgroundColor: Colors.grey80
178+
},
179+
gestureHandler: {
180+
flex: 1
181+
},
182+
panView: {
183+
flex: 1,
184+
position: 'absolute',
185+
bottom: 20,
186+
alignSelf: 'center'
187+
},
188+
toast: {
189+
backgroundColor: Colors.white,
190+
width: 200,
191+
height: 40,
192+
borderRadius: BorderRadiuses.br20,
193+
borderWidth: 0.5,
194+
borderColor: Colors.grey30
195+
},
196+
dialog: {
197+
backgroundColor: Colors.white,
198+
width: 200,
199+
height: 300,
200+
borderRadius: BorderRadiuses.br20
201+
},
202+
verticalScroll: {
203+
marginTop: 20
204+
}
205+
});

demo/src/screens/incubatorScreens/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export function registerScreens(registrar) {
55
gestureHandlerRootHOC(require('./TouchableOpacityScreen').default));
66
registrar('unicorn.components.IncubatorTextFieldScreen', () => require('./IncubatorTextFieldScreen').default);
77
registrar('unicorn.incubator.WheelPickerScreen', () => gestureHandlerRootHOC(require('./WheelPickerScreen').default));
8+
registrar('unicorn.incubator.PanViewScreen', () => require('./PanViewScreen').default);
89
}

generatedTypes/incubator/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export { default as TextField, TextFieldProps, FieldContextType } from './TextFi
33
export { default as TouchableOpacity, TouchableOpacityProps } from './TouchableOpacity';
44
export { default as TouchableOpacity2 } from './TouchableOpacity2';
55
export { default as WheelPicker, WheelPickerProps } from './WheelPicker';
6+
export { default as PanView, PanViewProps, PanViewDirections, PanViewDismissThreshold } from './panView';
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React from 'react';
2+
import { StyleProp, ViewStyle } from 'react-native';
3+
import { ViewProps } from '../../components/view';
4+
import { PanViewDirections, PanViewDismissThreshold } from './panningUtil';
5+
export { PanViewDirections, PanViewDismissThreshold };
6+
export interface PanViewProps extends ViewProps {
7+
/**
8+
* The directions of the allowed pan (default is all)
9+
* Types: UP, DOWN, LEFT and RIGHT (using PanView.directions.###)
10+
*/
11+
directions?: PanViewDirections[];
12+
/**
13+
* Dismiss the view if over the threshold (translation or velocity).
14+
*/
15+
dismissible?: boolean;
16+
/**
17+
* Animate to start if not dismissed.
18+
*/
19+
springBack?: boolean;
20+
/**
21+
* Callback to the dismiss animation end
22+
*/
23+
onDismiss?: () => void;
24+
/**
25+
* Should the direction of dragging be locked once a drag has started.
26+
*/
27+
directionLock?: boolean;
28+
/**
29+
* Object to adjust the dismiss threshold limits (eg {x, y, velocity}).
30+
*/
31+
threshold?: PanViewDismissThreshold;
32+
/**
33+
* Add a style to the container
34+
*/
35+
containerStyle?: StyleProp<ViewStyle>;
36+
}
37+
interface Props extends PanViewProps {
38+
children?: React.ReactNode | React.ReactNode[];
39+
}
40+
declare const _default: React.ComponentClass<PanViewProps & {
41+
useCustomTheme?: boolean | undefined;
42+
}, any> & {
43+
(props: Props): JSX.Element;
44+
displayName: string;
45+
directions: typeof PanViewDirections;
46+
defaultProps: {
47+
threshold: Required<PanViewDismissThreshold>;
48+
};
49+
};
50+
export default _default;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { PanGestureHandlerEventPayload } from 'react-native-gesture-handler';
2+
export declare enum PanViewDirections {
3+
UP = "up",
4+
DOWN = "down",
5+
LEFT = "left",
6+
RIGHT = "right"
7+
}
8+
export interface Frame {
9+
x: number;
10+
y: number;
11+
}
12+
export interface TranslationOptions {
13+
directionLock?: boolean;
14+
currentTranslation: Frame;
15+
}
16+
export interface PanViewDismissThreshold {
17+
/**
18+
* The (positive) velocity of a drag\swipe past it the view will be dismissed.
19+
*/
20+
velocity?: number;
21+
/**
22+
* The x translation from the start location past it the view will be dismissed.
23+
*/
24+
x?: number;
25+
/**
26+
* The y translation from the start location past it the view will be dismissed.
27+
*/
28+
y?: number;
29+
}
30+
export declare function getTranslationDirectionClamp(translation: Frame, options: TranslationOptions): Frame;
31+
export declare function getTranslation(event: PanGestureHandlerEventPayload, initialTranslation: Frame, directions: PanViewDirections[], options: TranslationOptions): Frame;
32+
export declare const DEFAULT_THRESHOLD: Required<PanViewDismissThreshold>;
33+
/**
34+
* Will return undefined if should not dismiss
35+
*/
36+
export declare function getDismissVelocity(event: PanGestureHandlerEventPayload, directions: PanViewDirections[], options: TranslationOptions, threshold?: PanViewDismissThreshold): Partial<Frame> | undefined;

jest-setup.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ jest.spyOn(AccessibilityInfo, 'isScreenReaderEnabled').mockImplementation(() =>
77
jest.mock('@react-native-community/blur', () => {});
88
jest.mock('@react-native-community/netinfo', () => {});
99
jest.mock('react-native-reanimated', () => require('react-native-reanimated/mock'));
10+
global.__reanimatedWorkletInit = jest.fn();
1011
jest.mock('react-native-gesture-handler', () => {});
1112
jest.mock('@react-native-picker/picker', () => ({Picker: {Item: {}}}));

src/incubator/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export {default as TextField, TextFieldProps, FieldContextType} from './TextFiel
44
export {default as TouchableOpacity, TouchableOpacityProps} from './TouchableOpacity';
55
export {default as TouchableOpacity2} from './TouchableOpacity2';
66
export {default as WheelPicker, WheelPickerProps} from './WheelPicker';
7+
export {default as PanView, PanViewProps, PanViewDirections, PanViewDismissThreshold} from './panView';

0 commit comments

Comments
 (0)