关于动画和Home页面顶部频道栏和模态框的问题.

来源:21-8 实战仿写小红书App-实现频道编辑数据修改

阜東原

2025-03-07

有三个问题请教一下公爵老师, 指导一下debug的方向:
1、run ios命令使用LayoutAnimation.easeInAndOut()时app会崩溃.
2、首次进入Home页面打开频道编辑modal, 会遮挡顶部的标题栏.
3、编辑并保存频道后, Home页面中的的scrollview的频道列表没有更新
环境: macOS 15.3.1; Xcode 16; react native 0.78; react: 19; iOS simulator: iPhone 16 Pro; iOS: 18.3
以下是源代码

// Home.tsx
import React, { useEffect } from 'react';
import { Dimensions, Image, StyleSheet, Text, View } from 'react-native';

import { useLocalObservable, observer } from 'mobx-react';
import HomeStore from './HomeStore';
import { ArticleExcert, Category } from '@/types';
import { FlowList, Like, ResizeImage } from '@/components';
import { CategoryList, Footer, HomeTabHeader } from './components';

const SCREEN_WIDTH = Dimensions.get('window').width;
let categories: Category[] = [];
let addedCategories: Category[] = [];
const Home = () => {
  // const navigation = useNavigation<StackNavigationProp<any>>()
  const store = useLocalObservable(() => new HomeStore());

  useEffect(() => {
    queryCategories();
    queryNewArticle();
  }, []);

  const queryCategories = async () => {
    await store.getCategories();
    categories = store.categories || [];
    addedCategories = categories.filter(category => category.isAdd);
  };

  const queryNewArticle = async () => {
    store.resetpage();
    await store.requestHomeList();
  };

  const loadMoreArticle = async () => {
    await store.requestHomeList();
  };

  const renderArticleExcert = ({ item }: { item: ArticleExcert }) => {
    return (
      <View style={styles.article} key={item.id}>
        <ResizeImage uri={item.image} />
        <Text style={styles.articleTitle}>{item.title}</Text>
        <View style={styles.articleInfoGroup}>
          <Image style={styles.avatar} source={{ uri: item.avatarUrl }} />
          <Text style={styles.username}>{item.userName}</Text>
          <Like liked={item.isFavorite} />
          <Text style={styles.favoriteCountLabel}>{item.favoriteCount}</Text>
        </View>
      </View>
    );
  };

  return (
    <View style={styles.container}>
      <HomeTabHeader index={1} />
      <FlowList
        keyExtractor={(article: ArticleExcert) => article.id}
        style={styles.flatList}
        data={store.homeList}
        renderItem={renderArticleExcert}
        numColumns={2}
        refreshing={store.refreshing}
        onRefresh={queryNewArticle}
        onEndReachedThreshold={0.1}
        onEndReached={loadMoreArticle}
        ListHeaderComponent={
          <CategoryList
            categories={categories}
            addedCategories={addedCategories}
            onCategoryItemChange={(category: Category) =>
              console.log(JSON.stringify(category))
            }
          />
        }
        ListFooterComponent={<Footer />}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    width: '100%',
    height: '100%',
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#f0f0f0'
  },
  flatList: {
    width: '100%',
    height: '100%'
  },
  article: {
    width: (SCREEN_WIDTH - 18) >> 1,
    marginLeft: 6,
    marginBottom: 6,
    backgroundColor: '#fff',
    borderRadius: 8,
    overflow: 'hidden'
  },
  articleTitle: {
    fontSize: 16,
    color: '#333',
    marginHorizontal: 10,
    marginVertical: 8
  },
  articleInfoGroup: {
    width: '100%',
    flexDirection: 'row',
    alignItems: 'center',
    paddingHorizontal: 10,
    marginBottom: 10
  },
  avatar: {
    width: 18,
    height: 18,
    borderRadius: 9,
    resizeMode: 'cover'
  },
  username: {
    flex: 1,
    fontSize: 14,
    color: '#999',
    marginLeft: 6
  },
  favoriteCountLabel: {
    fontSize: 14,
    color: '#999',
    marginLeft: 4
  }
});

export default observer(Home);
// CategoryList.tsx
import React, { useEffect, useState, useRef } from 'react';
import {
  Image,
  ScrollView,
  StyleSheet,
  Text,
  TouchableOpacity,
  View
} from 'react-native';
import { icon_arrow } from '@/assets/images';
import { Category } from '@/types';
import CategoryModal, { CategoryModalRef } from './CategoryModal';

type Props = {
  categories: Category[];
  addedCategories: Category[];
  onCategoryItemChange?: (category: Category) => void;
};
const CategoryList = ({
  categories,
  addedCategories,
  onCategoryItemChange
}: Props) => {
  const [seclectdCategory, setSeclectdCategory] = useState<Category>();
  const modalRef = useRef<CategoryModalRef>(null);

  useEffect(() => {
    setSeclectdCategory(
      addedCategories.find(category => category.name === '推荐')
    );
  }, [addedCategories]);

  const onCategoryItemPressHandler = (category: Category) => {
    setSeclectdCategory(category);
    onCategoryItemChange?.(category);
  };

  return (
    <View style={styles.categoryContainer}>
      <ScrollView
        style={styles.categoryList}
        horizontal={true}
        showsHorizontalScrollIndicator={false}
      >
        {addedCategories.map((category: Category) => {
          const isSelected = category.name === seclectdCategory?.name;
          return (
            <TouchableOpacity
              style={styles.categoryItem}
              onPress={() => onCategoryItemPressHandler(category)}
              key={category.name}
            >
              <Text
                style={
                  isSelected
                    ? styles.categoryItemTextSelected
                    : styles.categoryItemText
                }
              >
                {category.name}
              </Text>
            </TouchableOpacity>
          );
        })}
      </ScrollView>
      <TouchableOpacity
        style={styles.openBtn}
        onPress={() => modalRef.current?.showModal()}
      >
        <Image style={styles.iconArrow} source={icon_arrow} />
      </TouchableOpacity>
      <CategoryModal ref={modalRef} categoryList={categories} />
    </View>
  );
};

const styles = StyleSheet.create({
  categoryContainer: {
    width: '100%',
    height: 36,
    flexDirection: 'row',
    backgroundColor: '#fff',
    // paddingHorizontal: 16,
    marginBottom: 8
  },
  categoryList: {
    width: '100%'
  },
  openBtn: {
    width: 40,
    height: '100%',
    justifyContent: 'center',
    alignItems: 'center',
    padding: 16
  },
  iconArrow: {
    width: 16,
    height: 16,
    transform: [{ rotate: '-90deg' }]
  },
  categoryItem: {
    width: 64,
    height: '100%',
    justifyContent: 'center',
    alignItems: 'center'
  },
  categoryItemText: {
    fontSize: 16,
    color: '#999'
  },
  categoryItemTextSelected: {
    fontSize: 16,
    color: '#333',
    fontWeight: 'bold'
  }
});

export default CategoryList;
// CategoryModal.tsx
import React, {
  useState,
  useEffect,
  forwardRef,
  useImperativeHandle,
  useCallback
} from 'react';
import {
  Dimensions,
  Image,
  Modal,
  StatusBar,
  StyleSheet,
  Text,
  TouchableOpacity,
  View
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
// import Animated, { LinearTransition } from 'react-native-reanimated'

import { icon_arrow, icon_delete } from '@/assets/images';
import { Category } from '@/types';
import { save } from '@/tools/Storage';

type Props = {
  categoryList?: Category[];
};

export interface CategoryModalRef {
  showModal: () => void;
  hideModal: () => void;
}

let statusBarHeight = 0;
const SCREEN_WIDTH = Dimensions.get('window').width;

const CategoryModal = forwardRef(({ categoryList }: Props, ref) => {
  const [modalVisibility, setModalVisibility] = useState<boolean>(false);
  const [addedCategories, setAddedCategories] = useState<Category[]>([]);
  const [unaddedCategories, setUnaddedCategories] = useState<Category[]>([]);
  const [isInEditMode, setIsInEditMode] = useState<boolean>(false);

  useEffect(() => {
    statusBarHeight = StatusBar.currentHeight || 0;
    if (!categoryList) {
      return;
    }
    setAddedCategories(categoryList.filter(item => item.isAdd));
    setUnaddedCategories(categoryList.filter(item => !item.isAdd));
  }, [categoryList]);

  useImperativeHandle(ref, () => {
    return { showModal, hideModal };
  });

  const showModal = () => {
    setModalVisibility(true);
  };

  const hideModal = () => {
    setModalVisibility(false);
  };

  const removeCategoryHandler = useCallback(
    (category: Category) => () => {
      if (!isInEditMode) return;
      const newAddedCategories = addedCategories.filter(
        item => item.name !== category.name
      );
      const removedCategory = { ...category, isAdd: false };
      const newUnaddedCategories = [...unaddedCategories, removedCategory];
      setAddedCategories(newAddedCategories);
      setUnaddedCategories(newUnaddedCategories);
    },
    [isInEditMode, addedCategories, unaddedCategories]
  );

  const addCategoryHandler = useCallback(
    (category: Category) => () => {
      if (!isInEditMode) return;
      const newUnaddedCategories = unaddedCategories.filter(
        item => item.name !== category.name
      );
      const addedCategory = { ...category, isAdd: true };
      const newAddedCategories = [...addedCategories, addedCategory];
      setAddedCategories(newAddedCategories);
      setUnaddedCategories(newUnaddedCategories);
    },
    [isInEditMode, addedCategories, unaddedCategories]
  );

  const AddedCategories = () => (
    <>
      <View style={styles.categoryHeader}>
        <Text style={styles.categoryTitle}>我的频道</Text>
        <Text style={styles.categoryDescription}>点击进入我的频道</Text>
        <TouchableOpacity
          style={styles.editBtn}
          onPress={() =>
            setIsInEditMode(data => {
              if (data) {
                save(
                  'categories',
                  JSON.stringify([...addedCategories, ...unaddedCategories])
                );
                return false;
              } else {
                return true;
              }
            })
          }
        >
          <Text style={styles.editBtnText}>
            {isInEditMode ? '完成' : '编辑'}
          </Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.closeIcon} onPress={hideModal}>
          <Image style={styles.closeIconImg} source={icon_arrow} />
        </TouchableOpacity>
      </View>
      <View style={styles.categoryItemContainer}>
        {addedCategories.map((category: Category) => (
          <TouchableOpacity
            key={`${category.name}`}
            style={
              category.default
                ? styles.categoryItemDefault
                : styles.categoryItem
            }
            onPress={removeCategoryHandler(category)}
          >
            <Text style={styles.categoryItemText}>{category.name}</Text>
            {isInEditMode && !category.default && (
              <Image style={styles.deleteImg} source={icon_delete} />
            )}
          </TouchableOpacity>
        ))}
      </View>
    </>
  );

  const UnaddedCategories = () => {
    return (
      <>
        <View style={[styles.categoryHeader, { marginTop: 32 }]}>
          <Text style={styles.categoryTitle}>推荐频道</Text>
          <Text style={styles.categoryDescription}>点击添加频道</Text>
        </View>
        <View style={styles.categoryItemContainer}>
          {unaddedCategories.map((category: Category) => (
            <TouchableOpacity
              key={`${category.name}`}
              style={styles.categoryItem}
              onPress={addCategoryHandler(category)}
            >
              <Text style={styles.categoryItemText}>
                {isInEditMode ? `+${category.name}` : category.name}
              </Text>
            </TouchableOpacity>
          ))}
        </View>
      </>
    );
  };

  const styles = StyleSheet.create({
    modalContainer: {
      width: '100%',
      height: '100%',
      backgroundColor: 'transparent'
    },
    content: {
      width: '100%',
      // height: '80%',
      backgroundColor: 'white',
      marginTop: 48 + statusBarHeight,
      paddingBottom: 40
    },
    mask: {
      width: '100%',
      flex: 1,
      backgroundColor: '#00000060'
    },
    categoryHeader: {
      width: '100%',
      flexDirection: 'row',
      alignItems: 'center'
    },
    categoryTitle: {
      fontSize: 16,
      color: '#333',
      fontWeight: 'bold',
      marginLeft: 16
    },
    categoryDescription: {
      fontSize: 13,
      color: '#999',
      marginLeft: 12,
      flex: 1
    },
    editBtn: {
      paddingHorizontal: 10,
      height: 28,
      backgroundColor: '#EEE',
      borderRadius: 14,
      justifyContent: 'center',
      alignItems: 'center'
    },
    editBtnText: {
      fontSize: 14,
      color: '#ff2442'
    },
    closeIcon: {
      padding: 16
    },
    closeIconImg: {
      width: 16,
      height: 16,
      resizeMode: 'contain',
      transform: [{ rotate: '90deg' }]
    },
    categoryItemContainer: {
      width: '100%',
      flexDirection: 'row',
      flexWrap: 'wrap'
    },
    categoryItem: {
      width: (SCREEN_WIDTH - 80) >> 2,
      height: 32,
      justifyContent: 'center',
      alignItems: 'center',
      borderWidth: 1,
      borderColor: '#eee',
      borderRadius: 6,
      marginLeft: 16,
      marginTop: 12
    },
    categoryItemDefault: {
      width: (SCREEN_WIDTH - 80) >> 2,
      height: 32,
      justifyContent: 'center',
      alignItems: 'center',
      backgroundColor: '#eee',
      borderRadius: 6,
      marginLeft: 16,
      marginTop: 12
    },
    categoryItemText: {
      fontSize: 16,
      color: '#666'
    },
    deleteImg: {
      width: 16,
      height: 16,
      position: 'absolute',
      top: -6,
      right: -6
    }
  });

  return (
    <Modal
      transparent={true}
      visible={modalVisibility}
      statusBarTranslucent={true}
      animationType='fade'
      onRequestClose={hideModal}
    >
      <SafeAreaView style={styles.modalContainer}>
        <View style={styles.content}>
          <AddedCategories />
          <UnaddedCategories />
        </View>
        <TouchableOpacity style={styles.mask} onPress={hideModal}>
          {/* <View style={styles.mask}></View> */}
        </TouchableOpacity>
      </SafeAreaView>
    </Modal>
  );
});

export default CategoryModal;


写回答

1回答

FE大公爵

2025-04-22

1、LayoutAnimation问题,我看你代码中好像没有用到LayoutAnimation,如果出现奔溃,那可能是用法不对,或者部分属性不支持LayoutAnimation。要看具体情况,你可以把LayoutAnimation的用法贴出来。

2、Modal组件高度问题,你的statusBarHeight要用State保存,第一次是0,加载之后才赋值。所以高度不对。

3、你的categories不是一个可监听对象,要么用State,要么放在store对象里。如果放在store里,组件记得加observer

0
0

RN从0到1系统精讲与小红书APP实战

30+小案例+2个实战项目,快人一步提升个职业竞争力

295 学习 · 211 问题

查看课程