HarmonyOS 5 Building a Draggable Floating View Component in Your Application

Introduction
In HarmonyOS development, implementing a floating draggable UI component (DragView) can greatly improve user experience. This is particularly useful for creating floating entries, quick access widgets, or contextual helpers. This article introduces a reusable DragView component that supports smooth dragging, edge constraint, magnetic snapping, and customizable initial alignment.
Design Concept
HarmonyOS provides built-in gesture support, such as PanGesture
, which makes it straightforward to implement draggable interactions. The idea is simple:
Track drag offset using
PanGesture
.Dynamically update the UI's
.position()
with the calculated offset.After the drag ends, trigger a smooth snapping animation (
animateTo
) to snap the view to the nearest edge or position.
DragView Container
We start by defining the DragView
component using .position(this.curPosition)
to place it on the screen, and accept a builder function dragContentBuilder
to support flexible content injection.
@State private curPosition: Position = { x: 0, y: 0 };
build() {
Stack() {
if (this.dragContentBuilder) {
this.dragContentBuilder()
} else {
this.defDragView()
}
}
.position(this.curPosition)
.onClick(this.onClickListener)
}
Edge Constraints
To ensure the drag stays within a desired area, we define a BoundArea
object to encapsulate the bounding logic.
export class BoundArea {
constructor(start: number, top: number, end: number, bottom: number) {
this.start = start
this.top = top
this.end = end
this.bottom = bottom
this.width = end - start
this.height = bottom - top
this.centerX = this.width / 2 + start
this.centerY = this.height / 2 + top
}
// ... other fields
}
By default, the bounding area is set to the full screen.
Measuring View Dimensions
Since the DragView content is dynamic, we calculate its width and height using .onAreaChange
.
.onAreaChange((oldValue: Area, newValue: Area) => {
if (newValue.width && newValue.height) {
this.dragWidth = newValue.width
this.dragHeight = newValue.height
}
})
Implementing Drag Interaction
We use PanGesture
with PanDirection.All
to allow free dragging in all directions.
PanGesture({ direction: PanDirection.All })
.onActionStart(event => this.changePosition(event.offsetX, event.offsetY))
.onActionUpdate(event => this.changePosition(event.offsetX, event.offsetY))
.onActionEnd(() => this.adsorbToEnd(this.curPosition.x, this.curPosition.y))
The actual dragging logic applies clamping to keep the view within bounds:
private changePosition(offsetX: number, offsetY: number) {
let targetX = clamp(this.endPosition.x + offsetX, this.boundArea.start, this.boundArea.end - this.dragHeight);
let targetY = clamp(this.endPosition.y + offsetY, this.boundArea.top, this.boundArea.bottom - this.dragWidth);
this.curPosition = { x: targetX, y: targetY };
}
Magnetic Snapping Animation
After the drag ends, the component snaps to the closest horizontal edge and clamps vertically within bounds:
private adsorbToEnd(startX: number, startY: number) {
let targetX = (startX <= this.boundArea.centerX)
? this.boundArea.start + this.dragMargin.left
: this.boundArea.end - this.dragWidth - this.dragMargin.right;
let targetY = clamp(
startY,
this.boundArea.top + this.dragMargin.top,
this.boundArea.bottom - this.dragWidth - this.dragMargin.bottom
);
this.startMoveAnimateTo(targetX, targetY);
}
Customizing Initial Alignment
To improve usability, we allow customizing the initial position using dragAlign
and dragMargin
:
dragAlign: Alignment = Alignment.BottomStart;
dragMargin: Margin = {};
The position is calculated once after the component's dimensions are known:
private initAlign() {
switch (this.dragAlign) {
case Alignment.BottomEnd:
this.curPosition = {
x: this.boundArea.end - this.dragWidth - this.dragMargin.right,
y: this.boundArea.bottom - this.dragHeight - this.dragMargin.bottom
};
break;
// ... other alignments
}
this.endPosition = this.curPosition;
}
Example Usage
A simple usage example:
DragView({
dragAlign: Alignment.Center,
dragMargin: bothway(10),
dragContentBuilder: this.defDragView()
})
@Builder
defDragView() {
Stack() {
Text("Drag Me")
.width(50).height(50)
.fontSize(15)
}
.shadow({ radius: 1.5, color: "#80000000", offsetX: 0, offsetY: 1 })
.padding(18)
.borderRadius(30)
.backgroundColor(Color.White)
.animation({ duration: 200, curve: Curve.Smooth })
}
Conclusion
With this DragView component, you can easily implement reusable floating views inside HarmonyOS applications. It supports customizable boundaries, snapping logic, initial placement, and dynamic UI content.
If you're building a system-wide floating assistant or cross-page notice, consider combining this approach with SubWindow
, as discussed in the in-app notice article.
Subscribe to my newsletter
Read articles from HarmonyOS Magician directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
