Understanding android input event

Cho dù bạn làm bất cứ ứng dụng android nào thì đều phục vụ 1 nhóm đối tượng người dùng nào đó. Và phần lớn người sử dụng tương tác với ứng dụng của bạn là thông qua 1 loạt các action click button, swipe left/right, fling ... tôi gọi chung các action đầu vào này là touch event. Về cơ bản mọi touch event đều được Android framework xử lý tuy nhiên một số trường hợp đặc biệt bạn cần phải can thiệp vào việc xử lý lại cho các touch event. Giả sử bạn muốn custom list view bên trong 1 srcollview chẳng hạn, để tránh xung đột giữa việc scroll của cả 2 thì bạn cần phải làm như thế nào? Dưới đây sẽ là những key point mà bạn cần hiểu để thực hiện được custom view của mình.

1. Compilation (biên tập mục tham khảo)

Thay vì giải thích toàn bộ theo ý hiểu của mình thì tôi sẽ trích dẫn các nguồn resource tham khảo mà tôi nghĩ nó làm việc này tốt hơn tôi rất nhiều.

2. Android Touch System Event Propagation (cơ chế truyền dẫn touch event)

Bằng việc tập hợp các thông tin từ các nguồn tham khảo trên cộng với việc thực hiện viết 1 vài custom view để test, tôi sẽ mô tả lại và giải thích các core flow. Khi mà sự kiện touch được kích hoạt (ACTION_DOWN) bởi android hardware (và tất nhiên cả do ngón tay của chúng ta) thì nơi đầu tiên được xử lý là phương thức: Activity.dispatchTouchEvent(). Đó luôn là nơi được gọi đầu tiên và sau đó sẽ truyền 1 object MotionEvent tới cho root view (chính là DecorView) cái được attach cùng với window.

Activity.dispatchTouchEvent()

  • Luôn được gọi đầu tiên.
  • Gửi 1 event tới cho root view. Và bây giờ nếu Activity.dispatchTouchEvent() trả về true thì nó sẽ xử lý các gesture event (MOVE/UP) sau đó, còn nếu trả về false thì các sự kiện sau đó sẽ không được xử lý. Trước khi chúng ta đi tiếp thì các bạn cần nhớ rằng dispatchTouchEvent() được định nghĩa trên Activity, View và ViewGroup. Chúng có trách nhiệm điều hướng các touch event tới nơi mà chúng phải tới (chính là đi xuống từng cấp phía dưới - hierarchy).

Các root view bắt đầu dispatching (gửi) các event xuống các view con của nó. Hãy giả sử rằng chúng ta đang có cấu trúc cây của layout như sau:

  • A – ViewGroup1 (parent of B).
  • B – ViewGroup2 (parent of C).
  • C – View (child of B) – receives a touch/tap/click. Bây giờ root view sẽ gọi A.dispatchTouchEvent() và công việc của ViewGroup.dispatchEvent() (not View.dispatchEvent()) sẽ là tìm ra tất cả các view con chứa trong nó và gửi sự kiện tới cho chúng bằng cách gọi dispatchEvent() của chúng. Đây là phần quan trọng, trước khi dispatchTouchEvent() được gọi trên các view con thì A.dispatchTouchEvent() sẽ gọi trước hàm A.onInterceptTouchEvent() để xem liệu viewgroup có intercept (chặn) và xử lý các gesture event bởi chính nó hay không (Scrolling là 1 ví dụ tốt, nếu bạn vuốt trên B thì sẽ dẫn đến scrolling trên A tức là A sẽ tự xử lý để scrolling). Phương thức onInterceptTouchEvent() chỉ xuất hiện ở ViewGroup (giống như là nơi có thể xử lý chặn touch event) cái có thể theo dõi các event và chiếm quyền xử lý bằng việc return true. Còn nếu return false thì dispatching sẽ tiếp tục xử lý như thường tức là B.dispatchTouchEvent() (child) sẽ được gọi. Khi return true được trả về thì cơ chế hoạt động sẽ như sau:

ViewGroup.dispatchTouchEvent()

  1. onInterceptTouchEvent()
  • Kiểm tra xem có xử lý event hay không.
  • Nếu có tức là return true thì sẽ gửi ACTION_CANCEL tới các view con (bởi dispatchTouchEvent()) và các sự kiện sau đó sẽ được xử lý bởi chính viewgroup đó.
  1. Nếu onInterceptTouchEvent() không xử lý tức là trả về false thì từng view con sẽ được xử lý theo thứ ngược lại với thứ tự được add vào.
  • Gọi child.dispatchTouchEvent() nếu vùng touch nằm bên trong view.
  • Còn nếu ko xử lý bởi previous view (bằng việc return true ở previous_child.dispatchTouchEvent() hoặc previous_child.onInterceptTouchEvent()) thì sẽ chuyển sang cho next_view xử lý.
  1. Nếu không có view con nào xử lý thì OnTouchListener.onTouch() sẽ có cơ hội được xử lý nếu được định nghĩa, còn ko thì onTouchEvent() sẽ thực hiện.

onInterceptTouchEvent() cho ViewGroup (như ví dụ trên scrollview) return true thì sẽ tác động tới chuỗi dispatch, vì thế View.dispatchTouchEvent() và View.onTouchEvent nếu đăng ký sẽ được gửi ACTION_CANCEL .

Bây giờ chúng ta đã hiểu quá trình hoạt động từ A.dispatchTouchEvent() tới B.dispatchTouchEvent() (Viewgroup tời Viewgroup) thì sẽ bắt đầu xem quá trình từ B.dispatchTouchEvent() tời C.dispatchTouchEvent() (Viewgroup tới View). Giờ hãy xem C.dispatchTouchEvent() sẽ hoạt động như thế nào.

View.dispatchTouchEvent()

  • Gửi event tới View.OnTouchListener.onTouch() trước tiên nếu listener này tồn tại
  • Nếu không thì View sẽ tự gọi View.onTouchEvent().

Bây giờ nếu event listener của View return true thì nó sẽ trở thành nơi xử lý của tất cả các sự kiện sau đó. Còn nếu 1 trong số chúng trả về false thì event sẽ được đẩy lên theo cấp (level) layout tree và ở mỗi level sẽ được gọi onTouchEvent() hoặc OnTouchListener.onTouch()

Cho nên ở ví dụ trên nếu như C.onTouchEvent() return false thì nó sẽ quay ngược lại gọi B.onTouchEvent(), tại đây tiếp tục trả về false thì lại quay ngược lại và gọi A.onTouchEvent(). Nếu ở bất kỳ level nào mà trả về true thì bạn có đoán được chuyện gì sẽ xảy ra rồi chứ. Nó sẽ xử lý tất cả các event sau đó và onInterceptTouchEvent() sẽ không được gọi. Khi tất cả các level đều trả về false thì cuối cùng sẽ gọi đến Activity.onTouchEvent() với điều kiện bạn phải nhớ gọi super. Như bạn cũng thấy rằng khi đặt ngón tay xuống màn hình thì event được truyền thông qua dispatchTouchEvent() tới mọi level. Trong khi quay trở lại các view cha thì nó lại được bubbles (nổi lên) thông qua onTouchEvent() (hoặc event listener được định nghĩa) ở mọi level. Khi đặt ngón tay xuống màn hình thì ở mọi level mà là ViewGroup thì đều tiến hành kiểm tra onInterceptTouchEvent(), nếu bạn không muốn thì có thể bỏ qua việc check này bằng cách return true ở requestDisallowInterceptTouchEvent() trên ViewGroup. Bằng cách này bạn có thể loại bỏ việc intercept event từ cha, ông , tổ tiên của nó được viết ở hàm onInterceptTouchEvent(). Một gợi ý ở đây là nếu bạn thực sự cần phải trả về true thì nên phân chia action, với ACTION_DOWN/ACTION_MOVE thì trả về true còn nếu ACTION_UP/ACTION_CANCEL thì trả về false để giữ hành vi thông thường (điều này có ích với trường hợp 2 scrollview lông nhau, bạn cần phải chặn hành vi của scroollview bên ngoài).

Một vấn đề quan trọng nữa cần quan tâm là phải hiểu được là kết quả trả về của dispatchTouchEvent() sẽ ảnh hưởng như thế nào tới hệ thống đặc biệt là khi bạn custom view. Ví dụ như 1 scrollview hoặc 1 listview nếu muốn cho việc scrolling hoạt động bình thường thì dispatchTouchEvent() phải return true. Và bây giờ khi bạn muốn extend 1 class như scrollview hoặc listview thì bạn có thể ủy nhiệm việc dispatch (gửi) các event xuống cho các child view bằng việc gọi super.dispatchTouchEvent() ở trong CustomView.dispatchTouchEvent() của bạn.super.dispatchTouchEvent() hay dispatchTouchEvent() nói chung sẽ rất hữu ích vì nó chỉ định rằng ViewGroup có thực sự xử lý event hay là không. Cho nên trong ví dụ trên nếu B.dispatchTouchEvent() return true thì giá trị trả về của super.dispatchTouchEvent() trong A.dispatchTouchEvent() cũng sẽ là true và đó cũng chính xác là cái mà A cần trả về (giống như sử dụng dòng code return super.dispatchTouchEvent()) để cho các event có thể đi xuống phía dưới được.

Đây là stack trace được gọi từ Thread.dumpStack() của onTouchEvent() mà tôi viết extend từ ListView sẽ cho bạn thông tin tốt hơn từ chuỗi event xảy ra.

: java.lang.Throwable: stack dump
:   at java.lang.Thread.dumpStack(Thread.java:489)
:   at com.pycitup.pyc.CustomList.DisabledListView.onTouchEvent(DisabledListView.java:60)
:   at android.view.View.dispatchTouchEvent(View.java:7706)
:   at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2210)
:   at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1945)
:   at com.pycitup.pyc.CustomList.ScrollDisabledListView.dispatchTouchEvent(ScrollDisabledListView.java:34)
:   at com.pycitup.pyc.CustomList.DisabledListView.dispatchTouchEvent(DisabledListView.java:36)
:   at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2216)
:   at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1917)
:   at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2216)
:   at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1917)
:   at com.pycitup.pyc.CustomList.CustomScrollView.dispatchTouchEvent(CustomScrollView.java:40)
:   at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2216)
:   at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1917)
:   at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2216)
:   at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1917)
:   at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2216)
:   at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1917)
:   at com.android.internal.policy.impl.PhoneWindow$DecorView.superDispatchTouchEvent(PhoneWindow.java:2068)
:   at com.android.internal.policy.impl.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1515)
:   at android.app.Activity.dispatchTouchEvent(Activity.java:2458)
:   at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchTouchEvent(PhoneWindow.java:2016)
:   at android.view.View.dispatchPointerEvent(View.java:7886)
:   at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:3947)
:   at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:3826)

Mọi người tham khảo thêm : https://github.com/Orange168/custom-touch-sample

Bài tham khảo từ: http://codetheory.in/understanding-android-input-touch-events/