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

Making Sense of the touch system

Making Sense of the touch system

Touch is pretty simple, right? You set an onClickListener and you are all set! But then... what happens when you want to drag stuff? What happens when you want to add gestures? How do you manage multiple views fighting for the focus? What is this pointer index thing? How do you animate a view after a fling and keep it natural? What is a touch slop?

In this talk, we will take a good look at what happens under the hood when your fingers touch the screen and how you can use this to create interesting ways to interact with your app!

Philippe Breault

August 27, 2015
Tweet

More Decks by Philippe Breault

Other Decks in Programming

Transcript

  1. Where? public class MyView extends View {
 @Override
 public boolean

    onTouchEvent(MotionEvent event) {
 …
 return super.onTouchEvent(event);
 }
 }
 Override
  2. The Code public boolean onTouchEvent(MotionEvent event) {
 super.onTouchEvent(event);
 switch (event.getActionMasked())

    {
 case MotionEvent.ACTION_DOWN:
 setBackgroundColor(RED);
 break;
 
 case MotionEvent.ACTION_UP:
 setBackgroundColor(BLUE);
 if (isInside(event.getX(), event.getY()))
 toast("Click!");
 break;
 }
 
 return true;
 }
  3. The Code public boolean onTouchEvent(MotionEvent event) {
 super.onTouchEvent(event);
 switch (event.getActionMasked())

    {
 case MotionEvent.ACTION_DOWN:
 setBackgroundColor(RED);
 break;
 
 case MotionEvent.ACTION_UP:
 setBackgroundColor(BLUE);
 if (isInside(event.getX(), event.getY()))
 toast("Click!");
 break;
 }
 
 return true;
 }
  4. The Code public boolean onTouchEvent(MotionEvent event) {
 super.onTouchEvent(event);
 switch (event.getActionMasked())

    {
 case MotionEvent.ACTION_DOWN:
 setBackgroundColor(RED);
 break;
 
 case MotionEvent.ACTION_UP:
 setBackgroundColor(BLUE);
 if (isInside(event.getX(), event.getY()))
 toast("Click!");
 break;
 }
 
 return true;
 }
  5. The Code public boolean onTouchEvent(MotionEvent event) {
 super.onTouchEvent(event);
 switch (event.getActionMasked())

    {
 case MotionEvent.ACTION_DOWN:
 setBackgroundColor(RED);
 break;
 
 case MotionEvent.ACTION_UP:
 setBackgroundColor(BLUE);
 if (isInside(event.getX(), event.getY()))
 toast("Click!");
 break;
 }
 
 return true;
 }
  6. The Code public boolean onTouchEvent(MotionEvent event) {
 super.onTouchEvent(event);
 switch (event.getActionMasked())

    {
 case MotionEvent.ACTION_DOWN:
 setBackgroundColor(RED);
 break;
 
 case MotionEvent.ACTION_UP:
 setBackgroundColor(BLUE);
 if (isInside(event.getX(), event.getY()))
 toast("Click!");
 break;
 }
 
 return true;
 }
  7. The Code @Override
 public boolean onTouchEvent(MotionEvent event) {
 super.onTouchEvent(event);
 


    switch (event.getActionMasked()) {
 case MotionEvent.ACTION_DOWN:
 mPreviousY = event.getY();
 break;
 
 case MotionEvent.ACTION_MOVE:
 float distance = mPreviousY - event.getY();
 scrollBy(0, Math.round(distance));
 mPreviousY = event.getY();
 break;
 }
 
 return true;
 }
  8. The Code @Override
 public boolean onTouchEvent(MotionEvent event) {
 super.onTouchEvent(event);
 


    switch (event.getActionMasked()) {
 case MotionEvent.ACTION_DOWN:
 mPreviousY = event.getY();
 break;
 
 case MotionEvent.ACTION_MOVE:
 float distance = mPreviousY - event.getY();
 scrollBy(0, Math.round(distance));
 mPreviousY = event.getY();
 break;
 }
 
 return true;
 }
  9. The Code @Override
 public boolean onTouchEvent(MotionEvent event) {
 super.onTouchEvent(event);
 


    switch (event.getActionMasked()) {
 case MotionEvent.ACTION_DOWN:
 mPreviousY = event.getY();
 break;
 
 case MotionEvent.ACTION_MOVE:
 float distance = mPreviousY - event.getY();
 scrollBy(0, Math.round(distance));
 mPreviousY = event.getY();
 break;
 }
 
 return true;
 }
  10. Top Level View receives the gesture Touch Target Receives all

    MotionEvents for the rest of the gesture
  11. Top Level View receives the gesture Touch Target Receives all

    MotionEvents for the rest of the gesture
  12. Touch Target @Override
 public boolean onTouchEvent(MotionEvent event) {
 boolean handled

    = super.onTouchEvent(event);
 
 if (canHandleIt()) {
 // do stuff
 handled = true;
 }
 
 return handled;
 }
  13. Touch Interception @Override
 public boolean onInterceptTouchEvent(MotionEvent event) {
 return super.onInterceptTouchEvent(ev);


    } @Override
 public boolean onTouchEvent(MotionEvent event) {
 . . .
 return super.onTouchEvent(ev);
 }
  14. • See MotionEvents before children • Steal MotionEvents if needed

    • Only available in ViewGroups Touch Interception
  15. • See MotionEvents before children • Steal MotionEvents if needed

    • Only available in ViewGroups • Interceptee receives ACTION_CANCEL Touch Interception
  16. The Code @Override
 public boolean onInterceptTouchEvent(MotionEvent ev) {
 boolean handled

    = super.onInterceptTouchEvent(ev);
 
 if (ev.getActionMasked() == ACTION_MOVE) {
 handled = true;
 mPreviousY = ev.getY();
 }
 return handled;
 }
  17. The Code @Override
 public boolean onInterceptTouchEvent(MotionEvent ev) {
 boolean handled

    = super.onInterceptTouchEvent(ev);
 
 if (ev.getActionMasked() == ACTION_MOVE) {
 handled = true;
 mPreviousY = ev.getY();
 }
 return handled;
 }
  18. The Code @Override
 public boolean onInterceptTouchEvent(MotionEvent ev) {
 boolean handled

    = super.onInterceptTouchEvent(ev);
 
 if (ev.getActionMasked() == ACTION_MOVE) {
 handled = true;
 mPreviousY = ev.getY();
 }
 return handled;
 } The state of the button is not restored when we scroll!
  19. The Code 
 public boolean onTouchEvent(MotionEvent event) {
 super.onTouchEvent(event);
 switch

    (event.getActionMasked()) {
 case ACTION_DOWN:
 setPressedState();
 break;
 case ACTION_CANCEL:
 restoreState();
 break;
 case ACTION_UP:
 restoreState();
 if (isInside(event.getX(), event.getY())) {
 toast("Click!");
 }
 break;
 }
 
 return true;
 }
  20. The Code 
 public boolean onTouchEvent(MotionEvent event) {
 super.onTouchEvent(event);
 switch

    (event.getActionMasked()) {
 case ACTION_DOWN:
 setPressedState();
 break;
 case ACTION_CANCEL:
 restoreState();
 break;
 case ACTION_UP:
 restoreState();
 if (isInside(event.getX(), event.getY())) {
 toast("Click!");
 }
 break;
 }
 
 return true;
 }
  21. ViewConfiguration int getScaledTouchSlop() int getScaledDoubleTapSlop()
 int getScaledEdgeSlop()
 int getScaledFadingEdgeLength()
 int

    getScaledMaximumFlingVelocity()
 int getScaledMinimumFlingVelocity()
 int getScaledOverflingDistance()
 int getScaledOverscrollDistance()
 int getScaledPagingTouchSlop()
 int getScaledScrollBarSize()
 int getScaledWindowTouchSlop()
  22. ViewConfiguration int getScaledTouchSlop() int getScaledDoubleTapSlop()
 int getScaledEdgeSlop()
 int getScaledFadingEdgeLength()
 int

    getScaledMaximumFlingVelocity()
 int getScaledMinimumFlingVelocity()
 int getScaledOverflingDistance()
 int getScaledOverscrollDistance()
 int getScaledPagingTouchSlop()
 int getScaledScrollBarSize()
 int getScaledWindowTouchSlop() Lots of useful values
  23. The Code @Override
 public boolean onInterceptTouchEvent(MotionEvent ev) {
 boolean handled

    = super.onInterceptTouchEvent(ev);
 
 switch (ev.getActionMasked()) {
 case ACTION_DOWN:
 initialDown = ev.getY();
 break;
 case ACTION_MOVE:
 float dy = ev.getY() - initialDown;
 if (Math.abs(dy) > touchSlop) {
 mPreviousY = ev.getY();
 handled = true;
 }
 break;
 }
 
 return handled;
 }
  24. The Code @Override
 public boolean onInterceptTouchEvent(MotionEvent ev) {
 boolean handled

    = super.onInterceptTouchEvent(ev);
 
 switch (ev.getActionMasked()) {
 case ACTION_DOWN:
 initialDown = ev.getY();
 break;
 case ACTION_MOVE:
 float dy = ev.getY() - initialDown;
 if (Math.abs(dy) > touchSlop) {
 mPreviousY = ev.getY();
 handled = true;
 }
 break;
 }
 
 return handled;
 }
  25. The Code @Override
 public boolean onInterceptTouchEvent(MotionEvent ev) {
 boolean handled

    = super.onInterceptTouchEvent(ev);
 
 switch (ev.getActionMasked()) {
 case ACTION_DOWN:
 initialDown = ev.getY();
 break;
 case ACTION_MOVE:
 float dy = ev.getY() - initialDown;
 if (Math.abs(dy) > touchSlop) {
 mPreviousY = ev.getY();
 handled = true;
 }
 break;
 }
 
 return handled;
 }
  26. The Code @Override
 public boolean onInterceptTouchEvent(MotionEvent ev) {
 boolean handled

    = super.onInterceptTouchEvent(ev);
 
 switch (ev.getActionMasked()) {
 case ACTION_DOWN:
 initialDown = ev.getY();
 break;
 case ACTION_MOVE:
 float dy = ev.getY() - initialDown;
 if (Math.abs(dy) > touchSlop) {
 mPreviousY = ev.getY();
 handled = true;
 }
 break;
 }
 
 return handled;
 }
  27. • Computes Velocity in pixels/milliseconds • Used to calculate a

    natural motion VelocityTracker velocityTracker = VelocityTracker.obtain();
 velocityTracker.addMovement(event);
 velocityTracker.computeCurrentVelocity(1);
 velocityTracker.getXVelocity();
 velocityTracker.recycle();
  28. VelocityTracker @Override
 public boolean onTouchEvent(MotionEvent event) {
 switch (event.getActionMasked()) {


    case ACTION_UP:
 velocityTracker.computeCurrentVelocity(1);
 float xVelocity = velocityTracker.getXVelocity();
 float yVelocity = velocityTracker.getYVelocity();
 handleFling(xVelocity, yVelocity);
 break;
 } 
 velocityTracker.addMovement(event);
 
 return super.onTouchEvent(event);
 }
  29. VelocityTracker @Override
 public boolean onTouchEvent(MotionEvent event) {
 switch (event.getActionMasked()) {


    case ACTION_UP:
 velocityTracker.computeCurrentVelocity(1);
 float xVelocity = velocityTracker.getXVelocity();
 float yVelocity = velocityTracker.getYVelocity();
 handleFling(xVelocity, yVelocity);
 break;
 } 
 velocityTracker.addMovement(event);
 
 return super.onTouchEvent(event);
 }
  30. VelocityTracker @Override
 public boolean onTouchEvent(MotionEvent event) {
 switch (event.getActionMasked()) {


    case ACTION_UP:
 velocityTracker.computeCurrentVelocity(1);
 float xVelocity = velocityTracker.getXVelocity();
 float yVelocity = velocityTracker.getYVelocity();
 handleFling(xVelocity, yVelocity);
 break;
 } 
 velocityTracker.addMovement(event);
 
 return super.onTouchEvent(event);
 }
  31. • Natural scrolling behaviour for the platform • Computes values

    • The view has to apply the values Scroller
  32. Scroller private void onFling(float velocityX, float velocityY) { scroller.fling(getScrollX(), getScrollY(),


    (int) -velocityX, (int) -velocityY,
 minScrollX, maxScrollX,
 minScrollY, maxScrollY);
 invalidate();
 }
 
 @Override
 public void computeScroll() {
 super.computeScroll();
 if (!scroller.isFinished()) {
 scroller.computeScrollOffset();
 scrollTo(scroller.getCurrX(), scroller.getCurrY());
 postInvalidateOnAnimation();
 }
 }
  33. Scroller private void onFling(float velocityX, float velocityY) { scroller.fling(getScrollX(), getScrollY(),


    (int) -velocityX, (int) -velocityY,
 minScrollX, maxScrollX,
 minScrollY, maxScrollY);
 invalidate();
 }
 
 @Override
 public void computeScroll() {
 super.computeScroll();
 if (!scroller.isFinished()) {
 scroller.computeScrollOffset();
 scrollTo(scroller.getCurrX(), scroller.getCurrY());
 postInvalidateOnAnimation();
 }
 }
  34. Simple Drawing 
 case MotionEvent.ACTION_DOWN:
 path = new Path();
 path.setLastPoint(event.getX(),

    event.getY());
 break;
 case MotionEvent.ACTION_MOVE:
 path.lineTo(event.getX(), event.getY()); break;
 }
  35. Simple Drawing 
 case MotionEvent.ACTION_DOWN:
 path = new Path();
 path.setLastPoint(event.getX(),

    event.getY());
 break;
 case MotionEvent.ACTION_MOVE:
 path.lineTo(event.getX(), event.getY()); break;
 }
  36. • History of ACTION_MOVE between frames • Useful for timing

    dependent gestures Batching event.getHistorySize() event.getHistoricalX(i) event.getHistoricalY(i)
  37. Simple Drawing 
 case MotionEvent.ACTION_DOWN:
 path = new Path();
 path.setLastPoint(event.getX(),

    event.getY());
 break;
 case MotionEvent.ACTION_MOVE: for (int i = 0; i < event.getHistorySize(); i++) {
 path.lineTo(event.getHistoricalX(i), event.getHistoricalY(i));
 }
 path.lineTo(event.getX(), event.getY()); break;
 }
  38. Simple Drawing 
 case MotionEvent.ACTION_DOWN:
 path = new Path();
 path.setLastPoint(event.getX(),

    event.getY());
 break;
 case MotionEvent.ACTION_MOVE: for (int i = 0; i < event.getHistorySize(); i++) {
 path.lineTo(event.getHistoricalX(i), event.getHistoricalY(i));
 }
 path.lineTo(event.getX(), event.getY()); break;
 }
  39. • A finger is considered a pointer • Each pointer

    has an id • Each MotionEvent contains all pointers Multi-Touch
  40. Focal Point switch (event.getActionMasked()) {
 case ACTION_DOWN:
 pointerId1 = event.getPointerId(0);


    break;
 case ACTION_POINTER_DOWN:
 pointerId2 = event.getPointerId(event.getActionIndex());
 case ACTION_MOVE:
 int index1 = event.findPointerIndex(pointerId1);
 int index2 = event.findPointerIndex(pointerId2);
 if (index1 != -1 && index2 != -1) {
 focalX = (event.getX(index1) + event.getX(index2) / 2f);
 focalY = (event.getY(index1) + event.getY(index2) / 2f);
 }
 break;
 }

  41. Focal Point switch (event.getActionMasked()) {
 case ACTION_DOWN:
 pointerId1 = event.getPointerId(0);


    break;
 case ACTION_POINTER_DOWN:
 pointerId2 = event.getPointerId(event.getActionIndex());
 case ACTION_MOVE:
 int index1 = event.findPointerIndex(pointerId1);
 int index2 = event.findPointerIndex(pointerId2);
 if (index1 != -1 && index2 != -1) {
 focalX = (event.getX(index1) + event.getX(index2) / 2f);
 focalY = (event.getY(index1) + event.getY(index2) / 2f);
 }
 break;
 }

  42. • Accumulates touch events and notifies you • Does the

    hard work for you • Multitouch ready Gesture Detector
  43. Gesture Detector public interface OnGestureListener {
 
 boolean onDown(MotionEvent e);


    
 void onShowPress(MotionEvent e);
 
 boolean onSingleTapUp(MotionEvent e);
 
 boolean onScroll(MotionEvent e1, MotionEvent e2, 
 float distanceX, float distanceY);
 
 void onLongPress(MotionEvent e);
 
 boolean onFling(MotionEvent e1, MotionEvent e2, 
 float velocityX, float velocityY);
 }
  44. ScaleGestureDetectorCompat new ScaleGestureDetector.SimpleOnScaleGestureListener() {
 
 @Override
 public boolean onScaleBegin(ScaleGestureDetector detector)

    {
 return true;
 }
 
 @Override
 public boolean onScale(ScaleGestureDetector detector) {
 detector.getScaleFactor();
 detector.getCurrentSpanX();
 detector.getCurrentSpanY();
 detector.getFocusX();
 detector.getFocusY();
 return true;
 }
 }
  45. • Activity will dispatch MotionEvents to its root layout •

    Viewgroups will dispatch touch to their children Dispatch
  46. • Increase the touch area of a child • Delegates

    a portion of the parent to the child TouchDelegate
  47. TouchDelegate button.post(new Runnable() {
 @Override
 public void run() {
 Rect

    hitRect = new Rect();
 button.getHitRect(hitRect);
 hitRect.inset(-100, -100);
 if (button.getParent() instanceof ViewGroup) {
 ViewGroup parent = (ViewGroup) button.getParent();
 parent.setTouchDelegate(new TouchDelegate(hitRect, button));
 }
 }
 });
  48. button.post(new Runnable() {
 @Override
 public void run() {
 Rect hitRect

    = new Rect();
 button.getHitRect(hitRect);
 hitRect.inset(-100, -100);
 if (button.getParent() instanceof ViewGroup) {
 ViewGroup parent = (ViewGroup) button.getParent();
 parent.setTouchDelegate(new TouchDelegate(hitRect, button));
 }
 }
 }); TouchDelegate
  49. button.post(new Runnable() {
 @Override
 public void run() {
 Rect hitRect

    = new Rect();
 button.getHitRect(hitRect);
 hitRect.inset(-100, -100);
 if (button.getParent() instanceof ViewGroup) {
 ViewGroup parent = (ViewGroup) button.getParent();
 parent.setTouchDelegate(new TouchDelegate(hitRect, button));
 }
 }
 }); TouchDelegate
  50. button.post(new Runnable() {
 @Override
 public void run() {
 Rect hitRect

    = new Rect();
 button.getHitRect(hitRect);
 hitRect.inset(-100, -100);
 if (button.getParent() instanceof ViewGroup) {
 ViewGroup parent = (ViewGroup) button.getParent();
 parent.setTouchDelegate(new TouchDelegate(hitRect, button));
 }
 }
 }); TouchDelegate
  51. button.post(new Runnable() {
 @Override
 public void run() {
 Rect hitRect

    = new Rect();
 button.getHitRect(hitRect);
 hitRect.inset(-100, -100);
 if (button.getParent() instanceof ViewGroup) {
 ViewGroup parent = (ViewGroup) button.getParent();
 parent.setTouchDelegate(new TouchDelegate(hitRect, button));
 }
 }
 }); TouchDelegate
  52. button.post(new Runnable() {
 @Override
 public void run() {
 Rect hitRect

    = new Rect();
 button.getHitRect(hitRect);
 hitRect.inset(-100, -100);
 if (button.getParent() instanceof ViewGroup) {
 ViewGroup parent = (ViewGroup) button.getParent();
 parent.setTouchDelegate(new TouchDelegate(hitRect, button));
 }
 }
 }); TouchDelegate
  53. button.post(new Runnable() {
 @Override
 public void run() {
 Rect hitRect

    = new Rect();
 button.getHitRect(hitRect);
 hitRect.inset(-100, -100);
 if (button.getParent() instanceof ViewGroup) {
 ViewGroup parent = (ViewGroup) button.getParent();
 parent.setTouchDelegate(new TouchDelegate(hitRect, button));
 }
 }
 }); TouchDelegate
  54. • Part of View • Handle Non-Touch Events • Mouse

    Hover • Joystick OnGenericMotionEvent public boolean onGenericMotionEvent(MotionEvent event)
  55. • Touch multiple views at the same time 
 with

    different fingers • On by default • Per layout Split Touch Events <LinearLayout
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:splitMotionEvents="false"
 >
 …
 </LinearLayout>