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