HarmonyOS鸿蒙Next中Grid和List内拖拽交换子组件位置
HarmonyOS鸿蒙Next中Grid和List内拖拽交换子组件位置
一、项目概述
1.1 功能特性
- 基于HarmonyOS 4.0+ API实现
- Grid和List组件内拖拽排序
- 平滑的交互动画效果
- 支持多种拖拽手势
- 跨组件拖拽支持
- 高性能渲染优化
二、架构设计
2.1 核心组件结构
拖拽排序系统
├── DragManager.ets (拖拽管理器)
├── DraggableGrid.ets (可拖拽网格)
├── DraggableList.ets (可拖拽列表)
├── DragItem.ets (可拖拽项)
└── SortAnimation.ets (排序动画)
2.2 数据模型定义
// DragSortModel.ets
// 可拖拽项数据模型
export interface DraggableItem {
id: string;
title: string;
icon?: Resource;
color: ResourceColor;
order: number; // 排序序号
type?: 'grid' | 'list'; // 所属容器类型
disabled?: boolean; // 是否禁用拖拽
}
// 拖拽配置
export interface DragSortConfig {
animationDuration: number; // 动画时长(ms)
dragScale: number; // 拖拽时缩放比例
dragOpacity: number; // 拖拽时透明度
vibrationEnabled: boolean; // 是否启用震动反馈
crossDragEnabled: boolean; // 是否允许跨容器拖拽
placeholderColor: ResourceColor; // 占位符颜色
autoScrollEnabled: boolean; // 是否启用自动滚动
scrollThreshold: number; // 自动滚动阈值
scrollSpeed: number; // 自动滚动速度
}
// 默认配置
export class DragDefaultConfig {
static readonly DEFAULT_CONFIG: DragSortConfig = {
animationDuration: 300,
dragScale: 1.1,
dragOpacity: 0.8,
vibrationEnabled: true,
crossDragEnabled: false,
placeholderColor: '#F0F0F0',
autoScrollEnabled: true,
scrollThreshold: 50,
scrollSpeed: 20
};
}
这里定义了拖拽排序系统的数据模型和配置。DraggableItem接口定义每个可拖拽项的数据结构,包含ID、标题、图标、颜色、排序序号和是否禁用等属性。DragSortConfig接口定义拖拽行为的配置参数,如动画时长、拖拽缩放比例、震动反馈等。DragDefaultConfig提供默认配置值,方便快速使用。
三、核心实现
3.1 拖拽管理器
// DragManager.ets
export class DragManager {
private static instance: DragManager;
private draggingItem: DraggableItem | null = null;
private dragStartPosition: { x: number; y: number } = { x: 0, y: 0 };
private currentIndex: number = -1;
private targetIndex: number = -1;
private containerType: 'grid' | 'list' | null = null;
private listeners: Map<string, (event: DragEvent) => void> = new Map();
private vibration: vibrator.Vibrator | null = null;
// 单例模式
static getInstance(): DragManager {
if (!DragManager.instance) {
DragManager.instance = new DragManager();
}
return DragManager.instance;
}
DragManager是拖拽系统的核心管理器,采用单例模式确保全局唯一实例。它维护当前拖拽状态,包括拖拽中的项、起始位置、当前索引、目标索引、容器类型等。listeners用于存储事件监听器,vibration用于触觉反馈。
// 开始拖拽
startDrag(item: DraggableItem, index: number, startX: number, startY: number, type: 'grid' | 'list'): void {
this.draggingItem = item;
this.currentIndex = index;
this.targetIndex = index;
this.dragStartPosition = { x: startX, y: startY };
this.containerType = type;
// 震动反馈
if (this.vibrationEnabled()) {
this.vibrate(10);
}
this.notifyListeners('dragStart', {
item,
index,
type
});
}
// 更新拖拽位置
updateDragPosition(x: number, y: number, hoverIndex: number = -1): void {
if (!this.draggingItem) return;
this.targetIndex = hoverIndex;
this.notifyListeners('dragMove', {
x,
y,
hoverIndex,
item: this.draggingItem
});
}
startDrag方法开始拖拽操作,记录拖拽项、起始位置和容器类型,并触发震动反馈。updateDragPosition方法更新拖拽位置和目标索引,通知所有监听器拖拽移动事件。
// 结束拖拽
endDrag(): DraggableItem | null {
if (!this.draggingItem) return null;
const droppedItem = this.draggingItem;
const fromIndex = this.currentIndex;
const toIndex = this.targetIndex;
this.notifyListeners('dragEnd', {
item: droppedItem,
fromIndex,
toIndex,
type: this.containerType
});
// 重置状态
this.reset();
return droppedItem;
}
// 取消拖拽
cancelDrag(): void {
this.notifyListeners('dragCancel', {
item: this.draggingItem,
index: this.currentIndex
});
this.reset();
}
private reset(): void {
this.draggingItem = null;
this.currentIndex = -1;
this.targetIndex = -1;
this.dragStartPosition = { x: 0, y: 0 };
this.containerType = null;
}
endDrag方法结束拖拽操作,返回拖拽的项,通知监听器拖拽结束事件,然后重置拖拽状态。cancelDrag方法取消拖拽操作,通知监听器拖拽取消事件。reset方法清理所有拖拽状态。
// 震动反馈
private vibrate(duration: number): void {
try {
this.vibration = vibrator.createVibrator();
this.vibration.vibrate(duration);
} catch (error) {
console.warn('Vibration not available:', error);
}
}
private vibrationEnabled(): boolean {
// 从配置中获取震动设置
return true;
}
// 事件监听
addEventListener(event: string, callback: (event: DragEvent) => void): void {
this.listeners.set(event, callback);
}
removeEventListener(event: string): void {
this.listeners.delete(event);
}
private notifyListeners(eventType: string, data: any): void {
const callback = this.listeners.get(eventType);
if (callback) {
callback({ type: eventType, data });
}
}
// 获取当前拖拽状态
getDragState(): {
isDragging: boolean;
draggingItem: DraggableItem | null;
fromIndex: number;
toIndex: number;
} {
return {
isDragging: this.draggingItem !== null,
draggingItem: this.draggingItem,
fromIndex: this.currentIndex,
toIndex: this.targetIndex
};
}
}
vibrate方法提供震动触觉反馈,增强拖拽交互体验。addEventListener和removeEventListener管理事件监听。getDragState方法返回当前拖拽状态,供其他组件查询。notifyListeners方法通知所有注册的监听器特定事件。
3.2 可拖拽列表项
// DragItem.ets
@Component
export struct DragItem {
[@Prop](/user/Prop) item: DraggableItem;
[@Prop](/user/Prop) index: number = 0;
[@Prop](/user/Prop) containerType: 'grid' | 'list' = 'list';
[@Prop](/user/Prop) config: DragSortConfig = DragDefaultConfig.DEFAULT_CONFIG;
[@State](/user/State) private isDragging: boolean = false;
[@State](/user/State) private scale: number = 1;
[@State](/user/State) private opacity: number = 1;
[@State](/user/State) private translateX: number = 0;
[@State](/user/State) private translateY: number = 0;
[@State](/user/State) private zIndex: number = 0;
private dragManager: DragManager = DragManager.getInstance();
private longPressTimer: number = 0;
private isLongPress: boolean = false;
private startX: number = 0;
private startY: number = 0;
DragItem组件是可拖拽的基础项,包含拖拽所需的所有状态:是否正在拖拽、缩放比例、透明度、平移位置和Z轴层级。dragManager是拖拽管理器实例,longPressTimer用于长按检测,isLongPress标记长按状态,startX/Y记录触摸起始位置。
aboutToAppear() {
// 监听拖拽事件
this.dragManager.addEventListener('dragStart', (event) => {
this.handleDragEvent(event);
});
this.dragManager.addEventListener('dragMove', (event) => {
this.handleDragEvent(event);
});
this.dragManager.addEventListener('dragEnd', (event) => {
this.handleDragEvent(event);
});
}
private handleDragEvent(event: DragEvent): void {
switch (event.type) {
case 'dragStart':
this.onDragStart(event.data);
break;
case 'dragMove':
this.onDragMove(event.data);
break;
case 'dragEnd':
this.onDragEnd(event.data);
break;
}
}
在aboutToAppear生命周期中注册拖拽事件监听器,当拖拽管理器触发事件时调用对应的处理方法。handleDragEvent方法根据事件类型分发到不同的处理函数。
// 长按开始拖拽
private onLongPress(event: GestureEvent): void {
if (this.item.disabled) return;
this.isLongPress = true;
this.isDragging = true;
this.scale = this.config.dragScale;
this.opacity = this.config.dragOpacity;
this.zIndex = 1000; // 确保在最上层
// 开始拖拽
this.dragManager.startDrag(
this.item,
this.index,
this.startX,
this.startY,
this.containerType
);
}
// 触摸开始
private onTouchStart(event: TouchEvent): void {
if (this.item.disabled) return;
const touch = event.touches[0];
this.startX = touch.x;
this.startY = touch.y;
// 开始长按计时
this.longPressTimer = setTimeout(() => {
this.onLongPress({} as GestureEvent);
}, 500) as unknown as number;
}
onLongPress方法处理长按手势,设置拖拽状态(缩放、透明度、Z轴层级),然后通知拖拽管理器开始拖拽。onTouchStart方法记录触摸起始位置,并启动500ms定时器检测长按。
// 触摸移动
private onTouchMove(event: TouchEvent): void {
if (!this.isDragging || !this.isLongPress) return;
const touch = event.touches[0];
const deltaX = touch.x - this.startX;
const deltaY = touch.y - this.startY;
// 更新位置
this.translateX = deltaX;
this.translateY = deltaY;
// 通知拖拽管理器
this.dragManager.updateDragPosition(touch.x, touch.y, this.index);
}
// 触摸结束
private onTouchEnd(): void {
// 清除长按定时器
if (this.longPressTimer) {
clearTimeout(this.longPressTimer);
this.longPressTimer = 0;
}
if (this.isDragging && this.isLongPress) {
// 结束拖拽
this.dragManager.endDrag();
this.resetState();
}
this.isLongPress = false;
}
onTouchMove方法在拖拽过程中更新项的位置,计算相对于起始位置的偏移量,并通知拖拽管理器当前拖拽位置。onTouchEnd方法清理长按定时器,如果正在拖拽则结束拖拽,然后重置组件状态。
private resetState(): void {
this.isDragging = false;
animateTo({
duration: this.config.animationDuration,
curve: animation.Curve.EaseOut
}, () => {
this.scale = 1;
this.opacity = 1;
this.translateX = 0;
this.translateY = 0;
this.zIndex = 0;
});
}
// 拖拽事件处理
private onDragStart(data: any): void {
if (data.item.id === this.item.id) {
// 自己开始拖拽,已处理
return;
}
// 其他项开始拖拽,降低透明度
if (this.containerType === data.type) {
this.opacity = 0.6;
}
}
private onDragMove(data: any): void {
if (data.hoverIndex === this.index && this.containerType === data.item.type) {
// 拖拽到当前项上方
this.scale = 0.95;
} else {
this.scale = 1;
}
}
private onDragEnd(data: any): void {
// 重置所有状态
animateTo({
duration: this.config.animationDuration,
curve: animation.Curve.EaseOut
}, () => {
this.scale = 1;
this.opacity = 1;
});
}
resetState方法重置组件状态,使用animateTo实现平滑的动画恢复。onDragStart、onDragMove、onDragEnd方法处理拖拽管理器发出的事件:当其他项开始拖拽时降低透明度;当拖拽到当前项上方时轻微缩放;拖拽结束时恢复所有状态。
build() {
Column()
.width(this.containerType === 'grid' ? '100%' : '100%')
.height(this.containerType === 'grid' ? 100 : 60)
.backgroundColor(this.item.color)
.borderRadius(8)
.scale({ x: this.scale, y: this.scale })
.opacity(this.opacity)
.translate({ x: this.translateX, y: this.translateY })
.zIndex(this.zIndex)
.shadow({ radius: this.isDragging ? 10 : 0, color: Color.Gray })
.animation({
duration: this.config.animationDuration,
curve: animation.Curve.EaseInOut
})
.gesture(
// 长按手势
LongPressGesture({ repeat: false })
.onAction(() => {
if (!this.item.disabled) {
this.onLongPress({} as GestureEvent);
}
})
)
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Down) {
this.onTouchStart(event);
} else if (event.type === TouchType.Move) {
this.onTouchMove(event);
} else if (event.type === TouchType.Up) {
this.onTouchEnd();
}
})
{
// 内容
Row({ space: 8 }) {
if (this.item.icon) {
Image(this.item.icon)
.width(24)
.height(24)
}
Text(this.item.title)
.fontSize(16)
.fontColor(Color.White)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.padding(12)
.justifyContent(FlexAlign.Center)
}
}
}
build方法构建可拖拽项的外观和交互。应用缩放、透明度、平移、Z轴层级和阴影等视觉效果。通过gesture添加长按手势识别,onTouch处理触摸事件实现拖拽。内部使用Row布局显示图标和标题。
3.3 可拖拽网格组件
// DraggableGrid.ets
@Component
export struct DraggableGrid {
// 数据源
[@State](/user/State) items: DraggableItem[] = [];
// 配置
[@Prop](/user/Prop) config: DragSortConfig = DragDefaultConfig.DEFAULT_CONFIG;
[@Prop](/user/Prop) columns: number = 3; // 网格列数
[@Prop](/user/Prop) columnGap: Length = 8; // 列间距
[@Prop](/user/Prop) rowGap: Length = 8; // 行间距
// 事件回调
[@Prop](/user/Prop) onOrderChange?: (items: DraggableItem[]) => void;
[@Prop](/user/Prop) onDragStart?: (item: DraggableItem, index: number) => void;
[@Prop](/user/Prop) onDragEnd?: (fromIndex: number, toIndex: number) => void;
[@State](/user/State) private placeholderIndex: number = -1;
[@State](/user/State) private isDragging: boolean = false;
[@State](/user/State) private draggingIndex: number = -1;
[@State](/user/State) private autoScrollDirection: 'up' | 'down' | null = null;
private dragManager: DragManager = DragManager.getInstance();
private gridRef: Grid | null = null;
private autoScrollTimer: number = 0;
private scrollContainer: Scroller | null = null;
aboutToAppear() {
this.setupEventListeners();
}
DraggableGrid组件实现可拖拽排序的网格布局。@State装饰器管理内部状态,@Prop接收配置和回调。placeholderIndex标记拖拽占位符位置,isDragging标记拖拽状态,draggingIndex记录正在拖拽的项索引,autoScrollDirection控制自动滚动方向。
private setupEventListeners(): void {
this.dragManager.addEventListener('dragStart', (event) => {
this.handleDragStart(event.data);
});
this.dragManager.addEventListener('dragMove', (event) => {
this.handleDragMove(event.data);
});
this.dragManager.addEventListener('dragEnd', (event) => {
this.handleDragEnd(event.data);
});
this.dragManager.addEventListener('dragCancel', () => {
this.handleDragCancel();
更多关于HarmonyOS鸿蒙Next中Grid和List内拖拽交换子组件位置的实战教程也可以访问 https://www.itying.com/category-93-b0.html
了解学习下
在HarmonyOS Next中,Grid和List组件支持通过onItemDragStart、onItemDragEnter、onItemDragMove和onItemDrop等事件回调实现子组件位置的拖拽交换。开发者需要为可拖拽组件设置draggable(true),并在事件中处理拖拽数据交换与UI更新。
这是一个非常详尽的HarmonyOS Next拖拽排序实现方案,展示了良好的架构设计和工程化思维。从专业角度看,您的方案有以下几个亮点:
- 架构清晰:采用管理器模式(DragManager)集中管理拖拽状态,组件间通过事件通信,耦合度低。
- 性能考虑:在DraggableList中缓存位置偏移(listOffsets),避免拖拽过程中的重复计算。
- 体验完整:涵盖了视觉反馈(缩放、阴影)、触觉反馈(震动)、边界处理(自动滚动)等细节。
针对HarmonyOS Next的特性,有几点可以进一步优化:
1. 使用ArkTS的增强特性
// 可以利用ArkTS的@Track装饰器优化局部更新
@Track private dragOffset: { x: number, y: number } = { x: 0, y: 0 };
// 使用@Watch监听状态变化
@Watch('placeholderIndex')
onPlaceholderChange() {
this.updateAnimation();
}
2. 手势识别优化 您的方案结合了长按手势和触摸事件,在HarmonyOS Next中可以考虑:
- 使用
PanGesture替代部分手动触摸跟踪 - 利用
PinchGesture实现多指操作支持 - 通过
GestureGroup组合手势优先级
3. 渲染性能 对于大数据量场景:
// 使用LazyForEach替代ForEach
LazyForEach(this.dataSource, (item: DraggableItem) => {
// 仅渲染可见项
}, (item: DraggableItem) => item.id)
4. 跨组件拖拽的改进 在CrossDragManager中,可以考虑使用:
@CustomDialog创建全局拖拽预览window.getWindowRect()获取精确的屏幕坐标- 使用
Matrix4进行更复杂的变换动画
5. 无障碍支持增强 除了基本的accessibility属性,还可以:
// 动态更新无障碍提示
accessibilityDescription(this.getDragDescription())
private getDragDescription(): string {
if (this.isDragging) {
return `正在拖拽${this.item.title},当前位于第${this.placeholderIndex + 1}位`;
}
return `可拖拽项${this.item.title},当前位于第${this.index + 1}位`;
}
6. 状态持久化 考虑拖拽过程中的异常恢复:
// 使用PersistentStorage保存临时状态
PersistentStorage.PersistProp('dragState', this.dragState);
// 应用恢复时检查
aboutToAppear() {
const savedState = PersistentStorage.Get('dragState');
if (savedState) {
this.restoreDragState(savedState);
}
}
您的实现方案已经相当完善,上述建议主要是针对HarmonyOS Next的新特性和生产环境中的进一步优化。特别是在手势处理、性能优化和无障碍支持方面,HarmonyOS Next提供了更多原生能力可以利用。

