Read the original article:Long Press & Drag-to-Reorder in ArkUI: Dynamic List Manipulation
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;
-
LongPressGestureto activate drag mode -
PanGestureto track movement -
GestureGroupfor 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 })
}
}
LongPressGesture and PanGesture
LongPressGesture()
.onAction(() => animateTo({ curve: Curve.Friction }, 0 => {
this.scaleItem = item;
}))
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;
})
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));
});
}
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)
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:
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.


Top comments (0)