RN for OpenHarmony 实战 TodoList 项目:删除任务按钮
案例开源地址:https://atomgit.com/lqjmac/rn_openharmony_todolist
在待办事项应用中,删除功能是必不可少的。用户需要能够移除已经不需要的任务,保持列表的整洁。然而,删除是一个"危险"操作,一旦执行就无法撤销,因此在设计删除按钮时需要特别注意用户体验,既要让操作便捷,又要防止误删。
本文是 RN for OpenHarmony 实战 TodoList 系列的第五篇,我们将深入探讨删除按钮的实现,从基础的点击删除到进阶的确认机制,带你全面掌握删除功能的开发技巧。
删除按钮的基础实现
在我们的 TodoList 项目中,每个任务卡片的右侧都有一个删除按钮:
const TaskItem = ({item, index}: {item: Task; index: number}) => {
const itemAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
Animated.timing(itemAnim, {toValue: 1, duration: 300, delay: index * 50, useNativeDriver: true}).start();
}, []);
return (
<Animated.View style={[styles.taskCard, {backgroundColor: theme.card, borderColor: theme.border, opacity: itemAnim,
transform: [{translateX: itemAnim.interpolate({inputRange: [0, 1], outputRange: [-50, 0]})}],
shadowColor: '#000', shadowOffset: {width: 0, height: 2}, shadowOpacity: darkMode ? 0.3 : 0.1, shadowRadius: 4, elevation: 3}]}>
<View style={[styles.priorityBar, {backgroundColor: priorityColors[item.priority]}]} />
<View style={styles.taskContent}>
<TouchableOpacity onPress={() => toggleTask(item.id)} style={styles.checkbox}>
<View style={[styles.checkboxInner, {borderColor: theme.accent}, item.completed && {backgroundColor: theme.accent}]}>
{item.completed && <Text style={styles.checkmark}>✓</Text>}
</View>
</TouchableOpacity>
<View style={styles.taskInfo}>
<Text style={[styles.taskTitle, {color: theme.text}, item.completed && styles.completedText]}>{item.title}</Text>
<View style={styles.taskMeta}>
<View style={[styles.categoryTag, {backgroundColor: theme.accent + '30'}]}>
<Text style={[styles.categoryText, {color: theme.accent}]}>{item.category}</Text>
</View>
<Text style={[styles.dueDate, {color: theme.subText}]}>📅 {item.dueDate}</Text>
</View>
{item.note ? <Text style={[styles.noteText, {color: theme.subText}]} numberOfLines={1}>💬 {item.note}</Text> : null}
</View>
<TouchableOpacity onPress={() => deleteTask(item.id)} style={styles.deleteBtn}>
<Text style={styles.deleteBtnText}>×</Text>
</TouchableOpacity>
</View>
</Animated.View>
);
};
让我们聚焦于删除按钮相关的代码部分。
删除按钮结构解析
<TouchableOpacity onPress={() => deleteTask(item.id)} style={styles.deleteBtn}>
<Text style={styles.deleteBtnText}>×</Text>
</TouchableOpacity>
组件选择
我们使用 TouchableOpacity 作为删除按钮的容器,它提供了点击时的透明度反馈效果。
onPress 事件处理
onPress={() => deleteTask(item.id)}
点击时调用 deleteTask 函数,传入当前任务的 id。这里使用箭头函数是因为我们需要传递参数。
删除图标
<Text style={styles.deleteBtnText}>×</Text>
使用 × 字符作为删除图标。这是一个简单而通用的删除符号,用户一眼就能理解其含义。
使用文字字符而不是图标库的好处是简单直接,不需要额外的依赖。当然,如果你的项目已经使用了图标库,也可以使用更精美的删除图标。
删除按钮样式详解
const styles = StyleSheet.create({
deleteBtn: {width: 32, height: 32, justifyContent: 'center', alignItems: 'center'},
deleteBtnText: {fontSize: 24, color: '#ff6b6b', fontWeight: '300'},
});
deleteBtn 样式
deleteBtn: {
width: 32,
height: 32,
justifyContent: 'center',
alignItems: 'center'
},
尺寸设置
width: 32和height: 32- 32x32 像素的点击区域,比视觉上的 × 符号大,方便用户点击
居中对齐
justifyContent: 'center'和alignItems: 'center'- 让 × 符号在按钮区域内居中显示
deleteBtnText 样式
deleteBtnText: {
fontSize: 24,
color: '#ff6b6b',
fontWeight: '300'
},
字号
fontSize: 24- 较大的字号让删除符号清晰可见
颜色
color: '#ff6b6b'- 红色是删除操作的标准颜色,用户一看就知道这是一个"危险"操作
字重
fontWeight: '300'- 较细的字重让 × 符号看起来更优雅,不会太过突兀
红色在用户界面设计中通常表示警告或危险操作,使用红色作为删除按钮的颜色是一个广泛接受的设计惯例。
删除逻辑实现
当用户点击删除按钮时,会触发 deleteTask 函数:
const deleteTask = (id: string) => {
setTasks(tasks.filter(task => task.id !== id));
};
函数参数
(id: string)
接收要删除的任务 ID。
过滤逻辑
tasks.filter(task => task.id !== id)
使用数组的 filter 方法创建一个新数组,只保留 id 不等于传入 id 的任务。
工作原理
- filter 方法遍历 tasks 数组中的每个任务
- 对于每个任务,检查其 id 是否不等于要删除的 id
- 如果条件为 true,该任务被保留在新数组中
- 如果条件为 false(即 id 相等),该任务被排除
状态更新
setTasks(tasks.filter(...))
将过滤后的新数组设置为新的 tasks 状态,触发组件重新渲染,被删除的任务从列表中消失。
这种方式是 React 中删除数组元素的标准做法。我们不直接修改原数组(如使用 splice),而是创建一个不包含被删除元素的新数组。
删除确认机制
直接删除虽然操作简便,但存在误删的风险。在实际应用中,通常会添加删除确认机制:
使用 Alert 确认
import {Alert} from 'react-native';
const confirmDelete = (id: string, title: string) => {
Alert.alert(
'确认删除',
`确定要删除任务"${title}"吗?`,
[
{
text: '取消',
style: 'cancel',
},
{
text: '删除',
style: 'destructive',
onPress: () => deleteTask(id),
},
],
{cancelable: true}
);
};
Alert.alert 参数说明
第一个参数:标题
'确认删除'
弹窗的标题,简洁明了地说明这是一个删除确认。
第二个参数:消息
`确定要删除任务"${title}"吗?`
弹窗的详细消息,包含任务标题让用户确认是否删除正确的任务。
第三个参数:按钮数组
[
{
text: '取消',
style: 'cancel',
},
{
text: '删除',
style: 'destructive',
onPress: () => deleteTask(id),
},
]
定义两个按钮:
- 取消按钮 - style 为 ‘cancel’,在 iOS 上会显示为蓝色,表示这是安全的选择
- 删除按钮 - style 为 ‘destructive’,在 iOS 上会显示为红色,表示这是危险操作
第四个参数:选项
{cancelable: true}
在 Android 上,设置 cancelable 为 true 允许用户点击弹窗外部来取消。
修改删除按钮的 onPress
<TouchableOpacity onPress={() => confirmDelete(item.id, item.title)} style={styles.deleteBtn}>
<Text style={styles.deleteBtnText}>×</Text>
</TouchableOpacity>
将 deleteTask 替换为 confirmDelete,并传入任务标题用于显示。
批量删除功能
在设置页面,我们实现了批量删除功能:
const SettingsPage = () => (
<ScrollView style={styles.pageContainer} showsVerticalScrollIndicator={false}>
<Text style={[styles.pageTitle, {color: theme.text}]}>⚙️ 设置</Text>
{/* 数据管理 */}
<View style={[styles.settingsCard, {backgroundColor: theme.card, borderColor: theme.border}]}>
<Text style={[styles.settingsCardTitle, {color: theme.text}]}>数据管理</Text>
<TouchableOpacity style={[styles.settingButton, {backgroundColor: theme.accent}]} onPress={clearCompleted}>
<Text style={styles.settingButtonText}>清除已完成任务</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.settingButton, {backgroundColor: '#ff6b6b', marginTop: 12}]} onPress={() => setTasks([])}>
<Text style={styles.settingButtonText}>清除所有任务</Text>
</TouchableOpacity>
</View>
</ScrollView>
);
清除已完成任务
const clearCompleted = () => {
setTasks(tasks.filter(task => !task.completed));
};
这个函数使用 filter 方法只保留未完成的任务:
!task.completed- 当任务未完成时为 true,保留该任务- 当任务已完成时为 false,排除该任务
清除所有任务
onPress={() => setTasks([])}
直接将 tasks 设置为空数组,删除所有任务。
批量删除按钮样式
settingButton: {paddingVertical: 14, borderRadius: 12, alignItems: 'center'},
settingButtonText: {color: '#fff', fontSize: 14, fontWeight: '600'},
清除已完成任务按钮
style={[styles.settingButton, {backgroundColor: theme.accent}]}
使用主题强调色(紫色),表示这是一个相对安全的操作。
清除所有任务按钮
style={[styles.settingButton, {backgroundColor: '#ff6b6b', marginTop: 12}]}
使用红色背景,强调这是一个危险操作。marginTop 添加与上一个按钮的间距。
批量删除是高风险操作,建议在实际应用中也添加确认弹窗。
删除动画效果
为了提升用户体验,可以在删除时添加动画效果,让任务"优雅地"消失:
淡出动画
const TaskItemWithDeleteAnimation = ({item, onDelete}) => {
const fadeAnim = useRef(new Animated.Value(1)).current;
const heightAnim = useRef(new Animated.Value(1)).current;
const handleDelete = () => {
Animated.parallel([
Animated.timing(fadeAnim, {
toValue: 0,
duration: 300,
useNativeDriver: true,
}),
Animated.timing(heightAnim, {
toValue: 0,
duration: 300,
useNativeDriver: false,
}),
]).start(() => {
onDelete(item.id);
});
};
return (
<Animated.View style={{
opacity: fadeAnim,
transform: [{scaleY: heightAnim}],
}}>
{/* 任务内容 */}
<TouchableOpacity onPress={handleDelete}>
<Text>×</Text>
</TouchableOpacity>
</Animated.View>
);
};
动画流程说明
- 触发删除 - 用户点击删除按钮
- 播放动画 - 同时执行淡出和缩小动画
- 动画完成 - 在 start 的回调中执行实际的删除操作
- 更新状态 - 任务从列表中移除
滑动删除
更高级的交互方式是滑动删除,用户可以向左滑动任务卡片来显示删除按钮:
import {PanResponder} from 'react-native';
const SwipeableTaskItem = ({item, onDelete}) => {
const translateX = useRef(new Animated.Value(0)).current;
const panResponder = useRef(
PanResponder.create({
onMoveShouldSetPanResponder: (_, gestureState) => {
return Math.abs(gestureState.dx) > 10;
},
onPanResponderMove: (_, gestureState) => {
if (gestureState.dx < 0) {
translateX.setValue(gestureState.dx);
}
},
onPanResponderRelease: (_, gestureState) => {
if (gestureState.dx < -100) {
Animated.timing(translateX, {
toValue: -400,
duration: 200,
useNativeDriver: true,
}).start(() => onDelete(item.id));
} else {
Animated.spring(translateX, {
toValue: 0,
useNativeDriver: true,
}).start();
}
},
})
).current;
return (
<View style={styles.swipeContainer}>
<View style={styles.deleteBackground}>
<Text style={styles.deleteBackgroundText}>删除</Text>
</View>
<Animated.View
style={{transform: [{translateX}]}}
{...panResponder.panHandlers}
>
{/* 任务内容 */}
</Animated.View>
</View>
);
};
滑动删除是移动应用中非常流行的交互模式,它既节省空间又符合用户的直觉。不过实现起来相对复杂,需要处理手势识别和动画。
撤销删除功能
为了进一步防止误删,可以实现撤销删除功能:
软删除方案
不立即从数组中移除任务,而是标记为已删除:
interface Task {
id: string;
title: string;
completed: boolean;
deleted: boolean;
deletedAt?: number;
// ...其他字段
}
const softDelete = (id: string) => {
setTasks(tasks.map(task =>
task.id === id
? {...task, deleted: true, deletedAt: Date.now()}
: task
));
showUndoSnackbar(id);
};
const undoDelete = (id: string) => {
setTasks(tasks.map(task =>
task.id === id
? {...task, deleted: false, deletedAt: undefined}
: task
));
};
const permanentDelete = (id: string) => {
setTasks(tasks.filter(task => task.id !== id));
};
显示撤销提示
const [undoTask, setUndoTask] = useState<string | null>(null);
const showUndoSnackbar = (id: string) => {
setUndoTask(id);
setTimeout(() => {
if (undoTask === id) {
permanentDelete(id);
setUndoTask(null);
}
}, 5000);
};
{undoTask && (
<View style={styles.snackbar}>
<Text style={styles.snackbarText}>任务已删除</Text>
<TouchableOpacity onPress={() => {
undoDelete(undoTask);
setUndoTask(null);
}}>
<Text style={styles.undoButton}>撤销</Text>
</TouchableOpacity>
</View>
)}
撤销流程
- 用户点击删除,任务被标记为 deleted
- 显示底部提示条,包含"撤销"按钮
- 如果用户在 5 秒内点击撤销,任务恢复
- 如果 5 秒后没有撤销,任务被永久删除
删除按钮的位置设计
删除按钮的位置会影响用户的操作效率和误触概率:
当前设计:右侧固定位置
<View style={styles.taskContent}>
<TouchableOpacity style={styles.checkbox}>...</TouchableOpacity>
<View style={styles.taskInfo}>...</View>
<TouchableOpacity style={styles.deleteBtn}>...</TouchableOpacity>
</View>
任务内容区域使用 flexDirection: ‘row’ 布局,删除按钮自然位于最右侧。
优点
- 位置固定,用户容易找到
- 与勾选框分开,减少误触
缺点
- 始终可见,可能造成视觉干扰
- 占用水平空间
替代方案:长按显示
const [showDelete, setShowDelete] = useState(false);
<TouchableOpacity
onLongPress={() => setShowDelete(true)}
onPress={() => setShowDelete(false)}
>
<View style={styles.taskContent}>
{/* 任务内容 */}
{showDelete && (
<TouchableOpacity style={styles.deleteBtn} onPress={() => deleteTask(item.id)}>
<Text style={styles.deleteBtnText}>×</Text>
</TouchableOpacity>
)}
</View>
</TouchableOpacity>
优点
- 界面更简洁
- 减少误触
缺点
- 操作不够直观
- 需要用户学习
无障碍支持
删除按钮需要良好的无障碍支持:
<TouchableOpacity
onPress={() => deleteTask(item.id)}
style={styles.deleteBtn}
accessible={true}
accessibilityRole="button"
accessibilityLabel={`删除任务:${item.title}`}
accessibilityHint="点击删除此任务"
>
<Text style={styles.deleteBtnText}>×</Text>
</TouchableOpacity>
无障碍属性说明
accessibilityRole
accessibilityRole="button"
告诉辅助技术这是一个按钮。
accessibilityLabel
accessibilityLabel={`删除任务:${item.title}`}
包含任务标题,让用户知道将要删除哪个任务。
accessibilityHint
accessibilityHint="点击删除此任务"
提供操作提示。
性能优化
使用 useCallback
const deleteTask = useCallback((id: string) => {
setTasks(prevTasks => prevTasks.filter(task => task.id !== id));
}, []);
使用 useCallback 包裹 deleteTask 函数,避免每次渲染都创建新的函数引用。
避免在 onPress 中创建新函数
如果列表很长,每个任务项都创建一个新的箭头函数会影响性能:
// 不推荐
onPress={() => deleteTask(item.id)}
// 推荐:使用 bind
onPress={deleteTask.bind(null, item.id)}
// 或者:将 id 作为组件属性传递
const DeleteButton = React.memo(({id, onDelete}) => (
<TouchableOpacity onPress={() => onDelete(id)}>
<Text>×</Text>
</TouchableOpacity>
));
常见问题与解决方案
问题 1:删除后列表闪烁
可能的原因和解决方案:
- key 设置不正确 - 确保使用唯一且稳定的 key(如 id),不要使用数组索引
- 动画冲突 - 检查是否有多个动画同时作用于同一元素
问题 2:快速连续删除导致问题
解决方案:添加防抖或禁用状态
const [isDeleting, setIsDeleting] = useState(false);
const handleDelete = async (id: string) => {
if (isDeleting) return;
setIsDeleting(true);
await deleteTask(id);
setIsDeleting(false);
};
问题 3:删除确认弹窗在某些设备上不显示
解决方案:检查 Alert 的导入和使用是否正确
import {Alert} from 'react-native';
// 确保在组件内部调用
Alert.alert('标题', '消息', [...]);
总结
删除功能虽然看似简单,但要做好需要考虑很多细节。好的删除体验应该是:操作便捷、有确认机制、支持撤销、视觉反馈清晰。
在下一篇文章中,我们将探讨 TodoList 项目中的任务完成划线效果,学习如何通过样式变化来表示任务的完成状态,敬请期待。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)