ژست های لمسی (Gestures) در React Native — راهنمای کاربردی

۴۸ بازدید
آخرین به‌روزرسانی: ۲۰ شهریور ۱۴۰۲
زمان مطالعه: ۵ دقیقه
ژست های لمسی (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 ریست می‌شود. نتیجه کار به صورت زیر است:

ژست در React Native

فایل 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});

امیدواریم از مطالعه این راهنما بهره لازم را برده باشید.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۰ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
handlebar-labs
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *