Understanding Android Views and Gestures

Caren Chang explains how setOnClickListener() and setOnLongClickListener() methods work, and examines how view hierarchy is laid out in Android!


Introduction

My motivations for exploring Views in depth started when I worked at June, which made a smart phone app for a smart oven. Writing UI for a smart oven is different from writing UI for the phone because the experience should feel more like an oven as opposed to a phone.

Other apps, such as Periscope, utilize custom touches to create a better user experience. Understanding how Android handles Views and custom gestures will help you implement these features.

How Android Layout Views

Views drawn on Android do not follow a traditional X, Y coordinate system. A view knows where it is relative to the screen, and where the touches should be sent based on the top left corner, along with the width and height.

cheng-android-view-coordinate

When a User Touches a View

Every touch on the screen gets translated into a MotionEvent. The MotionEvent encapsulates an action code and axis values. The action code entails where a user put their finger down, up, or around the screen. Axis values specify where exactly the MotionEvent happened, or where exactly the user touched on the screen.

MotionEvent

Under our example, the blue is the screen and the red dot is where the user touched. This is what it would like as encapsulated MotionEvent:

Get more development news like this

android-motion-touch-cheng

getAction() -> ACTION_DOWN
getX() -> 100
getyY() -> 120

When a touch occurs, Android sends it to the appropriate Views that it thinks should handle the touch. Then Views themselves can decide if they want to handle this MotionEvent or pass it along to another view.

boolean onTouchEvent()

An important method to note is the onTouchEvent. This method is called whenever a touch needs to be handled - it returns true if the view that calls onTouchEvent handles the touch.

Let’s create a custom view that handles these touch events.

When we want to handle different MotionEvents, it’s important to distinguish them. Here, we use a switch statement.


public class CustomView extends View {
    @Override
    public boolean onTouchEvent(MotionEvent motionEvent) {
        super.onTouchEvent(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;
    }
}

Creating a Custom ‘onClick’ Listener

What constitutes a click is when a user’s finger touches the screen to when they lift their finger.


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) {
        showClickedToast();
    } else if (totalTimeElapsed < longClickTimeoutMs) {
        showLongClickedToast();
    } else {
        Log.i("touch", "click action timed out");
    }

A click is some certain amount of time before the finger lifts off the screen, and a long click is some certain amount of time longer than that.

We want to measure how long it takes before a user lifts their finger. Based on this, we could just evaluate whether we should show OnClick event or we should do a LongClick event.


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) {
    showClickedToast();
    } else if (totalTimeElapsed < longClickTimeoutMs) {
    showLongClickedToast();
    } else {
    Log.i("touch", "click action timed out");
    }

The View now has to tell Android that it is interested in these events. You want to return true on onTouchEvent, but this is not ideal. Because when you return true on a touch event, all the other views that are laid on top of this view are not going to get any touch events because it’s all getting intercepted.

Instead, you want to return whether this touch is in the view itself.


public class CustomView extends View {

    private boolean isTouchInView(MotionEvent motionEvent) {

        int[] location = new int[2];

        getLocationOnScreen(location);

    return motionEvent.getX() > location[0]

            && motionEvent.getX() < location[0] + getWidth()

            && motionEvent.getY() > location[1]

            && motionEvent.getY() < location[1] + getHeight();
}


Here, the helper method getLocationOnScreen gives the location of the top left corner of the view. Based on that, we can determine whether the MotionEvent was in the interested view. But this was not ideal as well.


public class CustomView extends View {

    private boolean isTouchInView(MotionEvent motionEvent) {

        int[] location = new int[2];

        getLocationOnScreen(location);

    return motionEvent.getRawX() > location[0]

        && motionEvent.getRawX() < location[0] + getWidth()
        && motionEvent.getRawY() > location[1]
        && motionEvent.getRawY() < location[1] + getHeight();

}

Instead, you want to use getRawX, and getRawY instead of get X or get Y. The reason you want to do this is because when MotionEvents are passed down from view to view, the events get translated each time.

Swipe Down to Dismiss

Many apps use the swipe down to dismiss. To implement this, first, you want to detect where the finger started on the screen. We capture the initial Y coordinate so that we can know if the user moved down, to the side, or up.

If the user lifts their finger while it was still in the view, we then calculate the difference between the initial Y and ending Y, and if it’s greater than a specific range, we start the animation to set the view down.


@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) {
            startAnimation(slide_down);
            return true;
        }
    }

    return super.onTouchEvent(motionEvent);

}

boolean dispatchTouchEvent()

Another method to consider when we do touches and views is the dispatchTouchEvent. dispatchTouchEvent passes a MotionEvent to the target view and returns true if it’s handled by the current view and false otherwise. And it can be used to listen to events, spy on it and do stuff accordingly.

For every MotionEvent, dispatchTouchEvent is called from the top of the tree to the bottom of the tree, which means every single view will call dispatchTouchEvent if nothing else is intercepted.

I created a GitHub project where I logged everything to see what would happen if I did a press. I changed it wherever it returned true, e.g. onTouchEvent, and see how that affected the other views and did the opposite to see the other result. I think the only way to understand how these views and touches work is by playing it by yourself.

Things to Consider When Customizing Behavior

  • It is very complicated, prone to bugs. Android has given us GestureDetector, which handles most of the common gestures that we would care about, e.g. tap or double tap or scroll.

  • If you do want to customize stuff, there’s a view configuration class that has a lot of the standard constants that Android uses.

  • You should consider multitouch. Everything we talked about so far is just considering that we’re using just one finger on the screen.

In Summary

Custom touch events are very complicated. But, on the other hand, all touch events on Android are packaged into a MotionEvent, and it’s passed down the view hierarchy until an interested event wants to handle it. As such, view can be intercepted and redirected based on what you think should happen or what you think will happen.

Next Up: Architecture Patterns in the Realm SDK

General link arrow white
`

Caren Chang

Caren is an Android developer working at June, where they currently build an intelligent oven that will help everybody become great cooks. You can find her on twitter at @calren24