[React Native] Dựng base App React Native - Swipe Panel with panResponder - P1
Bài đăng này đã không được cập nhật trong 4 năm
Hi All. Hôm nay mình sẽ làm 1 bài hướng dẫn để làm 1 swipe panel bằng panResponder nha mn.
Requirement
Bài toán đặt ra cho chúng ta những yêu cầu như sau: Màn hình sẽ bao gồm các component:
- Swipe Panel: Panel này cho phép chúng ta swipe, Panel này có thể swipe lên và xuống
- Modal Panel: Panel này không cho phép chúng ta swipe, nó sẽ di chuyển theo Swipe Panel
- BackGround Panel: đây là Panel sẽ đứng yên chứa các control, khi Swipe Panel đi lên phía trên thì toàn bộ control trong Panel này sẽ bị block và không thể sử dụng
- Opacity Panel: Panel này chỉ xuất hiện khi Swipe Panel đi lên phía trên và sẽ biến mất khi Swipe Panel trở về đúng vị trí xuất phát, nó có nhiệm vụ block toàn bộ BackGround Panel
Với các Component như trên thì mình có đánh màu nó như sau
- Swipe Panel: màu xanh
- Modal Panel: màu đỏ
- BackGround Panel: màu vàng
Về yêu cầu logic như sau:
- Khi Swipe Panelrời khỏi vị trí ban đầu và di chuyển lên phía trên thìOpacity Panelsẽ xuất hiện và block toàn bộBackGround Panel, đồng thờiModal Panelsẽ di chuyển theoSwipe Panel
- Swipe Panelđược thoải mái di chuyển lên xuống nhưng khi User thả tay ra thì- Swipe Panelsẽ phải tự động tính toán vị trí sao cho:- Vị trí đứng của  Swipe Panelkhi swipe đi lên chính là chiều cao củaSwipe Panel+Modal Panel
- Vị trí đứng của  Swipe Panelkhi swipe đi xuống chính là điểm xuất phát ban đầu
 
- Vị trí đứng của  
Các bạn có thể xem ví dụ như ảnh gif nha:

Ok chúng ta sẽ cùng bắt tay vào làm nào, nhưng để làm được như thế này thì các bạn cần phải nắm rõ về 2 kiến thức sau:
Vì sao lại là Animated và Panresponse. Vì Panresponse sẽ giúp chúng ta tính toán các vị trí khi User Swipe, còn Animated sẽ giúp chúng ta xử lý các hiệu ứng được mượt mà hơn.
Setup
Panresponder
Đây là đoạn code tạo Panresponder.
import { PanResponder, Animated } from 'react-native';
 const pan = useRef(new Animated.ValueXY({ x: 0, y: 0 })).current;
const handleShouldSetPanResponder = (evt: any, gestureState: any) => {
    return evt.nativeEvent.touches.length === 1 && Math.abs(gestureState.dy) > 5;
  };
const panResponder = PanResponder.create({
    onStartShouldSetPanResponder: handleShouldSetPanResponder,
    onMoveShouldSetPanResponder: handleShouldSetPanResponder,
    onPanResponderMove: (e, gestureState) => {
      return Animated.event(
        [
          null,
          {
            dy: pan.y,
          },
        ],
        {
          useNativeDriver: false,
        },
      )(e, gestureState);
    },
    onPanResponderGrant: () => {
      pan.setOffset(pan.__getValue());
      pan.setValue({ x: 0, y: 0 });
    },
    onPanResponderRelease: (_e, gestureState) => {
      pan.flattenOffset();
      if (gestureState.dy > 0) {
        fadeOut();
      } else {
        fadeIn();
      }
    },
  });
  
  <Animated.View
        ref={ref}
        {...panResponder.panHandlers}
        style={[pan.getLayout(), styles.swipePanel]}
      />
   <Animated.View style={[pan.getLayout(), styles.modalPanel]} />
trước hết chúng ta cần phải tạo ra 1 Panresponder bằng cách sử dụng method PanResponder.create và tiến hành config một vài option.
Và trong bài toán của chúng ta thì chúng ta cần config những gì
Check Should Panresponder run?
Bài toán thực tế thì sẽ có rất nhiều case và trong đó có một vài trường hợp như thế này thì chúng ta cần phải block Panresponder:
- User touch và ko swipe
- User swipe nhưng khoảng cách swipe quá ngắn
Để giải quyết vấn đề này thì Panresponder đã support bằng cách cho phép chúng ta config các option và ở đây mình sử dụng 2 option :
- onStartShouldSetPanResponder
- onMoveShouldSetPanResponder
Như đoạn code sau:
const handleShouldSetPanResponder = (evt: any, gestureState: any) => {
    return evt.nativeEvent.touches.length === 1 && Math.abs(gestureState.dy) > 5;
  };
  
const panResponder = PanResponder.create({
    onStartShouldSetPanResponder: handleShouldSetPanResponder,
    onMoveShouldSetPanResponder: handleShouldSetPanResponder,
  });
2 Option này sẽ sử dụng handleShouldSetPanResponder để check xem User toucher bao nhiêu lần và khoảng cách mà User đã swipe là bao xa, function handleShouldSetPanResponder sẽ trả về kết quả true hoặc false (true thì Panresponder sẽ chạy và false thì không)
onPanResponderMove
Đây là phần quan trọng nhất, tại function này chúng ta sẽ quyết định việc di chuyển của các panel bằng cách set các toạ độ tương ứng.
onPanResponderMove: (e, gestureState) => {
      return Animated.event(
        [
          null,
          {
            dy: pan.y,
          },
        ],
        {
          useNativeDriver: false,
        },
      )(e, gestureState);
    },
Ở đây mình đang trả về cho onPanResponderMove là một Animated.event, điều này có nghĩa là gì. PanResponder sẽ tạo ra một function mà nó sẽ tự động lấy 2 giá trị của gestureState là dx và dy, sau đó nó đi update giá trị x và y tương ứng của Animated. Và tất nhiên khi giá trị của Animated được update thì nhiệm vụ còn lại là của Animated.View sẽ biểu diễn sự thay đổi của giá trị.
onPanResponderGrant
Mỗi một lần User swipe thì function này sẽ được gọi một lần duy nhất với mục đích thiết lập các giá trị cần thiết (tuỳ vào mục đích chúng ta cần cái gì).
 onPanResponderGrant: () => {
      pan.setOffset(pan.__getValue());
      pan.setValue({ x: 0, y: 0 });
    },
Ở đoạn code này chúng ta sẽ quan tâm tới 1 giá trị đó là Offset
- Offsetdịch ra Tiếng Việt là- Giá Trị Bù.
- Mình sẽ giải thích vì sao chúng ta lại cần Giá Trị Bùnày:- Khi bạn chạm vào bất cứ đâu trên màn hình điện thoại và tiến hành swipe thì chúng ta không thể tính toán được toạ độ chính xác là đang ở đâu.
- Còn ở trong function onPanResponderMovesẽ chỉ trả về cho bạn khoảng cách mà các bạn đã swipe
 
Dựa trên 2 bài toán trên thì Offset hay còn gọi là  Giá Trị Bù đã được ra đời để giải quyết bài toán.
Ví dụ:
- Bạn đang ở ví trí { x: 0, y: 0 }
- Bạn swipe lần 1 lên phía trên và khoảng cách là 200
- Bạn dừng swipe
- Bạn lại swipe lần thứ 2 lên phía trên và khoảng cách là 200
Với bài toán này thì ở lần thứ 2 chúng ta mong muốn rằng điểm xuất phát phải ở { x: 0, y: 200 }  đúng ko ạ.
Và mục đích của onPanResponderGrant là để giải quyết bài toán ở trên, chúng ta set Offset chính là vị trí hiện tại (trước khi thực hiện swipe), và sau đó dựa vào giá trị này chúng ta có thể tính toán chính xác toạ độ bắt đầu và toạ độ di chuyển.
Còn giá trị Value sau khi lưu trữ bằng cách set Offset thì chúng ta sẽ tiến hành reset nó về  { x: 0, y: 0 }
onPanResponderRelease
onPanResponderRelease: (_e, gestureState) => {
      pan.flattenOffset();
}
onPanResponderRelease: đây là function sẽ chạy khi User kết thúc swipe (User nhấc tay khỏi màn hình), và ở đây chúng ta sẽ call function pan.flattenOffset();.
pan.flattenOffset();: Nó sẽ set lại giá trị của Animated bằng cách gộp giá trị hiện tại với giá trị Offset
Ví dụ
Tại onPanResponderRelease Animated có giá trị Offset là { x: 0, y: 200 }  và giá trị Value của Animated là { x: 0, y: 200 } thì sau khi call pan.flattenOffset(); giá trị của Offset là { x: 0, y: 0 } và giá trị value của Animated là { x: 0, y: 400 }.
Như vậy ở function onPanResponderRelease này là điểm kết thúc của một quá trình swipe, function này kết hợp với onPanResponderGrant sẽ đảm bảo rằng chúng ta sẽ luôn có giá trị cuối cùng của swipe.
Part 1 Conlusion
Phần 1 chúng ta sẽ dừng lại ở mức hiểu về nguyên lý hoạt động của PanResponder, ở phần sau chúng ta sẽ đi chi tiết hơn về bài toán cụ thể mà chúng ta đã nêu ở trên.
Tạm biệt và hẹn gặp lại mn.
Nếu bạn nào muốn xem code thì Xem tại đây
All rights reserved
 
  
 