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

۲ بازدید
آخرین به‌روزرسانی: ۲۴ شهریور ۱۳۹۸
زمان مطالعه: ۵ دقیقه
ژست لمسی در React Native

ژست‌های لمسی (Gestures) نقش مهمی در شیوه تعامل کاربران گوشی‌های هوشمند با اپلیکیشن‌ها دارند. در این راهنما با شیوه افزودن ژست لمسی در React Native از طریق PanResponder آشنا می‌شویم.

فهرست مطالب این نوشته

شروع

ما در این مقاله از create-react-native-app برای ساخت اپلیکیشن خود استفاده می‌کنیم. در آغاز دستور زیر را از ترمینال اجرا کنید:

create-react-native-app rn-js-navigator
cd rn-js-navigator

محتوای App.js را به صورت زیر عوض کنید:

import React from 'react';
import { StyleSheet, View, Button } from 'react-native';
import { Navigator, Route } from './Navigator';

const Screen1 = ({ navigator }) => (
  <View style={[styles.screen, { backgroundColor: '#59C9A5' }]}>
    <Button
      title="Screen 2"
      onPress={() => navigator.push('Screen2')}
    />
    <Button
      title="Pop"
      onPress={() => navigator.pop()}
    />
  </View>
);

const Screen2 = ({ navigator }) => (
  <View style={[styles.screen, { backgroundColor: '#23395B' }]}>
    <Button
      title="Screen 3"
      onPress={() => navigator.push('Screen3')}
    />
    <Button
      title="Pop"
      onPress={() => navigator.pop()}
    />
  </View>
);

const Screen3 = ({ navigator }) => (
  <View style={[styles.screen, { backgroundColor: '#B9E3C6' }]}>
    <Button
      title="Pop"
      onPress={() => navigator.pop()}
    />
  </View>
);

export default class App extends React.Component {
  render() {
    return (
      <Navigator>
        <Route name="Screen1" component={Screen1} />
        <Route name="Screen2" component={Screen2} />
        <Route name="Screen3" component={Screen3} />
      </Navigator>
    );
  }
}

const styles = StyleSheet.create({
  screen: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
});

سپس یک فایل جدید به نام Navigator.js با محتوای زیر بسازید:

import React from 'react';
import { View, StyleSheet, Animated, Dimensions } from 'react-native';

const { width } = Dimensions.get('window');

export const Route = () => null;

const buildSceneConfig = (children = []) => {
  const config = {};

  children.forEach(child => {
    config[child.props.name] = { key: child.props.name, component: child.props.component };
  });

  return config;
};

export class Navigator extends React.Component {
  constructor(props) {
    super(props);

    const sceneConfig = buildSceneConfig(props.children);
    const initialSceneName = props.children[0].props.name;

    this.state = {
      sceneConfig,
      stack: [sceneConfig[initialSceneName]],
    };
  }

  _animatedValue = new Animated.Value(0);

  handlePush = (sceneName) => {
    this.setState(state => ({
      ...state,
      stack: [...state.stack, state.sceneConfig[sceneName]],
    }), () => {
      this._animatedValue.setValue(width);
      Animated.timing(this._animatedValue, {
        toValue: 0,
        duration: 250,
        useNativeDriver: true,
      }).start();
    });
  }

  handlePop = () => {
    Animated.timing(this._animatedValue, {
      toValue: width,
      duration: 250,
      useNativeDriver: true,
    }).start(() => {
      this._animatedValue.setValue(0);
      this.setState(state => {
        const { stack } = state;
        if (stack.length > 1) {
          return {
            stack: stack.slice(0, stack.length - 1),
          };
        }

        return state;
      });
    });
  }

  render() {
    return (
      <View style={styles.container}>
        {this.state.stack.map((scene, index) => {
          const CurrentScene = scene.component;
          const sceneStyles = [styles.scene];

          if (index === this.state.stack.length - 1 && index > 0) {
            sceneStyles.push({
              transform: [
                {
                  translateX: this._animatedValue,
                }
              ]
            });
          }

          return (
            <Animated.View key={scene.key} style={sceneStyles}>
              <CurrentScene
                navigator={{ push: this.handlePush, pop: this.handlePop }}
              />
            </Animated.View>
          );
        })}
      </View>
    )
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    flexDirection: 'row',
  },
  scene: {
    ...StyleSheet.absoluteFillObject,
    flex: 1,
  },
});

اینک می‌توانیم ژست‌های لمسی را تنظیم کنیم. تنها ژستی که قصد داریم داشته باشیم زمانی است که چند صفحه در پشته قرار دارند و می‌خواهید به صفحه قبلی سوایپ کنید.

تنظیم PanResponder

ابتدا باید PanResponder را از ری‌اکت نیتیو ایمپورت کنیم. سپس در ادامه یک پاسخ‌دهنده Pan جدید روی کامپوننت خود تنظیم می‌کنیم.

فایل Navigator.js

export class Navigator extends React.Component {
  // ...

  _panResponder = PanResponder.create({
    onMoveShouldSetPanResponder: (evt, gestureState) => {

    },
    onPanResponderMove: (evt, gestureState) => {

    },
    onPanResponderTerminationRequest: (evt, gestureState) => true,
    onPanResponderRelease: (evt, gestureState) => {

    },
    onPanResponderTerminate: (evt, gestureState) => {

    },
  });

  // ...
}

قبل از تعریف کردن توابع فوق کارکرد هر یک از آن‌ها را بررسی می‌کنیم.

  • onMoveShouldSetPanResponder – این تابع تعیین می‌کند که پاسخ‌دهنده pan ما باید فعلاً کاری انجام دهد یا خیر. در این مثال می‌خواهیم پاسخ‌دهنده pan روی همه صفحه‌ها به جز صفحه آغاز فعال شود و تنها زمانی که ژست لمسی در 25 درصد سمت چپ صفحه آغاز می‌شود اجرا گردد.
  • onPanResponderMove – هنگامی که پاسخ‌دهنده pan فعال می‌شود و حرکتی تشخیص داده می‌شود، این تابع فراخوانی می‌شود.
  • onPanResponderTerminationRequest – اگر چیزی بخواهد ژست لمسی را تصاحب کند، تعیین می‌کند که آیا مجاز است یا خیر.
  • onPanResponderRelease – تعریف می‌کند که هنگامی که ژست انتشار می‌یابد یا کامل می‌شود، چه اتفاقی باید بیفتد. در این مورد اگر ژست لمسی فضایی بیش از 50 درصد صفحه را اشغال کند آن را اجرا می‌کنیم و در غیر این صورت کاربر در همین صفحه باقی می‌ماند.
  • onPanResponderTerminate – تعیین می‌کند که وقتی ژست لمسی تمام شد باید چه کاری انجام یابد. ما صفحه جاری را ریست می‌کنیم.

در نهایت باید در عمل دستگیره‌های pan را در کامپوننت جاری اعمال کنیم.

فایل Navigator.js

export class Navigator extends React.Component {
  // ...

  render() {
    return (
      <View style={styles.container} {...this._panResponder.panHandlers}>
        {this.state.stack.map((scene, index) => {
          // ...
        })}
      </View>
    )
  }
}

اینک باید آن را پیاده‌سازی کنیم.

onMoveShouldSetPanResponder: (evt, gestureState) => {
  const isFirstScreen = this.state.stack.length === 1
  const isFarLeft = evt.nativeEvent.pageX < Math.floor(width * 0.25);

  if (!isFirstScreen && isFarLeft) {
    return true;
  }
  return false;
},

ابتدا بررسی می‌کنیم که آیا در صفحه اول قرار داریم یا نه. این کار از طریق تحلیل this.state.stack صورت می‌گیرد که صفحه‌های فعال جاری را نشان می‌دهد. سپس با نگاه کردن به evt.nativeEvent.pageX بررسی می‌کنیم که آیا ژست لمسی در ابتدا آغاز شده است و یا در 25 درصد سمت چپ صفحه بوده است یا خیر.

سپس بررسی می‌کنیم که آیا باید عملاً به ژست لمسی پاسخ ‌دهیم یا خیر. ما تنها در صورتی پاسخ می‌دهیم که در صفحه شماره دو یا بالاتر اپلیکیشن قرار داشته باشیم و ژست لمسی در ربع چپ صفحه آغاز شده باشد.

onPanResponderMove: (evt, gestureState) => {
  this._animatedValue.setValue(gestureState.moveX);
},

اکنون وقتی که پاسخ‌دهنده pan فعال شد، this._animatedValue را به‌روزرسانی می‌کنیم که آفست ما را روی هر مقداری که تعیین شده است می‌برد. اینک gestureState.moveX روی مقداری تعیین می‌شود که انگشت کاربر در محور x قرار دارد.

می‌توانید این وضعیت را در عمل تست کنید. این کد کار می‌کند اما زمانی که ژست را اجرا کنید صفحه تغییر نمی‌یابد.

onPanResponderRelease: (evt, gestureState) => {
  if (Math.floor(gestureState.moveX) >= width / 2) {
    this.handlePop();
  } else {
    Animated.timing(this._animatedValue, {
      toValue: 0,
      duration: 250,
      useNativeDriver: true,
    }).start();
  }
},

برای اصلاح این وضعیت باید onPanResponderRelease را پیاده‌سازی کنیم. در این تابع بررسی می‌کنیم که آیا کاربر صفحه را در 50 درصد رها کرده است یا خیر. اگر چنین بود، تابع this.handlePop را فرا می‌خوانیم تا انیمیشن کامل شود و صفحه از پشته برداشته شود.

اگر صفحه در 50 درصد راست نبود، در این صورت آفست صفحه را به 0 ریست می‌کنیم.

onPanResponderTerminate: (evt, gestureState) => {
  Animated.timing(this._animatedValue, {
    toValue: 0,
    duration: 250,
    useNativeDriver: true,
  }).start();
},

در ادامه پاسخ‌دهنده pan تصاحب می‌شود و صفحه به آفست 0 ریست می‌شود. نتیجه کار به صورت زیر است:

ژست در React Native

فایل Navigator.js در نهایت به صورت زیر درمی‌آید:

import React from 'react';
import { View, StyleSheet, Animated, Dimensions, PanResponder } from 'react-native';

const { width } = Dimensions.get('window');

export const Route = () => null;

const buildSceneConfig = (children = []) => {
  const config = {};

  children.forEach(child => {
    config[child.props.name] = { key: child.props.name, component: child.props.component };
  });

  return config;
};

export class Navigator extends React.Component {
  constructor(props) {
    super(props);

    const sceneConfig = buildSceneConfig(props.children);
    const initialSceneName = props.children[0].props.name;

    this.state = {
      sceneConfig,
      stack: [sceneConfig[initialSceneName]],
    };
  }

  _animatedValue = new Animated.Value(0);

  _panResponder = PanResponder.create({
    onMoveShouldSetPanResponder: (evt, gestureState) => {
      const isFirstScreen = this.state.stack.length === 1
      const isFarLeft = evt.nativeEvent.pageX < Math.floor(width * 0.25);

      if (!isFirstScreen && isFarLeft) {
        return true;
      }
      return false;
    },
    onPanResponderMove: (evt, gestureState) => {
      this._animatedValue.setValue(gestureState.moveX);
    },
    onPanResponderTerminationRequest: (evt, gestureState) => true,
    onPanResponderRelease: (evt, gestureState) => {
      if (Math.floor(gestureState.moveX) >= width / 2) {
        this.handlePop();
      } else {
        Animated.timing(this._animatedValue, {
          toValue: 0,
          duration: 250,
          useNativeDriver: true,
        }).start();
      }
    },
    onPanResponderTerminate: (evt, gestureState) => {
      Animated.timing(this._animatedValue, {
        toValue: 0,
        duration: 250,
        useNativeDriver: true,
      }).start();
    },
  });

  handlePush = (sceneName) => {
    this.setState(state => ({
      ...state,
      stack: [...state.stack, state.sceneConfig[sceneName]],
    }), () => {
      this._animatedValue.setValue(width);
      Animated.timing(this._animatedValue, {
        toValue: 0,
        duration: 250,
        useNativeDriver: true,
      }).start();
    });
  }

  handlePop = () => {
    Animated.timing(this._animatedValue, {
      toValue: width,
      duration: 250,
      useNativeDriver: true,
    }).start(() => {
      this._animatedValue.setValue(0);
      this.setState(state => {
        const { stack } = state;
        if (stack.length > 1) {
          return {
            stack: stack.slice(0, stack.length - 1),
          };
        }

        return state;
      });
    });
  }

  render() {
    return (
      <View style={styles.container} {...this._panResponder.panHandlers}>
        {this.state.stack.map((scene, index) => {
          const CurrentScene = scene.component;
          const sceneStyles = [styles.scene];

          if (index === this.state.stack.length - 1 && index > 0) {
            sceneStyles.push({
              transform: [
                {
                  translateX: this._animatedValue,
                }
              ]
            });
          }

          return (
            <Animated.View key={scene.key} style={sceneStyles}>
              <CurrentScene
                navigator={{ push: this.handlePush, pop: this.handlePop }}
              />
            </Animated.View>
          );
        })}
      </View>
    )
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    flexDirection: 'row',
  },
  scene: {
    ...StyleSheet.absoluteFillObject,
    flex: 1,
  },
});

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

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

==

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

نظر شما چیست؟

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