侧滑菜单控件,支持ListView和RecyclerView的左右滑动操作
SwipeMenuListView
简介
滑动菜单列表组件,支持自定义菜单项和流畅动画效果。
安装
ohpm install @ohos/swipemenulistview
OpenHarmony ohpm 环境配置等更多内容,请参考如何安装 OpenHarmony ohpm 包。
基础用法
1. 导入组件
import {
SwipeMenuList,
SwipeMenu,
SwipeMenuItem,
SwipeMenuCreator,
SwipeStateController,
TouchConfig,
AnimationConfig,
AnimationType,
OnMenuItemClickListener,
OnSwipeListener,
OnMenuStateChangeListener,
OnAnimationStateChangeListener,
OnItemLongPressListener,
InterpolatorType,
SwipeDirection
} from '@ohos/swipemenulistview';
2. 创建菜单创建器
菜单创建器接口支持多视图类型,可以根据数据项和位置创建不同的菜单:
class MyMenuCreator implements SwipeMenuCreator {
create(menu: SwipeMenu, item?: Object, position?: number): void {
// 添加删除菜单
const deleteItem = new SwipeMenuItem({
id: 'delete',
title: '删除',
background: '#F44336',
titleColor: '#FFFFFF',
width: 90,
icon: $r('app.media.delete_icon')
});
menu.addMenuItem(deleteItem);
// 添加编辑菜单
const editItem = new SwipeMenuItem({
id: 'edit',
title: '编辑',
background: '#2196F3',
titleColor: '#FFFFFF',
width: 90
});
menu.addMenuItem(editItem);
}
}
3. 使用组件
@Entry
@Component
struct MainPage {
// 定义数据源,每个对象需要有唯一的id
@State dataList: Array<Object> = [
{ id: '1', name: '项目1' },
{ id: '2', name: '项目2' },
{ id: '3', name: '项目3' }
];
// 触摸配置,控制滑动敏感度
@State touchConfig: TouchConfig = TouchConfig.default();
// 菜单打开动画配置
@State openAnimationConfig: AnimationConfig = AnimationConfig.default();
// 菜单关闭动画配置,使用快速动画
@State closeAnimationConfig: AnimationConfig = AnimationConfig.fast();
// 提供滑动状态控制器,用于编程式控制菜单
@Provide('swipeStateController') swipeController: SwipeStateController = new SwipeStateController();
// 创建菜单创建器实例
private menuCreator: MyMenuCreator = new MyMenuCreator();
build() {
Column() {
SwipeMenuList({
data: this.dataList, // 绑定数据源
menuCreator: this.menuCreator, // 设置菜单创建器
touchConfig: this.touchConfig, // 应用触摸配置
openAnimationConfig: this.openAnimationConfig, // 设置打开动画
closeAnimationConfig: this.closeAnimationConfig, // 设置关闭动画
itemBuilder: (item: Object, index: number): void => {
this.ItemBuilder(item, index); // 自定义列表项构建器
},
onMenuItemClick: (position: number, menu: SwipeMenu, menuIndex: number): boolean => {
const clickedItem = menu.getMenuItem(menuIndex); // 获取被点击的菜单项
if (clickedItem?.getId() === 'delete') { // 判断是否为删除按钮
this.deleteItem(position); // 执行删除操作
}
return false; // 返回false表示点击后关闭菜单
}
})
}
}
// 自定义列表项构建器
@Builder
ItemBuilder(item: Object, index: number) {
Row() {
Text((item as any).name)
.fontSize(16)
.fontColor('#333333')
}
.width('100%')
.height(60)
.padding({ left: 16, right: 16 })
.backgroundColor('#FFFFFF')
.justifyContent(FlexAlign.Start)
.alignItems(VerticalAlign.Center)
}
// 删除指定位置的数据项
private deleteItem(position: number) {
this.dataList = this.dataList.filter((_, index) => index !== position);
}
}
更多配置
触摸配置
// 敏感模式 - 容易触发滑动
@State touchConfig: TouchConfig = TouchConfig.sensitive();
// 严格模式 - 难以触发滑动
@State touchConfig: TouchConfig = TouchConfig.strict();
// 自定义配置
@State touchConfig: TouchConfig = new TouchConfig({
minSwipeDistance: 20, // 最小滑动距离(像素)
minFlingVelocity: 800, // 快速滑动最小速度(像素/秒)
menuOpenThreshold: 0.6, // 菜单打开阈值(0-1,表示滑动距离占菜单宽度的比例)
enableTouchIntercept: true // 启用触摸拦截,防止与父容器手势冲突
});
动画配置
// 预设动画配置
@State openConfig: AnimationConfig = AnimationConfig.spring(); // 弹簧动画
@State closeConfig: AnimationConfig = AnimationConfig.bounce(); // 回弹动画
// 自定义动画配置
@State customConfig: AnimationConfig = new AnimationConfig({
duration: 300, // 动画持续时间(毫秒)
interpolator: InterpolatorType.ACCELERATE_DECELERATE, // 插值器类型:先加速后减速
springConfig: { // 弹簧动画参数
stiffness: 1.0, // 弹簧刚度,值越大弹性越强
damping: 0.8, // 阻尼系数,值越大震荡越小
mass: 1.0 // 质量,影响动画速度
}
});
滑动方向控制
SwipeMenuList({
swipeDirection: SwipeDirection.DIRECTION_LEFT, // 左滑显示菜单(默认)
// swipeDirection: SwipeDirection.DIRECTION_RIGHT, // 右滑显示菜单
})
条件滑动控制
SwipeMenuList({
getSwipeEnabled: (position: number): boolean => {
// 只有偶数位置的项目可以滑动
return position % 2 === 0;
}
})
事件监听
菜单项点击
onMenuItemClick: (position: number, menu: SwipeMenu, menuIndex: number): boolean => {
const clickedItem = menu.getMenuItem(menuIndex); // 根据索引获取被点击的菜单项
const itemId = clickedItem?.getId(); // 获取菜单项的唯一标识
switch (itemId) {
case 'delete':
// 执行删除操作
console.log(`删除第${position}项`);
break;
case 'edit':
// 执行编辑操作
console.log(`编辑第${position}项`);
break;
}
return false; // 返回false关闭菜单,返回true保持菜单打开状态
}
滑动事件监听
class MySwipeListener implements OnSwipeListener {
onSwipeStart = (position: number): void => {
console.log(`滑动开始 - 位置: ${position}`);
}
onSwipeEnd = (position: number): void => {
console.log(`滑动结束 - 位置: ${position}`);
}
}
SwipeMenuList({
onSwipeListener: new MySwipeListener()
})
菜单状态监听
class MyMenuStateListener implements OnMenuStateChangeListener {
onMenuOpen = (position: number): void => {
console.log(`菜单打开 - 位置: ${position}`);
}
onMenuClose = (position: number): void => {
console.log(`菜单关闭 - 位置: ${position}`);
}
}
SwipeMenuList({
onMenuStateChangeListener: new MyMenuStateListener()
})
长按事件监听
class MyLongPressListener implements OnItemLongPressListener {
onItemLongPress = (position: number, item: Object): boolean => {
console.log(`长按了第${position}项`);
// 可以在这里弹出对话框、显示更多选项等
return true;
}
}
SwipeMenuList({
onItemLongPress: new MyLongPressListener()
})
动画状态监听
class MyAnimationListener implements OnAnimationStateChangeListener {
onAnimationStart = (itemPosition: number, animationType: AnimationType): void => {
console.log(`动画开始 - 位置: ${itemPosition}, 类型: ${animationType}`);
}
onAnimationEnd = (itemPosition: number, animationType: AnimationType): void => {
console.log(`动画结束 - 位置: ${itemPosition}, 类型: ${animationType}`);
}
onDragProgress = (itemPosition: number, progress: number, offsetX: number): void => {
console.log(`拖拽进度 - 位置: ${itemPosition}, 进度: ${progress.toFixed(2)}, 偏移: ${offsetX}`);
}
}
SwipeMenuList({
onAnimationStateChangeListener: new MyAnimationListener()
})
编程式控制
// 获取状态控制器(通过@Consume注入,需要与@Provide配对使用)
@Consume('swipeStateController') swipeController: SwipeStateController;
// 主动打开指定位置的菜单(带动画效果)
this.swipeController.smoothOpenMenu(position);
// 关闭当前打开的菜单(如果有的话)
this.swipeController.smoothCloseMenu();
// 检查指定位置的菜单是否处于打开状态
const isOpen = this.swipeController.isMenuOpen(position);
// 动态更新触摸配置,立即生效
this.swipeController.updateTouchConfig(TouchConfig.sensitive());
自定义菜单项
SwipeMenuItem 配置选项
const menuItem = new SwipeMenuItem({
id: 'custom', // 唯一标识
title: '自定义', // 显示文字
icon: $r('app.media.icon'), // 图标资源
background: '#FF5722', // 背景色
titleColor: '#FFFFFF', // 文字颜色
titleSize: 14, // 文字大小
width: 100 // 宽度
});
动态修改菜单项
// 获取菜单项
const menuItem = menu.getMenuItem(0);
// 修改属性
menuItem?.setTitle('新标题');
menuItem?.setBackground('#4CAF50');
menuItem?.setWidth(120);
API 参考
SwipeMenuList 属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| data | Array | [] | 数据源数组 |
| menuCreator | SwipeMenuCreator | DefaultMenuCreator | 菜单创建器 |
| itemBuilder | BuilderParam | - | 列表项构建器 |
| touchConfig | TouchConfig | TouchConfig.default() | 触摸配置 |
| openAnimationConfig | AnimationConfig | AnimationConfig.default() | 打开动画配置 |
| closeAnimationConfig | AnimationConfig | AnimationConfig.default() | 关闭动画配置 |
| swipeDirection | number | SwipeDirection.DIRECTION_LEFT | 滑动方向 |
| dragFollowAnimation | boolean | true | 是否启用拖拽跟随动画 |
| enableSpringBack | boolean | true | 是否启用回弹效果 |
| keyGenerator | (item: Object) => string | JSON.stringify | 唯一键生成器 |
| getSwipeEnabled | (position: number) => boolean | undefined | 滑动能力控制函数 |
SwipeMenuList 事件
| 事件 | 类型 | 说明 |
|---|---|---|
| onMenuItemClick | OnMenuItemClickListener | 菜单项点击事件 |
| onSwipeListener | OnSwipeListener | 滑动事件监听 |
| onMenuStateChangeListener | OnMenuStateChangeListener | 菜单状态变化监听 |
| onAnimationStateChangeListener | OnAnimationStateChangeListener | 动画状态变化监听 |
| onItemLongPress | OnItemLongPressListener | 长按事件监听 |
TouchConfig 预设配置
| 方法 | 说明 |
|---|---|
| TouchConfig.default() | 默认配置 |
| TouchConfig.sensitive() | 敏感配置(容易触发) |
| TouchConfig.strict() | 严格配置(难以触发) |
AnimationConfig 预设配置
| 方法 | 说明 |
|---|---|
| AnimationConfig.default() | 默认动画 |
| AnimationConfig.fast() | 快速动画 |
| AnimationConfig.slow() | 慢速动画 |
| AnimationConfig.spring() | 弹簧动画 |
| AnimationConfig.bounce() | 回弹动画 |
| AnimationConfig.overshoot() | 过冲动画 |
| AnimationConfig.anticipate() | 预期动画 |
| AnimationConfig.none() | 无动画 |
InterpolatorType 动画插值器
| 类型 | 说明 |
|---|---|
| LINEAR | 线性插值 |
| ACCELERATE | 加速 |
| DECELERATE | 减速 |
| ACCELERATE_DECELERATE | 加速减速 |
| SPRING | 弹簧效果 |
| BOUNCE | 回弹效果 |
| OVERSHOOT | 过冲效果 |
| ANTICIPATE | 预期效果 |
菜单动态更新
当需要在运行时动态更新菜单时,必须确保数据源的对象引用发生变化,以触发ForEach组件重新渲染。
数据模型定义
// 定义数据接口
interface ContactInfoData {
id: string;
name: string;
phone: string;
email: string;
avatar: Resource;
isVip: boolean;
isOnline: boolean;
lastMessage: string;
messageTime: string;
_updateTimestamp?: number; // 用于触发UI刷新的时间戳
}
// 使用@Observed装饰器声明数据类
@Observed
class ContactInfo {
id: string;
name: string;
phone: string;
email: string;
avatar: Resource;
isVip: boolean;
isOnline: boolean;
lastMessage: string;
messageTime: string;
_updateTimestamp?: number;
constructor(data: ContactInfoData) {
this.id = data.id;
this.name = data.name;
this.phone = data.phone;
this.email = data.email;
this.avatar = data.avatar;
this.isVip = data.isVip;
this.isOnline = data.isOnline;
this.lastMessage = data.lastMessage;
this.messageTime = data.messageTime;
this._updateTimestamp = data._updateTimestamp;
}
}
关键配置
SwipeMenuList({
data: this.contactList,
menuCreator: this.menuCreator,
// 关键:自定义keyGenerator,使用id+timestamp确保key值变化
keyGenerator: (item: Object) => {
const contact = item as ContactInfo;
return contact.id + '_' + (contact._updateTimestamp || 0);
},
})
统一刷新方法
@Entry
@Component
struct DemoPage {
@State contactList: ContactInfo[] = [];
@State dynamicMenuTestEnabled: boolean = false;
@State removeTestItemEnabled: boolean = false;
@State menuItemModifyEnabled: boolean = false;
/**
* 刷新联系人列表以触发UI更新
* ForEach需要通过修改对象引用来触发UI更新
* 添加时间戳确保keyGenerator生成不同的key值
*/
private refreshContactList(): void {
const timestamp = Date.now();
this.contactList = this.contactList.map((contact: ContactInfo): ContactInfo => {
const data: ContactInfoData = {
id: contact.id,
name: contact.name,
phone: contact.phone,
email: contact.email,
avatar: contact.avatar,
isVip: contact.isVip,
isOnline: contact.isOnline,
lastMessage: contact.lastMessage,
messageTime: contact.messageTime,
_updateTimestamp: timestamp
};
return new ContactInfo(data);
});
}
// 动态添加菜单项
private addMenuItem(): void {
this.dynamicMenuTestEnabled = true;
this.refreshContactList();
}
// 动态移除菜单项
private removeMenuItem(): void {
this.removeTestItemEnabled = true;
this.refreshContactList();
}
// 修改菜单项内容
private modifyMenuItem(): void {
this.menuItemModifyEnabled = true;
this.refreshContactList();
}
// 重置所有菜单状态
private resetMenuStates(): void {
this.dynamicMenuTestEnabled = false;
this.removeTestItemEnabled = false;
this.menuItemModifyEnabled = false;
this.refreshContactList();
}
}
动态菜单创建器
class MenuCreator implements SwipeMenuCreator {
private component: DemoPage;
constructor(component: DemoPage) {
this.component = component;
}
create(menu: SwipeMenu, item?: Object, position?: number): void {
if (item) {
const contact = item as ContactInfo;
// 基础菜单项
const deleteItem = new SwipeMenuItem({
id: 'delete',
title: '删除',
background: '#F44336',
titleColor: '#FFFFFF',
width: 90
});
menu.addMenuItem(deleteItem);
// 根据状态动态添加菜单项
if (this.component.isDynamicMenuTestEnabled()) {
const testItem = new SwipeMenuItem({
id: 'test_item',
title: '测试',
background: '#607D8B',
titleColor: '#FFFFFF',
width: 90
});
menu.addMenuItem(testItem);
}
// 根据状态动态移除菜单项
if (this.component.shouldRemoveTestItem()) {
const menuItems = menu.getMenuItems();
const itemToRemove = menuItems.find(item => item.getId() === 'test_item');
if (itemToRemove) {
menu.removeMenuItem(itemToRemove);
}
}
// 根据状态动态修改菜单项
if (this.component.shouldModifyMenuItem()) {
const firstMenuItem = menu.getMenuItem(0);
if (firstMenuItem) {
firstMenuItem.setTitle('已修改');
firstMenuItem.setBackground('#4CAF50');
firstMenuItem.setWidth(120);
}
}
}
}
}
实现要点
- 统一刷新方法: 将重复的刷新逻辑封装到
refreshContactList()方法中 - 对象引用更新: 使用
new ContactInfo(data)创建新的对象实例 - 时间戳机制: 通过
_updateTimestamp确保keyGenerator生成不同的key值 - 状态驱动: 通过组件状态控制菜单的动态行为
- 避免重复代码: 所有需要刷新UI的操作都调用统一的刷新方法
常见错误
// 错误:只更新数组引用,对象引用没变
this.contactList = [...this.contactList];
// 错误:直接修改对象属性
this.contactList[0].name = 'newName';
// 错误:使用spread但没有时间戳
this.contactList = this.contactList.map(contact => ({...contact}));
正确做法
// 正确:创建新对象实例 + 时间戳 + 统一方法
private refreshContactList(): void {
const timestamp = Date.now();
this.contactList = this.contactList.map((contact: ContactInfo): ContactInfo => {
const data: ContactInfoData = {
id: contact.id,
name: contact.name,
_updateTimestamp: timestamp
};
return new ContactInfo(data);
});
}
// 在需要刷新的地方调用
private addMenuItem(): void {
this.dynamicMenuTestEnabled = true;
this.refreshContactList(); // 统一调用刷新方法
}
注意事项
- 状态管理: 组件内部使用 @Provide/@Consume 模式管理状态,确保在使用时正确提供 SwipeStateController
- 手势冲突: 在滚动容器中使用时,组件会自动处理手势冲突,但建议根据实际情况调整 touchConfig
关于混淆
- 代码混淆,请查看代码混淆简介
- 如果希望@ohos/swipemenulistview库在代码混淆过程中不会被混淆,需要在混淆规则配置文件obfuscation-rules.txt中添加相应的排除规则:
-keep
./oh_modules/@ohos/swipemenulistview
约束与限制
在下述版本验证通过:
- IDE:DevEco Studio 5.1.0.849; SDK:API18 (5.1.0.125)。
- IDE:DevEco Studio 5.1.1.823; SDK:API19 (5.1.1.823)。
目录结构
library/ # SwipeMenuListView 组件库
├── src/
│ ├── main/
│ │ └── ets/
│ │ ├── components/ # 核心组件
│ │ │ ├── SwipeMenuList.ets
│ │ │ ├── SwipeMenuListItem.ets
│ │ │ └── SwipeMenuItemLayout.ets
│ │ ├── model/ # 数据模型
│ │ │ ├── SwipeMenuItem.ets
│ │ │ ├── SwipeMenu.ets
│ │ │ ├── SwipeMenuCreator.ets
│ │ │ ├── AnimationConfig.ets
│ │ │ └── TouchConfig.ets
│ │ ├── interfaces/ # 接口定义
│ │ │ ├── OnMenuItemClickListener.ets
│ │ │ ├── OnSwipeListener.ets
│ │ │ ├── OnMenuStateChangeListener.ets
│ │ │ ├── OnAnimationStateChangeListener.ets
│ │ │ └── OnItemLongPressListener.ets
│ │ ├── utils/ # 工具类
│ │ │ ├── SwipeStateController.ets
│ │ │ ├── AnimationInterpolator.ets
│ │ │ ├── SwipeMenuListUtils.ets
│ │ │ ├── SwipeMenuPreferences.ets
│ │ │ └── Logger.ets
│ │ └── constants/ # 常量定义
│ │ ├── SwipeDirection.ets
│ │ └── TouchState.ets
│ ├── ohosTest/ # 组件测试
│ └── test/ # 单元测试
├── Index.ets # 对外导出接口
├── build-profile.json5 # 构建配置
└── oh-package.json5 # 组件库依赖配置
贡献代码
使用过程中发现任何问题都可以提 Issue 给组件,当然也非常欢迎给 发 PR 共建。
开源协议
本项目基于 MIT LICENSE ,请自由地享受和参与开源。