Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Understanding Android Gestures

Caren
October 28, 2017

Understanding Android Gestures

For developers working with custom views and touches for the first time, it could sometimes be difficult to decipher what goes on when a user touches the screen. In this talk we’ll trace through the steps to see how Android decides which view should handle a touch, and how the system differentiates between swipes and clicks. We’ll start by visually examining how a view hierarchy is laid out and examine the multiple methods involved when the system receives a touch. Then we’ll look into the different kinds of gestures that are categorized in Android and how the system finally decides to call the ubiquitous onClickListener().

Caren

October 28, 2017
Tweet

More Decks by Caren

Other Decks in Programming

Transcript

  1. button.setOnClickListener(new View.OnClickListener() {
 @Override
 public boolean onClick() {
 
 }


    }); button.setOnLongClickListener(new View.OnLongClickListener() {
 @Override
 public boolean onLongClick(View v) {
 return false;
 }
 });
  2. Motivations UI for an oven != UI for phone Remove

    “feeling” of using a phone
  3. Motivations UI for an oven != UI for phone Remove

    “feeling” of using a phone Psychology of clicking
  4. Motivations UI for an oven != UI for phone Remove

    “feeling” of using a phone Psychology of clicking Angling
  5. Motivations for you Understand your phone Customize for the behaviors

    you want Create better user experience Be less stressed when developing and debugging
  6. What happens when a user interacts with their phone? Initiate

    ‘contact’ Moves finger around Lifts finger
  7. What happens when a user interacts with their phone? Initiate

    ‘contact’ ACTION_DOWN Moves finger around ACTION_MOVE Lifts finger ACTION_UP
  8. MotionEvent Every action on the screen is translated into 


    MotionEvent Description movements with an action code and
 set of axis values
  9. MotionEvent Every action on the screen is translated into 


    MotionEvent Description movements with an action code and 
 set of axis values Action code = up, down, move
  10. MotionEvent Every action on the screen is translated into 


    MotionEvent Description movements with an action code and 
 set of axis values Action code = up, down, move Axis values = touch position
  11. Screen receives a touch event Using the (x , y)

    coordinate, 
 the MotionEvent is sent to the 
 appropriate child view(s) MotionEvent
  12. Screen receives a touch event Using the (x , y)

    coordinate, 
 the MotionEvent is sent to the 
 appropriate child view(s) The view can decide how to 
 handle the MotionEvent MotionEvent
  13. Decide? What does the view decide? boolean onTouchEvent() Views can

    decide if they want to take action Returns true if the event was handled, false otherwise
  14. public class CustomView extends View {
 @Override
 public boolean onTouchEvent(MotionEvent

    motionEvent) {
 
 switch (motionEvent.getAction()) {
 case MotionEvent.ACTION_DOWN:
 Log.i("touch", "down event");
 break;
 case MotionEvent.ACTION_MOVE:
 Log.i("touch", "move event");
 break;
 case MotionEvent.ACTION_UP:
 Log.i("touch", "up event");
 }
 return false;
 }
 }
  15. public class CustomView extends View {
 @Override
 public boolean onTouchEvent(MotionEvent

    motionEvent) {
 
 switch (motionEvent.getAction()) {
 case MotionEvent.ACTION_DOWN:
 Log.i("touch", "down event");
 break;
 case MotionEvent.ACTION_MOVE:
 Log.i("touch", "move event");
 break;
 case MotionEvent.ACTION_UP:
 Log.i("touch", "up event");
 }
 return false;
 }
 }
  16. public class CustomView extends View {
 @Override
 public boolean onTouchEvent(MotionEvent

    motionEvent) {
 
 switch (motionEvent.getAction()) {
 case MotionEvent.ACTION_DOWN:
 Log.i("touch", "down event");
 break;
 case MotionEvent.ACTION_MOVE:
 Log.i("touch", "move event");
 break;
 case MotionEvent.ACTION_UP:
 Log.i("touch", "up event");
 }
 return false;
 }
 }
  17. public class CustomView extends View {
 @Override
 public boolean onTouchEvent(MotionEvent

    motionEvent) {
 
 switch (motionEvent.getAction()) {
 case MotionEvent.ACTION_DOWN:
 Log.i("touch", "down event");
 break;
 case MotionEvent.ACTION_MOVE:
 Log.i("touch", "move event");
 break;
 case MotionEvent.ACTION_UP:
 Log.i("touch", "up event");
 }
 return false;
 }
 }
  18. public class CustomView extends View {
 @Override
 public boolean onTouchEvent(MotionEvent

    motionEvent) {
 
 switch (motionEvent.getAction()) {
 case MotionEvent.ACTION_DOWN:
 Log.i("touch", "down event");
 break;
 case MotionEvent.ACTION_MOVE:
 Log.i("touch", "move event");
 break;
 case MotionEvent.ACTION_UP:
 Log.i("touch", "up event");
 }
 return false;
 }
 }
  19. Creating our own click listener What does it take for

    a series of motion events to be considered a “click” or a “long click” ?
  20. Creating our own click listener What does it take for

    a series of motion events to 
 be considered a “click” or a “long click” ? time it takes from the first down event till the last up event
  21. Creating our own click listener What does it take for

    a series of motion events to 
 be considered a “click” or a “long click” ? time it takes from the first down event till the last up event difference in distance in between down and up
  22. custom ‘click’ listener case MotionEvent.ACTION_UP:
 Log.i("touch", "up event");
 long eventEndTime

    = System.currentTimeMillis();
 
 // time in ms when the first down event registered
 long eventStartTime = motionEvent.getDownTime();
 long totalTimeElapsed = eventEndTime - eventStartTime;
 
 if (totalTimeElapsed < clickTimeoutMs) {
 performClick();
 } else if (totalTimeElapsed < longClickTimeoutMs) {
 performLongClick();
 } else {
 Log.i(TAG, "click action timed out");
 }
  23. custom ‘click’ listener case MotionEvent.ACTION_UP:
 Log.i("touch", "up event");
 long eventEndTime

    = System.currentTimeMillis();
 
 // time in ms when the first down event registered
 long eventStartTime = motionEvent.getDownTime();
 long totalTimeElapsed = eventEndTime - eventStartTime;
 
 if (totalTimeElapsed < clickTimeoutMs) {
 performClick();
 } else if (totalTimeElapsed < longClickTimeoutMs) {
 performLongClick();
 } else {
 Log.i(TAG, "click action timed out");
 }
  24. custom ‘click’ listener case MotionEvent.ACTION_UP:
 Log.i("touch", "up event");
 long eventEndTime

    = System.currentTimeMillis();
 
 // time in ms when the first down event registered
 long eventStartTime = motionEvent.getDownTime();
 long totalTimeElapsed = eventEndTime - eventStartTime;
 
 if (totalTimeElapsed < clickTimeoutMs) {
 performClick();
 } else if (totalTimeElapsed < longClickTimeoutMs) {
 performLongClick();
 } else {
 Log.i(TAG, "click action timed out");
 }
  25. custom ‘click’ listener case MotionEvent.ACTION_UP:
 Log.i("touch", "up event");
 long eventEndTime

    = System.currentTimeMillis();
 
 // time in ms when the first down event registered
 long eventStartTime = motionEvent.getDownTime();
 long totalTimeElapsed = eventEndTime - eventStartTime;
 
 if (totalTimeElapsed < clickTimeoutMs) {
 performClick();
 } else if (totalTimeElapsed < longClickTimeoutMs) {
 performLongClick();
 } else {
 Log.i(TAG, "click action timed out");
 }
  26. custom ‘click’ listener public class CustomView extends View {
 


    
 @Override
 public boolean onTouchEvent(MotionEvent motionEvent) {
 if (motionEvent.getAction() == MotionEvent.ACTION_UP && isTouchInView(motionEvent)) {
 … // input code we had previously
 }
 
 return false;
 }
 }
  27. custom ‘click’ listener public class CustomView extends View {
 


    
 @Override
 public boolean onTouchEvent(MotionEvent motionEvent) {
 if (motionEvent.getAction() == MotionEvent.ACTION_UP && isTouchInView(motionEvent)) {
 … // input code we had previously
 }
 
 return false;
 }
 }
  28. custom ‘click’ listener public class CustomView extends View {
 


    
 @Override
 public boolean onTouchEvent(MotionEvent motionEvent) {
 if (motionEvent.getAction() == MotionEvent.ACTION_UP && isTouchInView(motionEvent)) {
 … // input code we had previously
 }
 
 return false;
 }
 }
  29. custom ‘click’ listener public class CustomView extends View {
 


    
 @Override
 public boolean onTouchEvent(MotionEvent motionEvent) {
 if (motionEvent.getAction() == MotionEvent.ACTION_UP && isTouchInView(motionEvent)) {
 … // input code we had previously
 }
 
 return false;
 }
 }
  30. swipe down to dismiss @Override
 public boolean onTouchEvent(MotionEvent motionEvent) {


    
 if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
 initialDownYCoord = motionEvent.getRawY();
 }
 
 if (motionEvent.getAction() == MotionEvent.ACTION_UP && isTouchInView(motionEvent)) {
 if (motionEvent.getRawY() - initialDownYCoord > SWIPE_DOWN_RANGE) { // swiped down
 startAnimation(slide_down); // slide_down is a predefined animation
 return true;
 }
 }
 
 return super.onTouchEvent(motionEvent);
 }
  31. swipe down to dismiss @Override
 public boolean onTouchEvent(MotionEvent motionEvent) {


    
 if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
 initialDownYCoord = motionEvent.getRawY();
 }
 
 if (motionEvent.getAction() == MotionEvent.ACTION_UP && isTouchInView(motionEvent)) {
 if (motionEvent.getRawY() - initialDownYCoord > SWIPE_DOWN_RANGE) { // swiped down
 startAnimation(slide_down); // slide_down is a predefined animation
 return true;
 }
 }
 
 return super.onTouchEvent(motionEvent);
 }
  32. swipe down to dismiss @Override
 public boolean onTouchEvent(MotionEvent motionEvent) {


    
 if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
 initialDownYCoord = motionEvent.getRawY();
 }
 
 if (motionEvent.getAction() == MotionEvent.ACTION_UP && isTouchInView(motionEvent)) {
 if (motionEvent.getRawY() - initialDownYCoord > SWIPE_DOWN_RANGE) { // swiped down
 startAnimation(slide_down); // slide_down is a predefined animation
 return true;
 }
 }
 
 return super.onTouchEvent(motionEvent);
 }
  33. swipe down to dismiss @Override
 public boolean onTouchEvent(MotionEvent motionEvent) {


    
 if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
 initialDownYCoord = motionEvent.getRawY();
 }
 
 if (motionEvent.getAction() == MotionEvent.ACTION_UP && isTouchInView(motionEvent)) {
 if (motionEvent.getRawY() - initialDownYCoord > SWIPE_DOWN_RANGE) { // swiped down
 startAnimation(slide_down); // slide_down is a predefined animation
 return true;
 }
 }
 
 return super.onTouchEvent(motionEvent);
 }
  34. Why have we been extending the View class? Based on

    what we’ve done so far, we could have simplified things with something such as :
 view.setOnTouchListener(new View.OnTouchListener() {
 @Override
 public boolean onTouch(View v, MotionEvent event) {
 return false;
 }
 });
  35. Why have we been extending the View class? Based on

    what we’ve done so far, we could have simplified 
 things with something such as :
 view.setOnTouchListener(new View.OnTouchListener() {
 @Override
 public boolean onTouch(View v, MotionEvent event) {
 return false;
 }
 }); While onTouchListener is great , we get more control when we creating a custom view class
  36. boolean onInterceptTouchEvent() Called whenever a touch event occurs If this

    method returns true, the MotionEvent will not be passed to child Can be used to ‘listen’ to events and act accordingly
  37. boolean dispatchTouchEvent() Passes the motion event to the target view

    (or this view if it’s the target) Returns true if handled by current view, false otherwise
  38. For every MotionEvent : dispatchTouchEvent() is called in the order

    of Activity down to Button If FrameLayout.dispatchTouchEvent() returned true, Button would not call dispatchTouchEvent() Instead, FrameLayout.onTouchEvent() would be called Activity FrameLayout Button
  39. For every MotionEvent : dispatchTouchEvent() is called in the order

    of Activity down to Button If FrameLayout.dispatchTouchEvent() returned true, Button would not call dispatchTouchEvent() Instead, FrameLayout.onTouchEvent() would be called Activity FrameLayout Button
  40. For every MotionEvent : dispatchTouchEvent() is called in the order

    of Activity down to Button If FrameLayout.dispatchTouchEvent() returned true, Button would not call dispatchTouchEvent() Instead, FrameLayout.onTouchEvent() would be called Activity FrameLayout Button
  41. If dispatchTouchEvent() gets to Button, and there’s no more child

    views, Button.onTouchEvent() would be called Activity FrameLayout Button
  42. If dispatchTouchEvent() gets to Button, and there’s no more child

    views, Button.onTouchEvent() would be called onTouchEvent() is then called from Button to FrameLayout to Activity until one of the views decides to handle it Activity FrameLayout Button
  43. github.com/calren/viewsAndTouches @Override
 public boolean dispatchTouchEvent(MotionEvent motionEvent) {
 Log.i("touchevent", "dispatchTouchEvent ACTIVITY");


    return super.dispatchTouchEvent(motionEvent);
 }
 
 @Override
 public boolean onTouchEvent(MotionEvent motionEvent) {
 Log.i("touchevent", "onTouchEvent ACTIVITY");
 return super.onTouchEvent(motionEvent);
 }
  44. Things to consider when customizing behavior Most common gestures are

    already implemented (tap, doubleTap, fling)
  45. Things to consider when customizing behavior Most common gestures are

    already implemented (tap, doubleTap, fling) Try to base values off base values defined by Android TOUCH_SLOP, TAP_TIMEOUT, PAGING_TOUCH_SLOP
  46. In Summary… public boolean dispatchTouchEvent(MotionEvent event) {
 // If the

    event should be handled by accessibility focus first.
 if (event.isTargetAccessibilityFocus()) {
 // We don't have focus or no virtual descendant has it, do not handle the event.
 if (!isAccessibilityFocusedViewOrHost()) {
 return false;
 }
 // We have focus and got the event, then use normal event dispatch.
 event.setTargetAccessibilityFocus(false);
 }
 
 boolean result = false;
 
 if (mInputEventConsistencyVerifier != null) {
 mInputEventConsistencyVerifier.onTouchEvent(event, 0);
 }
 
 final int actionMasked = event.getActionMasked();
 if (actionMasked == MotionEvent.ACTION_DOWN) {
 // Defensive cleanup for new gesture
 stopNestedScroll();
 }
 
 if (onFilterTouchEventForSecurity(event)) {
 if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
 result = true;
 }
 //noinspection SimplifiableIfStatement
 ListenerInfo li = mListenerInfo;
 if (li != null && li.mOnTouchListener != null
 && (mViewFlags & ENABLED_MASK) == ENABLED
 && li.mOnTouchListener.onTouch(this, event)) {
 result = true;
 }
 
 if (!result && onTouchEvent(event)) {
 result = true;
 }
 }
 
 if (!result && mInputEventConsistencyVerifier != null) {
 mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
 }
 
 // Clean up after nested scrolls if this is the end of a gesture;
 // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
 // of the gesture.
 if (actionMasked == MotionEvent.ACTION_UP ||
 actionMasked == MotionEvent.ACTION_CANCEL ||
 (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
 stopNestedScroll();
 }
 
 return result;

  47. In Summary… Custom touch event handling is complicated All touch

    events on Android are packaged into a MotionEvent
  48. In Summary… Custom touch event handling is complicated All touch

    events on Android are packaged into a MotionEvent Touch events are passed down the view hierarchy until an interested view handles it onTouchEvent()
  49. In Summary… Custom touch event handling is complicated All touch

    events on Android are packaged into a MotionEvent Touch events are passed down the view hierarchy until an interested view handles it onTouchEvent() Events can be intercepted and redirected as needed
 dispatchTouchEvent() and onInterceptTouchEvent()
  50. Other great talks Writing Custom Views for Android Adam Powell,

    Romain Guy Measure, Layout, Draw, Repeat: Custom Views and ViewGroups Huyen Tue Dao Making Sense of the Touch System Philippe Breault Mastering the Android Touch System Dave Smith