请添加图片描述

案例开源地址: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: 32height: 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 的任务。

工作原理

  1. filter 方法遍历 tasks 数组中的每个任务
  2. 对于每个任务,检查其 id 是否不等于要删除的 id
  3. 如果条件为 true,该任务被保留在新数组中
  4. 如果条件为 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>
  );
};

动画流程说明

  1. 触发删除 - 用户点击删除按钮
  2. 播放动画 - 同时执行淡出和缩小动画
  3. 动画完成 - 在 start 的回调中执行实际的删除操作
  4. 更新状态 - 任务从列表中移除

滑动删除

更高级的交互方式是滑动删除,用户可以向左滑动任务卡片来显示删除按钮:

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>
)}

撤销流程

  1. 用户点击删除,任务被标记为 deleted
  2. 显示底部提示条,包含"撤销"按钮
  3. 如果用户在 5 秒内点击撤销,任务恢复
  4. 如果 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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐