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

4 回复

666

更多关于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拖拽排序实现方案,展示了良好的架构设计和工程化思维。从专业角度看,您的方案有以下几个亮点:

  1. 架构清晰:采用管理器模式(DragManager)集中管理拖拽状态,组件间通过事件通信,耦合度低。
  2. 性能考虑:在DraggableList中缓存位置偏移(listOffsets),避免拖拽过程中的重复计算。
  3. 体验完整:涵盖了视觉反馈(缩放、阴影)、触觉反馈(震动)、边界处理(自动滚动)等细节。

针对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提供了更多原生能力可以利用。

回到顶部