Giới Thiệu
Giao diện thân thiện với người dùng thường phụ thuộc vào những chi tiết nhỏ như cách dễ dàng để di chuyển các mục trong danh sách. Trong các ứng dụng HarmonyOS NEXT, việc kích hoạt các tương tác kéo và thả để sắp xếp có thể nâng cao tính khả dụng. Hãy nghĩ đến các trình chỉnh sửa danh sách phát, tổ chức nhiệm vụ hoặc danh sách cài đặt tùy chỉnh.
ArkUI, framework UI khai báo cho HarmonyOS, cung cấp mọi thứ cần thiết để thực hiện tương tác này một cách rõ ràng: chuỗi cử chỉ, phản hồi hình ảnh và hiệu suất rendering hiệu quả. Trong bài viết này, chúng ta sẽ khám phá cách tạo trải nghiệm kéo và thả mượt mà bằng cách sử dụng ArkUI và ArkTS.
Chúng ta sử dụng một số hàm như:
LongPressGestuređể kích hoạt chế độ kéoPanGestuređể theo dõi chuyển độngGestureGroupđể sắp xếp các cử chỉanimateTo()cho các chuyển tiếp hình ảnh mượt mà
Sự kết hợp này mang lại khả năng phản hồi cao với chi phí hiệu suất tối thiểu.
Cấu Trúc Mã Ví Dụ
typescript
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;
// Tính toán tỷ lệ của các mục liền kề dựa trên độ dịch chuyển
if (this.offsetY < 0) {
value = curveValue.interpolate(-this.offsetY / this.ITEM_INTV);
this.neighborItem = this.arr[index-1];
this.neighborScale = 1 - value / 20;
} else if (this.offsetY > 0) {
value = curveValue.interpolate(this.offsetY / this.ITEM_INTV);
this.neighborItem = this.arr[index+1];
this.neighborScale = 1 - value / 20;
}
// Sắp xếp theo độ dịch chuyển
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 });
}
}
Cử Chỉ Kéo Dài và Kéo
typescript
LongPressGesture()
.onAction(() => animateTo({ curve: Curve.Friction }, 0 => {
this.scaleItem = item;
}));
Khi nhấn lâu:
- Animation bắt đầu.
- Phần tử được kích hoạt bằng cách phóng to nhẹ với Curve.Friction (ví dụ, scale = 1.05).
typescript
PanGesture()
.onActionStart(() => {
this.dragItem = item;
this.dragRefOffset = 0;
});
Khi kéo bắt đầu:
- Mục bị kéo (dragItem) được ghi lại.
- Độ dịch chuyển được đặt lại (dragRefOffset = 0).
Logic Hoán Đổi Vị Trí
typescript
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));
});
}
Nếu khoảng cách kéo (offsetY) lớn hơn một nửa chiều cao của một phần tử (ví dụ, đã kéo hơn một nửa phần tử):
- Hoán đổi bắt đầu.
- Hàm animateTo được gọi với animation lò xo nội suy:
typescript
curves.interpolatingSpring(400, 38)
Ở đây:
- 400 = độ cứng của lò xo
- 38 = độ giảm
- offsetY được cập nhật bằng chiều cao của một mục trong hướng hoán đổi đó:
- offsetY = ± ITEM_INTV
- dragRefOffset cũng được cập nhật tương tự.
- Hàm itemMove được gọi:
- itemMove(currentIndex, newIndex)
- Hàm này hoán đổi vị trí của các mục trong mô hình dữ liệu.
👉 Ý nghĩa toán học:
- Nếu |offsetY| > ITEM_INTV / 2 thì các mục sẽ được hoán đổi.
- Math.sign(this.offsetY) xác định hướng hoán đổi: lên hoặc xuống.
Tại Sao Sử Dụng Lò Xo Nội Suy Để Hoán Đổi?
- Giúp UI cảm thấy phản hồi và tự nhiên.
- Ngăn chặn việc “nhảy” khó chịu của các mục.
- Cung cấp phản hồi hình ảnh về vị trí mới.
- Giả lập vật lý thực tế, cải thiện chất lượng cảm nhận.
Chức Năng Chính
- Nhấn lâu vào một mục danh sách để vào chế độ kéo.
- Kéo mục theo chiều dọc với tỷ lệ động và phản hồi bóng.
- Hoán đổi vị trí tự động dựa trên khoảng cách kéo.
- Animation thả mượt và đặt lại trạng thái.
Kết Luận
Việc triển khai các tương tác danh sách trực quan, có animation trong HarmonyOS NEXT không còn là một nhiệm vụ phức tạp. Với hệ thống cử chỉ và công cụ animation của ArkUI, bạn có thể tạo ra những hành vi danh sách phản hồi cao và động lực thị giác như kéo để sắp xếp với mã tối thiểu và hiệu suất tối đa. Bằng cách kết hợp cử chỉ nhấn lâu và kéo qua GestureGroup, sau đó bao bọc các thay đổi UI với animateTo(), các nhà phát triển có thể mang lại trải nghiệm người dùng mượt mà và thú vị cho các trường hợp sử dụng hàng ngày. Cách tiếp cận này cân bằng hiệu suất, sự đơn giản của mã và sự hài lòng của người dùng, biến nó thành một mẫu mạnh mẽ để áp dụng trong bất kỳ ứng dụng nào dựa trên ArkTS.
Tài Liệu Tham Khảo
- Sự kiện kéo
- Liên kết cử chỉ