关于动画和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
00
相似问题