DEV Community

HarmonyOS
HarmonyOS

Posted on

Long Press & Drag-to-Reorder in ArkUI: Dynamic List Manipulation

Read the original article:Long Press & Drag-to-Reorder in ArkUI: Dynamic List Manipulation

Cover Image(ChatGPT Generated)

Introduction

User-friendly interfaces often depend on the little details like how easy it is to move items in a list. In HarmonyOS NEXT apps, enabling drag-to-reorder interactions can enhance usability. Think of playlist editors, task organizers, or customized settings lists.

ArkUI, the declarative UI framework for HarmonyOS, provides everything needed to implement this interaction cleanly: gesture sequencing, visual feedback, and efficient rendering. In this article, we’ll explore how to create a smooth long-press and drag-to-reorder experience using ArkUI and ArkTS.

We use several functions like;

  • LongPressGesture to activate drag mode
  • PanGesture to track movement
  • GestureGroup for sequencing gestures
  • animateTo() for smooth visual transitions

The combination offers high responsiveness with minimal performance cost.

import curves from '@ohos.curves';
import Curves from '@ohos.curves'
@Entry
@Component
struct SwitchListItemExample {
  @State private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
  @State dragItem: number = -1
  @State scaleItem: number = -1
  @State neighborItem: number = -1
  @State neighborScale: number = -1
  private dragRefOffset: number = 0
  @State offsetX: number = 0
  @State offsetY: number = 0
  private ITEM_INTV: number = 120
  scaleSelect(item: number): number {
    if (this.scaleItem == item) {
      return 1.05
    } else if (this.neighborItem == item) {
      return this.neighborScale
    } else {
      return 1
    }
  }
  itemMove(index: number, newIndex: number): void {
    let tmp = this.arr.splice(index, 1)
    this.arr.splice(newIndex, 0, tmp[0])
  }
  build() {
    Stack() {
      List({ space: 20, initialIndex: 0 }) {
        ForEach(this.arr, (item: number) => {
          ListItem() {
            Text('' + item)
              .width('100%')
              .height(100)
              .fontSize(16)
              .textAlign(TextAlign.Center)
              .borderRadius(10)
              .backgroundColor(0xFFFFFF)
              .shadow(this.scaleItem == item ? { radius: 70, color: '#15000000', offsetX: 0, offsetY: 0 } :
                { radius: 0, color: '#15000000', offsetX: 0, offsetY: 0 })
              .animation({ curve: Curve.Sharp, duration: 300 })
          }
          .margin({ left: 12, right: 12 })
          .scale({ x: this.scaleSelect(item), y: this.scaleSelect(item) })
          .zIndex(this.dragItem == item ? 1 : 0)
          .translate(this.dragItem == item ? { y: this.offsetY } : { y: 0 })



          .gesture(

            GestureGroup(GestureMode.Sequence,
              LongPressGesture({ repeat: true })
                .onAction((event?: GestureEvent) => {
                  animateTo({ curve: Curve.Friction, duration: 300 }, () => {
                    this.scaleItem = item
                  })
                })
                .onActionEnd(() => {
                  animateTo({ curve: Curve.Friction, duration: 300 }, () => {
                    this.scaleItem = -1
                  })
                }),
              PanGesture({ fingers: 1, direction: null, distance: 0 })
                .onActionStart(() => {
                  this.dragItem = item
                  this.dragRefOffset = 0
                })
                .onActionUpdate((event: GestureEvent) => {
                  this.offsetY = event.offsetY - this.dragRefOffset
                  this.neighborItem = -1
                  let index = this.arr.indexOf(item)
                  let curveValue = Curves.initCurve(Curve.Sharp)
                  let value: number = 0
                  //Calculate the scaling of adjacent items based on displacement
                  if (this.offsetY < 0) {
                    value = curveValue.interpolate(-this.offsetY / this.ITEM_INTV)
                    this.neighborItem = this.arr[index-1]
                    this.neighborScale = 1 - value / 20;
                    console.log('neighborScale:' + this.neighborScale.toString())
                  } else if (this.offsetY > 0) {
                    value = curveValue.interpolate(this.offsetY / this.ITEM_INTV)
                    this.neighborItem = this.arr[index+1]
                    this.neighborScale = 1 - value / 20;
                  }



                  // Sort by displacement swap
                  if (this.offsetY > this.ITEM_INTV / 2) {
                    animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
                      this.offsetY -= this.ITEM_INTV
                      this.dragRefOffset += this.ITEM_INTV
                      this.itemMove(index, index + 1)
                    })
                  } else if (this.offsetY < -this.ITEM_INTV / 2) {
                    animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
                      this.offsetY += this.ITEM_INTV
                      this.dragRefOffset -= this.ITEM_INTV
                      this.itemMove(index, index - 1)
                    })
                  }
                })
                .onActionEnd((event: GestureEvent) => {
                  animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
                    this.dragItem = -1
                    this.neighborItem = -1
                  })
                  animateTo({
                    curve: curves.interpolatingSpring(14, 1, 170, 17), delay: 150
                  }, () => {
                    this.scaleItem = -1
                  })
                })
            )
              .onCancel(() => {
                animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
                  this.dragItem = -1
                  this.neighborItem = -1
                })
                animateTo({
                  curve: curves.interpolatingSpring(14, 1, 170, 17), delay: 150
                }, () => {
                  this.scaleItem = -1
                })
              })
          )
        }, (item: number) => item.toString())
      }
    }.width('100%').height('100%').backgroundColor(0xDCDCDC).padding({ top: 5 })
  }
}
Enter fullscreen mode Exit fullscreen mode

LongPressGesture and PanGesture

LongPressGesture()
  .onAction(() => animateTo({ curve: Curve.Friction }, 0 => {
    this.scaleItem = item;
  }))
Enter fullscreen mode Exit fullscreen mode

When long-pressing:

The animation starts.

The element is activated by slightly enlarging it with Curve. Friction (e.g., scale = 1.05).

PanGesture()
  .onActionStart(() => {
    this.dragItem = item;
    this.dragRefOffset = 0;
  })
Enter fullscreen mode Exit fullscreen mode

When dragging begins:

The dragged item (dragItem) is recorded.

The drag offset is reset (dragRefOffset = 0).

Position Swapping Logic

if (Math.abs(this.offsetY) > this.ITEM_INTV / 2) {
  animateTo({ curve: curves.interpolatingSpring(400, 38) }, 0 => {
    this.offsetY = Math.sign(this.offsetY) * this.ITEM_INTV;
    this.dragRefOffset += Math.sign(this.offsetY) * this.ITEM_INTV;
    this.itemMove(index, index + Math.sign(this.offsetY));
  });
}
Enter fullscreen mode Exit fullscreen mode

If the drag distance (offsetY) is greater than half an element’s height (e.g., more than half an element has been dragged):

Displacement is initiated.

The animateTo function is called with an interpolating spring animation:

curves.interpolatingSpring(400, 38)
Enter fullscreen mode Exit fullscreen mode

Here:

-400 = spring stiffness

38 = damping

offsetY is updated by an entire item height in that swap direction:

offsetY =± 𝐼𝑇𝐸𝑀_𝐼𝑁𝑇𝑉

dragRefOffset is updated in the same way.

The itemMove function is called:

itemMove(currentIndex, newIndex)

This function swaps the locations of items in the data model.

👉 Mathematical meaning:

If:

∣offsetY∣ > ITEM_INTV / 2

then, the items are swapped.

Math.sign(this.offsetY) determines the swap direction: up or down.

Why use interpolating springs for swapping?

Makes the UI feel responsive and natural
Prevents jarring “jumping” of items
Gives visual feedback of the new position
Mimics real-world physics, improving perceived quality

Output:

Draggable List Items

Core Functionality

  • Long-pressing a list item to enter drag mode
  • Dragging the item vertically with dynamic scaling and shadow feedback
  • Automatic position swapping based on drag distance
  • Smooth release animation and state reset

Conclusion

Implementing intuitive, animated list interactions in HarmonyOS NEXT is no longer a complex task. With ArkUI’s gesture system and animation tools, you can create highly responsive and visually dynamic list behaviors like drag-to-reorder with minimal code and maximum performance. By combining long press and pan gestures through GestureGroup, then wrapping UI changes with animateTo(), developers can deliver fluid, delightful UX for everyday use cases. This approach balances performance, code simplicity, and user satisfaction—making it a powerful pattern to adopt in any ArkTS-based application.

References

Written by Simay Ayberik

Top comments (0)