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

Thử cách 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.

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 làm cho chế độ xem 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ư các đối tượng thực. Ví dụ: đừng để hình ảnh trong ứng dụng xuất hiện 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 điều đó. Thay vào đó, hãy di chuyển hình ảnh từ vị trí này sang vị trí khác.

Người dùng cảm nhận được cả hành vi hoặc cảm giác tinh tế trong một giao diện và phản ứng tốt nhất với những chi tiết nhỏ bắt chước 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, hãy cho họ cảm giác quán tính lúc đầu làm trì hoãn chuyển động. Ở cuối chuyển động, hãy cho chúng cảm giác về động lượng đưa vật thể 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. Thao tác của người dùng chuyển thành các sự kiện kích hoạt lệnh gọi lại, đồng thời 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à 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);
   }

Bản thân 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 các hoạt động tương tác bằng cử chỉ như nhấn, kéo, đẩy, hất và thu phóng. Để chuyển đổi các sự kiện chạm thô thành cử chỉ, Android sẽ cung cấp GestureDetector.

Tạo GestureDetector bằng cách truyền vào một thực thể của lớp sẽ 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 sẽ 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 một 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ỉ đều 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 mà bạn 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à nó không nhận ra là một phần của cử chỉ, nó 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 thiết bị màn hình cảm ứng, nhưng cử chỉ có thể khác thường và khó nhớ trừ khi chúng tạo ra kết quả thực tế hợp lý.

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 khung hiển thị quay xung quanh trục tung của nó. Cử chỉ này có ý nghĩa 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 đó giảm tốc độ, như thể người dùng đẩy bánh đà và làm cho bánh quay tròn.

Tài liệu về cách tạo ảnh động cho cử chỉ cuộn giải thích chi tiết về cách triển khai hành vi scoll của riêng bạn. Tuy nhiên, việc mô phỏng cảm giác của bánh đà không phải là một việc đơn giản. Cần thực hiện rất nhiều vật lý và toán học để 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 vận tốc bắt đầu cũng như 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ị do GestureDetector tính toán.

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() 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 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 khung hiển thị đều truyền trực tiếp các vị trí xy của đối tượng Scroller đến scrollTo(). Ví dụ này hơi khác một chút: nó sử dụng vị trí cuộn x hiện tại để đặt góc quay 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 các 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ị của bạn. Á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ó hai 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 trình nghe để xử lý thông tin 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 chuyển cảnh mượt mà

Người dùng mong đợi 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á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, bất cứ khi nào một thuộc tính thay đổi những gì ảnh hưởng đến giao diện của thành phần hiển thị, thì bạn đừ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 được căn giữa. ValueAnimator thay đổi chế độ xoay trong khoảng thời gian vài trăm mili giây, thay vì đặt ngay giá trị xoay mới.

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 thực hiện ảnh động sẽ còn dễ dàng hơn nữa vì khung hiển thị có sẵn ViewPropertyAnimator được tối ưu hoá để tạo ảnh động cùng lúc 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();