Cơ chế kéo thả và ẩn bong bóng thông báo trong React Native

Quản lý tập hợp tham chiếu giao diện

Để thao tác di chuyển hoạt động chính xác trên từng mục trong danh sách động, hệ thống cần duy trì một cấu trúc ánh xạ tham chiếu ổn định. Thay vì khởi tạo thủ công trong mỗi chu kỳ render, có thể chuẩn bị sẵn một mảng chứa các đối tượng React.createRef() tương ứng với dung lượng tối đa dự kiến của danh sách.

const refPool = new Array(50).fill(null).map(() => React.createRef());

Thiết lập cử chỉ kéo và giá trị hoạt ảnh

Sử dụng PanResponder kết hợp với Animated.ValueXY cho phép ghi nhận tọa độ dịch chuyển theo thời gian thực. Giá trị này sẽ được ràng buộc trực tiếp vào thuộc tính transform của thành phần mục tiêu.

const [dragPosition] = useState(new Animated.ValueXY());
const [activeItemIndex, setActiveItemIndex] = useState(-1);

Áp dụng hoạt ảnh có chọn lọc

Mặc định, sự kiện di chuyển có thể vô tình cập nhật giá trị cho toàn bộ danh sách. Để chỉ dịch chuyển phần tử đang được chạm, cần lưu lại chỉ mục của mục khởi tạo thao tác. Trong quá trình render, hệ thống sẽ so sánh chỉ mục hiện tại với chỉ mục đang hoạt động, từ đó quyết định có gán giá trị translateXtranslateY hay không.

const getDynamicStyle = (index) => {
  if (index !== activeItemIndex) return {};
  const { x, y } = dragPosition.getLayout();
  return { transform: [{ translateX: x }, { translateY: y }] };
};

Xử lý logic ẩn phần tử khi đạt ngưỡng

Sự kiện onPanResponderRelease đóng vai trò xác nhận kết thúc thao tác. Nếu khoảng cách di chuyển ngang hoặc dọc vượt quá mức định trước (ví dụ: 80 đơn vị), cờ hiển thị của mục tương ứng trong mảng trạng thái sẽ được chuyển sang false. Điều này kích hoạt cơ chế conditional rendering, loại bỏ phần tử khỏi cây giao diện.

const DRAG_LIMIT = 80;

const releaseHandler = useCallback(({ dx, dy }) => {
  const shouldDismiss = Math.abs(dx) > DRAG_LIMIT || Math.abs(dy) > DRAG_LIMIT;
  if (shouldDismiss && activeItemIndex >= 0) {
    setVisibilityFlags(prev => {
      const next = [...prev];
      next[activeItemIndex] = false;
      return next;
    });
  }
  setActiveItemIndex(-1);
  dragPosition.setValue({ x: 0, y: 0 });
}, [activeItemIndex, dragPosition]);

Tích hợp hoàn chỉnh

Dưới đây là cấu trúc thành phần hoàn chỉnh sử dụng Functional Component và React Hooks, tối ưu hóa hiệu suất và loại bỏ các phương thức lifecycle đã lỗi thời.

import React, { useState, useRef, useCallback, useMemo } from 'react';
import { View, Text, PanResponder, Animated, FlatList, StyleSheet } from 'react-native';

const MOCK_DATA = [
  { id: 'c1', unread: 5, name: 'Phòng Dev' },
  { id: 'c2', unread: 12, name: 'Nhóm QA' },
  { id: 'c3', unread: 0, name: 'Thông báo hệ thống' },
];

const DRAG_THRESHOLD = 80;

export default function ChatBadgeScreen() {
  const [activeIdx, setActiveIdx] = useState(-1);
  const [isHidden, setIsHidden] = useState(MOCK_DATA.map(() => false));
  const offset = useRef(new Animated.ValueXY()).current;
  const refs = useRef(MOCK_DATA.map(() => React.createRef())).current;

  const panResponder = useMemo(() => PanResponder.create({
    onStartShouldSetPanResponder: () => true,
    onMoveShouldSetPanResponder: () => true,
    onPanResponderGrant: (evt) => {
      const targetTag = evt._targetInst?.stateNode?.tag;
      const foundIdx = refs.findIndex(ref => ref.current?._nativeTag === targetTag);
      setActiveIdx(foundIdx !== -1 ? foundIdx : -1);
      offset.setValue({ x: 0, y: 0 });
    },
    onPanResponderMove: Animated.event(
      [null, { dx: offset.x, dy: offset.y }],
      { useNativeDriver: false }
    ),
    onPanResponderRelease: (_, gesture) => {
      const { dx, dy } = gesture;
      const exceeded = Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD;
      
      if (exceeded && activeIdx >= 0) {
        setIsHidden(prev => prev.map((val, i) => (i === activeIdx ? true : val)));
      }
      setActiveIdx(-1);
      offset.setValue({ x: 0, y: 0 });
    },
  }), [activeIdx, offset, refs]);

  const getItemStyle = (index) => {
    if (index !== activeIdx) return {};
    const { x, y } = offset.getLayout();
    return { transform: [{ translateX: x }, { translateY: y }] };
  };

  const renderRow = ({ item, index }) => {
    if (isHidden[index]) return null;

    return (
      <Animated.View
        ref={refs[index]}
        style={[styles.badgeWrap, getItemStyle(index)]}
        {...panResponder.panHandlers}
      >
        {item.unread > 0 && (
          <View style={styles.badge}>
            <Text style={styles.badgeText}>{item.unread}</Text>
          </View>
        )}
        <Text style={styles.label}>{item.name}</Text>
      </Animated.View>
    );
  };

  return (
    <FlatList
      data={MOCK_DATA}
      keyExtractor={item => item.id}
      renderItem={renderRow}
      contentContainerStyle={styles.container}
    />
  );
}

const styles = StyleSheet.create({
  container: { padding: 16, gap: 16 },
  badgeWrap: { flexDirection: 'row', alignItems: 'center', zIndex: 99 },
  badge: {
    backgroundColor: '#e74c3c',
    borderRadius: 12,
    paddingHorizontal: 8,
    paddingVertical: 2,
    marginRight: 8,
  },
  badgeText: { color: '#fff', fontWeight: '600', fontSize: 12 },
  label: { fontSize: 16, color: '#333' },
});

Thẻ: react-native pan-responder animated-api gesture-tracking conditional-rendering

Đăng vào ngày 23 tháng 9 lúc 16:45