SwipeMenuListView
Introduction
Slide-out menu list component supporting custom menu items and smooth animation effects.
Installation
ohpm install @ohos/swipemenulistview
For more information on OpenHarmony ohpm environment configuration, please refer to How to install OpenHarmony ohpm packages.
Basic Usage
1. Import Components
import {
SwipeMenuList,
SwipeMenu,
SwipeMenuItem,
SwipeMenuCreator,
SwipeStateController,
TouchConfig,
AnimationConfig,
AnimationType,
OnMenuItemClickListener,
OnSwipeListener,
OnMenuStateChangeListener,
OnAnimationStateChangeListener,
OnItemLongPressListener,
InterpolatorType,
SwipeDirection
} from '@ohos/swipemenulistview';
2. Create a Menu Creator
The menu creator interface supports multiple view types, allowing you to create different menus based on data items and positions:
class MyMenuCreator implements SwipeMenuCreator {
create(menu: SwipeMenu, item?: Object, position?: number): void {
// Add delete menu
const deleteItem = new SwipeMenuItem({
id: 'delete',
title: 'Delete',
background: '#F44336',
titleColor: '#FFFFFF',
width: 90,
icon: $r('app.media.delete_icon')
});
menu.addMenuItem(deleteItem);
// Add edit menu
const editItem = new SwipeMenuItem({
id: 'edit',
title: 'Edit',
background: '#2196F3',
titleColor: '#FFFFFF',
width: 90
});
menu.addMenuItem(editItem);
}
}
3. Use the Component
@Entry
@Component
struct MainPage {
// Define the data source, each object needs a unique id
@State dataList: Array<Object> = [
{ id: '1', name: 'Item 1' },
{ id: '2', name: 'Item 2' },
{ id: '3', name: 'Item 3' }
];
// Touch configuration to control swipe sensitivity
@State touchConfig: TouchConfig = TouchConfig.default();
// Menu open animation configuration
@State openAnimationConfig: AnimationConfig = AnimationConfig.default();
// Menu close animation configuration, using a fast animation
@State closeAnimationConfig: AnimationConfig = AnimationConfig.fast();
// Provide a swipe state controller for programmatic menu control
@Provide('swipeStateController') swipeController: SwipeStateController = new SwipeStateController();
// Create a menu creator instance
private menuCreator: MyMenuCreator = new MyMenuCreator();
build() {
Column() {
SwipeMenuList({
data: this.dataList, // Bind the data source
menuCreator: this.menuCreator, // Set the menu creator
touchConfig: this.touchConfig, // Apply touch configuration
openAnimationConfig: this.openAnimationConfig, // Set the open animation
closeAnimationConfig: this.closeAnimationConfig, // Set the close animation
itemBuilder: (item: Object, index: number): void => {
this.ItemBuilder(item, index); // Custom list item builder
},
onMenuItemClick: (position: number, menu: SwipeMenu, menuIndex: number): boolean => {
const clickedItem = menu.getMenuItem(menuIndex); // Get the clicked menu item
if (clickedItem?.getId() === 'delete') { // Check if it's the delete button
this.deleteItem(position); // Perform the delete operation
}
return false; // Returning false closes the menu after a click
}
})
}
}
// Custom list item builder
@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)
}
// Delete the data item at the specified position
private deleteItem(position: number) {
this.dataList = this.dataList.filter((_, index) => index !== position);
}
}
More Configurations
Touch Configuration
// Sensitive mode - easier to trigger swipe
@State touchConfig: TouchConfig = TouchConfig.sensitive();
// Strict mode - harder to trigger swipe
@State touchConfig: TouchConfig = TouchConfig.strict();
// Custom configuration
@State touchConfig: TouchConfig = new TouchConfig({
minSwipeDistance: 20, // Minimum swipe distance (in pixels)
minFlingVelocity: 800, // Minimum fling velocity (in pixels/second)
menuOpenThreshold: 0.6, // Menu open threshold (0-1, the ratio of swipe distance to menu width)
enableTouchIntercept: true // Enable touch interception to prevent gesture conflicts with parent containers
});
Animation Configuration
// Preset animation configurations
@State openConfig: AnimationConfig = AnimationConfig.spring(); // Spring animation
@State closeConfig: AnimationConfig = AnimationConfig.bounce(); // Bounce animation
// Custom animation configuration
@State customConfig: AnimationConfig = new AnimationConfig({
duration: 300, // Animation duration (in milliseconds)
interpolator: InterpolatorType.ACCELERATE_DECELERATE, // Interpolator type: accelerate then decelerate
springConfig: { // Spring animation parameters
stiffness: 1.0, // Spring stiffness, higher value means stronger elasticity
damping: 0.8, // Damping ratio, higher value means less oscillation
mass: 1.0 // Mass, affects animation speed
}
});
Swipe Direction Control
SwipeMenuList({
// ...other configurations
swipeDirection: SwipeDirection.DIRECTION_LEFT, // Swipe left to show menu (default)
// swipeDirection: SwipeDirection.DIRECTION_RIGHT, // Swipe right to show menu
})
Conditional Swipe Control
SwipeMenuList({
// ...other configurations
getSwipeEnabled: (position: number): boolean => {
// Only items at even positions can be swiped
return position % 2 === 0;
}
})
Event Listeners
Menu Item Clicks
onMenuItemClick: (position: number, menu: SwipeMenu, menuIndex: number): boolean => {
const clickedItem = menu.getMenuItem(menuIndex); // Get the clicked menu item by index
const itemId = clickedItem?.getId(); // Get the unique ID of the menu item
switch (itemId) {
case 'delete':
// Perform delete operation
console.log(`Deleting item at position ${position}`);
break;
case 'edit':
// Perform edit operation
console.log(`Editing item at position ${position}`);
break;
}
return false; // Return false to close the menu, true to keep it open
}```
### Swipe Event Listener
```typescript
class MySwipeListener implements OnSwipeListener {
onSwipeStart = (position: number): void => {
console.log(`Swipe start - position: ${position}`);
}
onSwipeEnd = (position: number): void => {
console.log(`Swipe end - position: ${position}`);
}
}
SwipeMenuList({
// ...other configurations
onSwipeListener: new MySwipeListener()
})
Menu State Listener
class MyMenuStateListener implements OnMenuStateChangeListener {
onMenuOpen = (position: number): void => {
console.log(`Menu opened - position: ${position}`);
}
onMenuClose = (position: number): void => {
console.log(`Menu closed - position: ${position}`);
}
}
SwipeMenuList({
// ...other configurations
onMenuStateChangeListener: new MyMenuStateListener()
})
Long Press Event Listener
class MyLongPressListener implements OnItemLongPressListener {
onItemLongPress = (position: number, item: Object): boolean => {
console.log(`Long pressed item at position ${position}`);
// Show context menu or perform other actions
// You can show a dialog, display more options, etc., here
return true; // Return true to prevent the event from propagating to other components
}
}
SwipeMenuList({
// ...other configurations
onItemLongPress: new MyLongPressListener()
})
Animation State Listener
class MyAnimationListener implements OnAnimationStateChangeListener {
onAnimationStart = (itemPosition: number, animationType: AnimationType): void => {
console.log(`Animation start - position: ${itemPosition}, type: ${animationType}`);
}
onAnimationEnd = (itemPosition: number, animationType: AnimationType): void => {
console.log(`Animation end - position: ${itemPosition}, type: ${animationType}`);
}
onDragProgress = (itemPosition: number, progress: number, offsetX: number): void => {
console.log(`Drag progress - position: ${itemPosition}, progress: ${progress.toFixed(2)}, offset: ${offsetX}`);
}
}
SwipeMenuList({
// ...other configurations
onAnimationStateChangeListener: new MyAnimationListener()
})
Programmatic Control
// Get the state controller (injected via @Consume, needs to be paired with @Provide)
@Consume('swipeStateController') swipeController: SwipeStateController;
// Programmatically open the menu at a specific position (with animation)
this.swipeController.smoothOpenMenu(position);
// Close the currently open menu (if any)
this.swipeController.smoothCloseMenu();
// Check if the menu at a specific position is open
const isOpen = this.swipeController.isMenuOpen(position);
// Dynamically update the touch configuration, which takes effect immediately
this.swipeController.updateTouchConfig(TouchConfig.sensitive());
Customizing Menu Items
SwipeMenuItem Configuration Options
const menuItem = new SwipeMenuItem({
id: 'custom', // Unique identifier
title: 'Custom', // Display text
icon: $r('app.media.icon'), // Icon resource
background: '#FF5722', // Background color
titleColor: '#FFFFFF', // Text color
titleSize: 14, // Text size
width: 100 // Width
});
Dynamically Modifying Menu Items
// Get the menu item
const menuItem = menu.getMenuItem(0);
// Modify properties
menuItem?.setTitle('New Title');
menuItem?.setBackground('#4CAF50');
menuItem?.setWidth(120);
API Reference
SwipeMenuList Properties
| Property | Type | Default | Description |
|---|---|---|---|
| data | Array | [] | Data source array |
| menuCreator | SwipeMenuCreator | DefaultMenuCreator | Menu creator |
| itemBuilder | BuilderParam | - | List item builder |
| touchConfig | TouchConfig | TouchConfig.default() | Touch configuration |
| openAnimationConfig | AnimationConfig | AnimationConfig.default() | Open animation configuration |
| closeAnimationConfig | AnimationConfig | AnimationConfig.default() | Close animation configuration |
| swipeDirection | number | SwipeDirection.DIRECTION_LEFT | Swipe direction |
| dragFollowAnimation | boolean | true | Whether to enable drag-follow animation |
| enableSpringBack | boolean | true | Whether to enable spring-back effect |
| keyGenerator | (item: Object) => string | JSON.stringify | Unique key generator |
| getSwipeEnabled | (position: number) => boolean | undefined | Function to control swipe capability |
SwipeMenuList Events
| Event | Type | Description |
|---|---|---|
| onMenuItemClick | OnMenuItemClickListener | Menu item click event |
| onSwipeListener | OnSwipeListener | Swipe event listener |
| onMenuStateChangeListener | OnMenuStateChangeListener | Menu state change listener |
| onAnimationStateChangeListener | OnAnimationStateChangeListener | Animation state change listener |
| onItemLongPress | OnItemLongPressListener | Long press event listener |
TouchConfig Preset Configurations
| Method | Description |
|---|---|
| TouchConfig.default() | Default configuration |
| TouchConfig.sensitive() | Sensitive configuration (easier to trigger) |
| TouchConfig.strict() | Strict configuration (harder to trigger) |
AnimationConfig Preset Configurations
| Method | Description |
|---|---|
| AnimationConfig.default() | Default animation |
| AnimationConfig.fast() | Fast animation |
| AnimationConfig.slow() | Slow animation |
| AnimationConfig.spring() | Spring animation |
| AnimationConfig.bounce() | Bounce animation |
| AnimationConfig.overshoot() | Overshoot animation |
| AnimationConfig.anticipate() | Anticipate animation |
| AnimationConfig.none() | No animation |
InterpolatorType Animation Interpolators
| Type | Description |
|---|---|
| LINEAR | Linear interpolation |
| ACCELERATE | Accelerate |
| DECELERATE | Decelerate |
| ACCELERATE_DECELERATE | Accelerate-decelerate |
| SPRING | Spring effect |
| BOUNCE | Bounce effect |
| OVERSHOOT | Overshoot effect |
| ANTICIPATE | Anticipate effect |
Dynamic Menu Updates
When you need to dynamically update the menu at runtime, you must ensure that the object references in the data source change to trigger a re-render of the ForEach component.
Data Model Definition
// Define the data interface
interface ContactInfoData {
id: string;
name: string;
phone: string;
email: string;
avatar: Resource;
isVip: boolean;
isOnline: boolean;
lastMessage: string;
messageTime: string;
_updateTimestamp?: number; // Timestamp to trigger UI refresh
}
// Declare the data class with the @Observed decorator
@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;
}
}
Key Configuration
SwipeMenuList({
data: this.contactList,
menuCreator: this.menuCreator,
// Key: Customize keyGenerator, use id+timestamp to ensure the key value changes
keyGenerator: (item: Object) => {
const contact = item as ContactInfo;
return contact.id + '_' + (contact._updateTimestamp || 0);
},
// ...other configurations
})
Unified Refresh Method
@Entry
@Component
struct DemoPage {
@State contactList: ContactInfo[] = [];
@State dynamicMenuTestEnabled: boolean = false;
@State removeTestItemEnabled: boolean = false;
@State menuItemModifyEnabled: boolean = false;
/**
* Refreshes the contact list to trigger a UI update.
* ForEach needs to modify object references to trigger a UI update.
* Adding a timestamp ensures that keyGenerator generates different key values.
*/
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);
});
}
// Dynamically add a menu item
private addMenuItem(): void {
this.dynamicMenuTestEnabled = true;
this.refreshContactList();
}
// Dynamically remove a menu item
private removeMenuItem(): void {
this.removeTestItemEnabled = true;
this.refreshContactList();
}
// Modify menu item content
private modifyMenuItem(): void {
this.menuItemModifyEnabled = true;
this.refreshContactList();
}
// Reset all menu states
private resetMenuStates(): void {
this.dynamicMenuTestEnabled = false;
this.removeTestItemEnabled = false;
this.menuItemModifyEnabled = false;
this.refreshContactList();
}
}
Dynamic Menu Creator
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;
// Basic menu item
const deleteItem = new SwipeMenuItem({
id: 'delete',
title: 'Delete',
background: '#F44336',
titleColor: '#FFFFFF',
width: 90
});
menu.addMenuItem(deleteItem);
// Dynamically add a menu item based on state
if (this.component.isDynamicMenuTestEnabled()) {
const testItem = new SwipeMenuItem({
id: 'test_item',
title: 'Test',
background: '#607D8B',
titleColor: '#FFFFFF',
width: 90
});
menu.addMenuItem(testItem);
}
// Dynamically remove a menu item based on state
if (this.component.shouldRemoveTestItem()) {
const menuItems = menu.getMenuItems();
const itemToRemove = menuItems.find(item => item.getId() === 'test_item');
if (itemToRemove) {
menu.removeMenuItem(itemToRemove);
}
}
// Dynamically modify a menu item based on state
if (this.component.shouldModifyMenuItem()) {
const firstMenuItem = menu.getMenuItem(0);
if (firstMenuItem) {
firstMenuItem.setTitle('Modified');
firstMenuItem.setBackground('#4CAF50');
firstMenuItem.setWidth(120);
}
}
}
}
}
Implementation Points
- Unified Refresh Method: Encapsulate repetitive refresh logic into the
refreshContactList()method. - Object Reference Updates: Create new object instances using
new ContactInfo(data). - Timestamp Mechanism: Use
_updateTimestampto ensure keyGenerator generates different key values. - State-Driven: Control the dynamic behavior of the menu through component state.
- Avoid Code Duplication: All operations that require a UI refresh should call the unified refresh method.
Common Mistakes
// Wrong: Only the array reference is updated, not the object references
this.contactList = [...this.contactList];
// Wrong: Directly modifying object properties
this.contactList[0].name = 'newName';
// Wrong: Using spread without a timestamp
this.contactList = this.contactList.map(contact => ({...contact}));
Correct Approach
// Correct: Create new object instances + timestamp + unified method
private refreshContactList(): void {
const timestamp = Date.now();
this.contactList = this.contactList.map((contact: ContactInfo): ContactInfo => {
const data: ContactInfoData = {
id: contact.id,
name: contact.name,
// ...other properties
_updateTimestamp: timestamp
};
return new ContactInfo(data);
});
}
// Call this where a refresh is needed
private addMenuItem(): void {
this.dynamicMenuTestEnabled = true;
this.refreshContactList(); // Call the unified refresh method
}
Notes
- State Management: The component uses the @Provide/@Consume pattern internally for state management. Ensure that
SwipeStateControlleris correctly provided when using it. - Gesture Conflicts: When used within a scrolling container, the component automatically handles gesture conflicts. However, it's recommended to adjust the
touchConfigbased on your specific use case.
About Obfuscation
- For code obfuscation, please refer to the Introduction to Code Obfuscation.
- If you want to prevent the
@ohos/swipemenulistviewlibrary from being obfuscated, you need to add the corresponding exclusion rule to the obfuscation rules file,obfuscation-rules.txt:
-keep
./oh_modules/@ohos/swipemenulistview
Constraints and Limitations
Verified on the following versions:
- 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).
Directory Structure
library/ # SwipeMenuListView component library
├── src/
│ ├── main/
│ │ └── ets/
│ │ ├── components/ # Core components
│ │ │ ├── SwipeMenuList.ets
│ │ │ ├── SwipeMenuListItem.ets
│ │ │ └── SwipeMenuItemLayout.ets
│ │ ├── model/ # Data models
│ │ │ ├── SwipeMenuItem.ets
│ │ │ ├── SwipeMenu.ets
│ │ │ ├── SwipeMenuCreator.ets
│ │ │ ├── AnimationConfig.ets
│ │ │ └── TouchConfig.ets
│ │ ├── interfaces/ # Interface definitions
│ │ │ ├── OnMenuItemClickListener.ets
│ │ │ ├── OnSwipeListener.ets
│ │ │ ├── OnMenuStateChangeListener.ets
│ │ │ ├── OnAnimationStateChangeListener.ets
│ │ │ └── OnItemLongPressListener.ets
│ │ ├── utils/ # Utility classes
│ │ │ ├── SwipeStateController.ets
│ │ │ ├── AnimationInterpolator.ets
│ │ │ ├── SwipeMenuListUtils.ets
│ │ │ ├── SwipeMenuPreferences.ets
│ │ │ └── Logger.ets
│ │ └── constants/ # Constant definitions
│ │ ├── SwipeDirection.ets
│ │ └── TouchState.ets
│ ├── ohosTest/ # Component tests
│ └── test/ # Unit tests
├── Index.ets # Public exports
├── build-profile.json5 # Build configuration
└── oh-package.json5 # Component library dependency configuration
Contributing
If you encounter any issues while using the component, feel free to submit an Issue. We also welcome you to contribute by submitting a PR.
License
This project is based on the MIT LICENSE. Please feel free to enjoy and participate in open source.