ژست های لمسی (Gestures) در React Native — راهنمای کاربردی
ژستهای لمسی (Gestures) نقش مهمی در شیوه تعامل کاربران گوشیهای هوشمند با اپلیکیشنها دارند. در این راهنما با شیوه افزودن ژست لمسی در React Native از طریق PanResponder آشنا میشویم.
شروع
ما در این مقاله از create-react-native-app برای ساخت اپلیکیشن خود استفاده میکنیم.
1
در آغاز دستور زیر را از ترمینال اجرا کنید:
create-react-native-app rn-js-navigator cd rn-js-navigator
محتوای App.js را به صورت زیر عوض کنید:
1import React from 'react';
2import { StyleSheet, View, Button } from 'react-native';
3import { Navigator, Route } from './Navigator';
4
5const Screen1 = ({ navigator }) => (
6 <View style={[styles.screen, { backgroundColor: '#59C9A5' }]}>
7 <Button
8 title="Screen 2"
9 onPress={() => navigator.push('Screen2')}
10 />
11 <Button
12 title="Pop"
13 onPress={() => navigator.pop()}
14 />
15 </View>
16);
17
18const Screen2 = ({ navigator }) => (
19 <View style={[styles.screen, { backgroundColor: '#23395B' }]}>
20 <Button
21 title="Screen 3"
22 onPress={() => navigator.push('Screen3')}
23 />
24 <Button
25 title="Pop"
26 onPress={() => navigator.pop()}
27 />
28 </View>
29);
30
31const Screen3 = ({ navigator }) => (
32 <View style={[styles.screen, { backgroundColor: '#B9E3C6' }]}>
33 <Button
34 title="Pop"
35 onPress={() => navigator.pop()}
36 />
37 </View>
38);
39
40export default class App extends React.Component {
41 render() {
42 return (
43 <Navigator>
44 <Route name="Screen1" component={Screen1} />
45 <Route name="Screen2" component={Screen2} />
46 <Route name="Screen3" component={Screen3} />
47 </Navigator>
48 );
49 }
50}
51
52const styles = StyleSheet.create({
53 screen: {
54 flex: 1,
55 alignItems: 'center',
56 justifyContent: 'center',
57 },
58});
سپس یک فایل جدید به نام Navigator.js با محتوای زیر بسازید:
1import React from 'react';
2import { View, StyleSheet, Animated, Dimensions } from 'react-native';
3
4const { width } = Dimensions.get('window');
5
6export const Route = () => null;
7
8const buildSceneConfig = (children = []) => {
9 const config = {};
10
11 children.forEach(child => {
12 config[child.props.name] = { key: child.props.name, component: child.props.component };
13 });
14
15 return config;
16};
17
18export class Navigator extends React.Component {
19 constructor(props) {
20 super(props);
21
22 const sceneConfig = buildSceneConfig(props.children);
23 const initialSceneName = props.children[0].props.name;
24
25 this.state = {
26 sceneConfig,
27 stack: [sceneConfig[initialSceneName]],
28 };
29 }
30
31 _animatedValue = new Animated.Value(0);
32
33 handlePush = (sceneName) => {
34 this.setState(state => ({
35 ...state,
36 stack: [...state.stack, state.sceneConfig[sceneName]],
37 }), () => {
38 this._animatedValue.setValue(width);
39 Animated.timing(this._animatedValue, {
40 toValue: 0,
41 duration: 250,
42 useNativeDriver: true,
43 }).start();
44 });
45 }
46
47 handlePop = () => {
48 Animated.timing(this._animatedValue, {
49 toValue: width,
50 duration: 250,
51 useNativeDriver: true,
52 }).start(() => {
53 this._animatedValue.setValue(0);
54 this.setState(state => {
55 const { stack } = state;
56 if (stack.length > 1) {
57 return {
58 stack: stack.slice(0, stack.length - 1),
59 };
60 }
61
62 return state;
63 });
64 });
65 }
66
67 render() {
68 return (
69 <View style={styles.container}>
70 {this.state.stack.map((scene, index) => {
71 const CurrentScene = scene.component;
72 const sceneStyles = [styles.scene];
73
74 if (index === this.state.stack.length - 1 && index > 0) {
75 sceneStyles.push({
76 transform: [
77 {
78 translateX: this._animatedValue,
79 }
80 ]
81 });
82 }
83
84 return (
85 <Animated.View key={scene.key} style={sceneStyles}>
86 <CurrentScene
87 navigator={{ push: this.handlePush, pop: this.handlePop }}
88 />
89 </Animated.View>
90 );
91 })}
92 </View>
93 )
94 }
95}
96
97const styles = StyleSheet.create({
98 container: {
99 flex: 1,
100 flexDirection: 'row',
101 },
102 scene: {
103 ...StyleSheet.absoluteFillObject,
104 flex: 1,
105 },
106});
اینک میتوانیم ژستهای لمسی را تنظیم کنیم. تنها ژستی که قصد داریم داشته باشیم زمانی است که چند صفحه در پشته قرار دارند و میخواهید به صفحه قبلی سوایپ کنید.
تنظیم PanResponder
ابتدا باید PanResponder را از ریاکت نیتیو ایمپورت کنیم. سپس در ادامه یک پاسخدهنده Pan جدید روی کامپوننت خود تنظیم میکنیم.
فایل Navigator.js
1export class Navigator extends React.Component {
2 // ...
3
4 _panResponder = PanResponder.create({
5 onMoveShouldSetPanResponder: (evt, gestureState) => {
6
7 },
8 onPanResponderMove: (evt, gestureState) => {
9
10 },
11 onPanResponderTerminationRequest: (evt, gestureState) => true,
12 onPanResponderRelease: (evt, gestureState) => {
13
14 },
15 onPanResponderTerminate: (evt, gestureState) => {
16
17 },
18 });
19
20 // ...
21}
قبل از تعریف کردن توابع فوق کارکرد هر یک از آنها را بررسی میکنیم.
- onMoveShouldSetPanResponder – این تابع تعیین میکند که پاسخدهنده pan ما باید فعلاً کاری انجام دهد یا خیر. در این مثال میخواهیم پاسخدهنده pan روی همه صفحهها به جز صفحه آغاز فعال شود و تنها زمانی که ژست لمسی در 25 درصد سمت چپ صفحه آغاز میشود اجرا گردد.
- onPanResponderMove – هنگامی که پاسخدهنده pan فعال میشود و حرکتی تشخیص داده میشود، این تابع فراخوانی میشود.
- onPanResponderTerminationRequest - اگر چیزی بخواهد ژست لمسی را تصاحب کند، تعیین میکند که آیا مجاز است یا خیر.
- onPanResponderRelease – تعریف میکند که هنگامی که ژست انتشار مییابد یا کامل میشود، چه اتفاقی باید بیفتد. در این مورد اگر ژست لمسی فضایی بیش از 50 درصد صفحه را اشغال کند آن را اجرا میکنیم و در غیر این صورت کاربر در همین صفحه باقی میماند.
- onPanResponderTerminate – تعیین میکند که وقتی ژست لمسی تمام شد باید چه کاری انجام یابد. ما صفحه جاری را ریست میکنیم.
در نهایت باید در عمل دستگیرههای pan را در کامپوننت جاری اعمال کنیم.
فایل Navigator.js
1export class Navigator extends React.Component {
2 // ...
3
4 render() {
5 return (
6 <View style={styles.container} {...this._panResponder.panHandlers}>
7 {this.state.stack.map((scene, index) => {
8 // ...
9 })}
10 </View>
11 )
12 }
13}
اینک باید آن را پیادهسازی کنیم.
1onMoveShouldSetPanResponder: (evt, gestureState) => {
2 const isFirstScreen = this.state.stack.length === 1
3 const isFarLeft = evt.nativeEvent.pageX < Math.floor(width * 0.25);
4
5 if (!isFirstScreen && isFarLeft) {
6 return true;
7 }
8 return false;
9},
ابتدا بررسی میکنیم که آیا در صفحه اول قرار داریم یا نه. این کار از طریق تحلیل this.state.stack صورت میگیرد که صفحههای فعال جاری را نشان میدهد. سپس با نگاه کردن به evt.nativeEvent.pageX بررسی میکنیم که آیا ژست لمسی در ابتدا آغاز شده است و یا در 25 درصد سمت چپ صفحه بوده است یا خیر.
سپس بررسی میکنیم که آیا باید عملاً به ژست لمسی پاسخ دهیم یا خیر. ما تنها در صورتی پاسخ میدهیم که در صفحه شماره دو یا بالاتر اپلیکیشن قرار داشته باشیم و ژست لمسی در ربع چپ صفحه آغاز شده باشد.
1onPanResponderMove: (evt, gestureState) => {
2 this._animatedValue.setValue(gestureState.moveX);
3},
اکنون وقتی که پاسخدهنده pan فعال شد، this._animatedValue را بهروزرسانی میکنیم که آفست ما را روی هر مقداری که تعیین شده است میبرد. اینک gestureState.moveX روی مقداری تعیین میشود که انگشت کاربر در محور x قرار دارد.
میتوانید این وضعیت را در عمل تست کنید. این کد کار میکند اما زمانی که ژست را اجرا کنید صفحه تغییر نمییابد.
1onPanResponderRelease: (evt, gestureState) => {
2 if (Math.floor(gestureState.moveX) >= width / 2) {
3 this.handlePop();
4 } else {
5 Animated.timing(this._animatedValue, {
6 toValue: 0,
7 duration: 250,
8 useNativeDriver: true,
9 }).start();
10 }
11},
برای اصلاح این وضعیت باید onPanResponderRelease را پیادهسازی کنیم. در این تابع بررسی میکنیم که آیا کاربر صفحه را در 50 درصد رها کرده است یا خیر. اگر چنین بود، تابع this.handlePop را فرا میخوانیم تا انیمیشن کامل شود و صفحه از پشته برداشته شود.
اگر صفحه در 50 درصد راست نبود، در این صورت آفست صفحه را به 0 ریست میکنیم.
1onPanResponderTerminate: (evt, gestureState) => {
2 Animated.timing(this._animatedValue, {
3 toValue: 0,
4 duration: 250,
5 useNativeDriver: true,
6 }).start();
7},
در ادامه پاسخدهنده pan تصاحب میشود و صفحه به آفست 0 ریست میشود. نتیجه کار به صورت زیر است:
فایل Navigator.js در نهایت به صورت زیر درمیآید:
1import React from 'react';
2import { View, StyleSheet, Animated, Dimensions, PanResponder } from 'react-native';
3
4const { width } = Dimensions.get('window');
5
6export const Route = () => null;
7
8const buildSceneConfig = (children = []) => {
9 const config = {};
10
11 children.forEach(child => {
12 config[child.props.name] = { key: child.props.name, component: child.props.component };
13 });
14
15 return config;
16};
17
18export class Navigator extends React.Component {
19 constructor(props) {
20 super(props);
21
22 const sceneConfig = buildSceneConfig(props.children);
23 const initialSceneName = props.children[0].props.name;
24
25 this.state = {
26 sceneConfig,
27 stack: [sceneConfig[initialSceneName]],
28 };
29 }
30
31 _animatedValue = new Animated.Value(0);
32
33 _panResponder = PanResponder.create({
34 onMoveShouldSetPanResponder: (evt, gestureState) => {
35 const isFirstScreen = this.state.stack.length === 1
36 const isFarLeft = evt.nativeEvent.pageX < Math.floor(width * 0.25);
37
38 if (!isFirstScreen && isFarLeft) {
39 return true;
40 }
41 return false;
42 },
43 onPanResponderMove: (evt, gestureState) => {
44 this._animatedValue.setValue(gestureState.moveX);
45 },
46 onPanResponderTerminationRequest: (evt, gestureState) => true,
47 onPanResponderRelease: (evt, gestureState) => {
48 if (Math.floor(gestureState.moveX) >= width / 2) {
49 this.handlePop();
50 } else {
51 Animated.timing(this._animatedValue, {
52 toValue: 0,
53 duration: 250,
54 useNativeDriver: true,
55 }).start();
56 }
57 },
58 onPanResponderTerminate: (evt, gestureState) => {
59 Animated.timing(this._animatedValue, {
60 toValue: 0,
61 duration: 250,
62 useNativeDriver: true,
63 }).start();
64 },
65 });
66
67 handlePush = (sceneName) => {
68 this.setState(state => ({
69 ...state,
70 stack: [...state.stack, state.sceneConfig[sceneName]],
71 }), () => {
72 this._animatedValue.setValue(width);
73 Animated.timing(this._animatedValue, {
74 toValue: 0,
75 duration: 250,
76 useNativeDriver: true,
77 }).start();
78 });
79 }
80
81 handlePop = () => {
82 Animated.timing(this._animatedValue, {
83 toValue: width,
84 duration: 250,
85 useNativeDriver: true,
86 }).start(() => {
87 this._animatedValue.setValue(0);
88 this.setState(state => {
89 const { stack } = state;
90 if (stack.length > 1) {
91 return {
92 stack: stack.slice(0, stack.length - 1),
93 };
94 }
95
96 return state;
97 });
98 });
99 }
100
101 render() {
102 return (
103 <View style={styles.container} {...this._panResponder.panHandlers}>
104 {this.state.stack.map((scene, index) => {
105 const CurrentScene = scene.component;
106 const sceneStyles = [styles.scene];
107
108 if (index === this.state.stack.length - 1 && index > 0) {
109 sceneStyles.push({
110 transform: [
111 {
112 translateX: this._animatedValue,
113 }
114 ]
115 });
116 }
117
118 return (
119 <Animated.View key={scene.key} style={sceneStyles}>
120 <CurrentScene
121 navigator={{ push: this.handlePush, pop: this.handlePop }}
122 />
123 </Animated.View>
124 );
125 })}
126 </View>
127 )
128 }
129}
130
131const styles = StyleSheet.create({
132 container: {
133 flex: 1,
134 flexDirection: 'row',
135 },
136 scene: {
137 ...StyleSheet.absoluteFillObject,
138 flex: 1,
139 },
140});
امیدواریم از مطالعه این راهنما بهره لازم را برده باشید.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای جاوا اسکریپت
- آموزش مقدماتی فریمورک React Native برای طراحی نرم افزارهای اندروید و iOS
- مجموعه آموزشهای برنامهنویسی
- چگونه با React Native اپلیکیشن اندرویدی بنویسیم؟ — به زبان ساده
- ساخت یک اپلیکیشن چند پلتفرمی موبایل با React Native (بخش اول) — به زبان ساده
==