Tạo khung hiển thị tuỳ chỉnh có tính tương tác

Thử cách sử dụng Compose
Jetpack Compose là bộ công cụ giao diện người dùng được đề xuất cho Android. Tìm hiểu cách sử dụng bố cục trong ứng dụng Compose.

Việc vẽ giao diện người dùng chỉ là một phần trong quá trình tạo khung hiển thị tuỳ chỉnh. Bạn cũng cần phải làm cho khung hiển thị phản hồi hoạt động đầu vào của người dùng theo cách gần giống với hành động trong thế giới thực mà bạn đang bắt chước.

Làm cho các đối tượng trong ứng dụng hoạt động giống như đối tượng thực. Ví dụ: đừng để hình ảnh trong ứng dụng của bạn biến mất và xuất hiện lại ở nơi khác, vì các đối tượng trong thế giới thực không làm được như vậy. Thay vào đó, hãy di chuyển hình ảnh từ nơi này sang nơi khác.

Người dùng thậm chí cảm nhận được hành vi hoặc cảm nhận tinh tế trên một giao diện và phản ứng tốt nhất với những chi tiết nhỏ mô phỏng thế giới thực. Ví dụ: khi người dùng hất một đối tượng giao diện người dùng, cho họ cảm giác quán tính ngay từ đầu giúp trì hoãn chuyển động. Ở cuối chuyển động, hãy cho chúng cảm giác động lượng đưa đối tượng vượt ra ngoài hành động hất.

Trang này minh hoạ cách sử dụng các tính năng của khung Android để thêm các hành vi thực tế này vào khung hiển thị tuỳ chỉnh.

Bạn có thể tìm thêm thông tin liên quan trong bài viết Tổng quan về sự kiện đầu vàoTổng quan về ảnh động thuộc tính.

Xử lý cử chỉ nhập

Giống như nhiều khung giao diện người dùng khác, Android hỗ trợ mô hình sự kiện đầu vào. Hành động của người dùng biến thành các sự kiện kích hoạt lệnh gọi lại và bạn có thể ghi đè lệnh gọi lại để tuỳ chỉnh cách ứng dụng phản hồi người dùng. Sự kiện đầu vào phổ biến nhất trong hệ thống Android là thao tác chạm, sự kiện này sẽ kích hoạt onTouchEvent(android.view.MotionEvent). Ghi đè phương thức này để xử lý sự kiện như sau:

Kotlin

override fun onTouchEvent(event: MotionEvent): Boolean {
    return super.onTouchEvent(event)
}

Java

@Override
   public boolean onTouchEvent(MotionEvent event) {
    return super.onTouchEvent(event);
   }

Các sự kiện chạm không đặc biệt hữu ích. Giao diện người dùng cảm ứng hiện đại xác định hoạt động tương tác về các cử chỉ như nhấn, kéo, đẩy, hất và thu phóng. Android cung cấp GestureDetector để chuyển đổi các sự kiện chạm thô thành cử chỉ.

Tạo GestureDetector bằng cách truyền vào một thực thể của lớp giúp triển khai GestureDetector.OnGestureListener. Nếu chỉ muốn xử lý một vài cử chỉ, bạn có thể mở rộng GestureDetector.SimpleOnGestureListener thay vì triển khai giao diện GestureDetector.OnGestureListener. Ví dụ: mã này tạo một lớp mở rộng GestureDetector.SimpleOnGestureListener và ghi đè onDown(MotionEvent).

Kotlin

private val myListener =  object : GestureDetector.SimpleOnGestureListener() {
    override fun onDown(e: MotionEvent): Boolean {
        return true
    }
}

private val detector: GestureDetector = GestureDetector(context, myListener)

Java

class MyListener extends GestureDetector.SimpleOnGestureListener {
   @Override
   public boolean onDown(MotionEvent e) {
       return true;
   }
}
detector = new GestureDetector(getContext(), new MyListener());

Dù bạn có sử dụng GestureDetector.SimpleOnGestureListener hay không, hãy luôn triển khai phương thức onDown() trả về true. Điều này là cần thiết vì tất cả các cử chỉ bắt đầu bằng thông báo onDown(). Nếu bạn trả về false từ onDown(), như GestureDetector.SimpleOnGestureListener, hệ thống sẽ giả định rằng bạn muốn bỏ qua phần còn lại của cử chỉ và các phương thức khác của GestureDetector.OnGestureListener sẽ không được gọi. Chỉ trả về false từ onDown() nếu bạn muốn bỏ qua toàn bộ cử chỉ.

Sau khi triển khai GestureDetector.OnGestureListener và tạo một thực thể của GestureDetector, bạn có thể sử dụng GestureDetector để diễn giải các sự kiện chạm nhận được trong onTouchEvent().

Kotlin

override fun onTouchEvent(event: MotionEvent): Boolean {
    return detector.onTouchEvent(event).let { result ->
        if (!result) {
            if (event.action == MotionEvent.ACTION_UP) {
                stopScrolling()
                true
            } else false
        } else true
    }
}

Java

@Override
public boolean onTouchEvent(MotionEvent event) {
   boolean result = detector.onTouchEvent(event);
   if (!result) {
       if (event.getAction() == MotionEvent.ACTION_UP) {
           stopScrolling();
           result = true;
       }
   }
   return result;
}

Khi bạn truyền onTouchEvent() một sự kiện chạm mà sự kiện này không nhận ra là một phần của cử chỉ, sự kiện này sẽ trả về false. Sau đó, bạn có thể chạy mã phát hiện cử chỉ tuỳ chỉnh của riêng mình.

Tạo chuyển động vật lý hợp lý

Cử chỉ là một cách hiệu quả để điều khiển các thiết bị màn hình cảm ứng, nhưng các cử chỉ này có thể khác thường và khó nhớ trừ phi tạo ra kết quả hợp lý về mặt thực tế.

Ví dụ: giả sử bạn muốn triển khai cử chỉ hất theo chiều ngang để đặt mục được vẽ trong thành phần hiển thị xoay quanh trục tung. Cử chỉ này sẽ hợp lý nếu giao diện người dùng phản hồi bằng cách di chuyển nhanh theo hướng hất, sau đó chậm lại, như thể người dùng đẩy bánh đà và làm cho nó quay.

Tài liệu về cách tạo ảnh động cho cử chỉ cuộn sẽ giải thích chi tiết về cách triển khai hành vi scoll của riêng bạn. Nhưng việc mô phỏng cảm giác của bánh đà không phải là chuyện đơn giản. Rất nhiều yếu tố vật lý và toán học được yêu cầu để giúp mô hình bánh đà hoạt động chính xác. May mắn là Android cung cấp các lớp trợ giúp để mô phỏng hành vi này và các hành vi khác. Lớp Scroller là cơ sở để xử lý các cử chỉ hất theo kiểu bánh đà.

Để bắt đầu cử chỉ hất, hãy gọi fling() với tốc độ bắt đầu, các giá trị xy tối thiểu và tối đa của cử chỉ hất. Đối với giá trị vận tốc, bạn có thể sử dụng giá trị được tính toán bằng GestureDetector.

Kotlin

fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
    scroller.fling(
            currentX,
            currentY,
            (velocityX / SCALE).toInt(),
            (velocityY / SCALE).toInt(),
            minX,
            minY,
            maxX,
            maxY
    )
    postInvalidate()
    return true
}

Java

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
   scroller.fling(currentX, currentY, velocityX / SCALE, velocityY / SCALE, minX, minY, maxX, maxY);
   postInvalidate();
    return true;
}

Lệnh gọi đến fling() sẽ thiết lập mô hình vật lý cho cử chỉ hất. Sau đó, hãy cập nhật Scroller bằng cách gọi Scroller.computeScrollOffset() theo định kỳ. computeScrollOffset() cập nhật trạng thái bên trong của đối tượng Scroller bằng cách đọc thời gian hiện tại và sử dụng mô hình vật lý để tính toán vị trí xy tại thời điểm đó. Gọi getCurrX()getCurrY() để truy xuất các giá trị này.

Hầu hết các khung hiển thị đều truyền trực tiếp vị trí xy của đối tượng Scroller đến scrollTo(). Ví dụ này hơi khác: sử dụng vị trí cuộn x hiện tại để đặt góc xoay của khung hiển thị.

Kotlin

scroller.apply {
    if (!isFinished) {
        computeScrollOffset()
        setItemRotation(currX)
    }
}

Java

if (!scroller.isFinished()) {
    scroller.computeScrollOffset();
    setItemRotation(scroller.getCurrX());
}

Lớp Scroller tính toán vị trí cuộn cho bạn, nhưng không tự động áp dụng các vị trí đó cho khung hiển thị. Áp dụng các toạ độ mới thường xuyên đủ để làm cho ảnh động cuộn trông mượt mà. Có 2 cách để thực hiện việc này:

  • Buộc vẽ lại bằng cách gọi postInvalidate() sau khi gọi fling(). Kỹ thuật này yêu cầu bạn tính toán độ lệch cuộn trong onDraw() và gọi postInvalidate() mỗi khi độ lệch cuộn thay đổi.
  • Thiết lập ValueAnimator để tạo ảnh động trong thời gian hất và thêm một trình nghe để xử lý cập nhật ảnh động bằng cách gọi addUpdateListener(). Kỹ thuật này cho phép bạn tạo ảnh động cho các thuộc tính của View.

Giúp quá trình chuyển cảnh mượt mà

Người dùng mong muốn một giao diện người dùng hiện đại chuyển đổi suôn sẻ giữa các trạng thái: các thành phần trên giao diện người dùng mờ dần và biến mất thay vì xuất hiện và biến mất, cũng như các chuyển động bắt đầu và kết thúc suôn sẻ thay vì bắt đầu và dừng đột ngột. Khung ảnh động thuộc tính của Android giúp quá trình chuyển đổi diễn ra suôn sẻ hơn.

Để sử dụng hệ thống ảnh động, mỗi khi một thuộc tính thay đổi và ảnh hưởng đến giao diện của khung hiển thị, đừng trực tiếp thay đổi thuộc tính đó. Thay vào đó, hãy sử dụng ValueAnimator để thực hiện thay đổi. Trong ví dụ sau, việc sửa đổi thành phần con đã chọn trong khung hiển thị sẽ khiến toàn bộ khung hiển thị đã kết xuất xoay để con trỏ lựa chọn nằm ở giữa. ValueAnimator thay đổi chế độ xoay trong khoảng thời gian vài trăm mili giây, thay vì đặt giá trị xoay mới ngay lập tức.

Kotlin

autoCenterAnimator = ObjectAnimator.ofInt(this, "Rotation", 0).apply {
    setIntValues(targetAngle)
    duration = AUTOCENTER_ANIM_DURATION
    start()
}

Java

autoCenterAnimator = ObjectAnimator.ofInt(this, "Rotation", 0);
autoCenterAnimator.setIntValues(targetAngle);
autoCenterAnimator.setDuration(AUTOCENTER_ANIM_DURATION);
autoCenterAnimator.start();

Nếu giá trị bạn muốn thay đổi là một trong các thuộc tính View cơ sở, thì việc tạo ảnh động thậm chí còn dễ dàng hơn vì các khung hiển thị có ViewPropertyAnimator tích hợp được tối ưu hoá cho ảnh động đồng thời của nhiều thuộc tính, như trong ví dụ sau:

Kotlin

animate()
    .rotation(targetAngle)
    .duration = ANIM_DURATION
    .start()

Java

animate().rotation(targetAngle).setDuration(ANIM_DURATION).start();