0
0
Lập trình
Admin Team
Admin Teamtechmely

Kéo và Thả Để Sắp Xếp Danh Sách Trong ArkUI

Đăng vào 7 tháng trước

• 12 phút đọc

Chủ đề:

KungFuTech

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éo
  • PanGesture để theo dõi chuyển động
  • GestureGroup để 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 Copy
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 Copy
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 Copy
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 Copy
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 Copy
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ỉ

Tác giả: Simay Ayberik

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào