DEV Community

loading...
Cover image for Android Vitals - Tap Response Time 👉

Android Vitals - Tap Response Time 👉

pyricau profile image Py ⚔ Updated on ・4 min read

Header image: Alone Together by Romain Guy.

Android users expect apps to respond to their actions within a short time window.

💡 Did you know?

UX research teaches us that a response time shorter than 100ms feels immediate, and a response time beyond 1s makes users lose focus. When the response time gets closer to 10 seconds, users simply abandon their task (source).

👉📱

Measuring user action response times is critical to ensure a good user experience. Taps are the most common action apps must respond to. Can we measure Tap Response Time?

🎓 Tap Response Time

The Tap Response Time is the time from when the user is done pressing a button to when the app has visibly reacted to the tap.
More precisely, it's the time from when the finger leaves the touch screen to when the display has rendered a frame with a visible reaction to that tap (e.g. the start of a navigation animation). The Tap Response Time does not include any animation time.

Naive Tap Response Time

I opened the Navigation Advanced Sample project and added a call to measureTimeMillis() to measure the Tap Response Time when tapping on the about button.

aboutButton.setOnClickListener {
  val tapResponseTimeMs = measureTimeMillis {
    findNavController().navigate(R.id.action_title_to_about)
  }
  PerfAnalytics.logTapResponseTime(tapResponseTimeMs)
}
Enter fullscreen mode Exit fullscreen mode

Simple enough! However this approach presents several drawbacks:

  • ⌛️ It can return a negative time.
  • 📈 It doesn't scale with the codebase size.
  • 👉 It doesn't account for the time from when the finger leaves the touch screen to when the click listener is called.
  • 📱 It doesn't account for the time from when we're done calling NavController.navigate() to when the display has rendered a frame with the new screen visible.

⌛️ Negative time

measureTimeMillis() calls System.currentTimeMillis() whihch can be set by the user or the phone network, so the time may jump backwards or forwards unpredictably. Elapsed time measurements should not use System.currentTimeMillis() (learn more: Android Vitals - What time is it?
).

📈 Large codebases

Adding measuring code to every single meaningful click listener is a daunting task. We need a solution that scales with the codebase size, which means we need central hooks to detect when meaningful actions are triggered by taps.

👉 Touch pipeline

When a finger leaves the touch screen, the following happens:

  • The system_server process receives the information from the touch screen and determines which window should receive a MotionEvent.UP touch event.

💡 Did you know?

Every window is associated with an input event socket pair: the first socket is owned by system_server to send input events. That first socket is paired with a second socket owned by the app that created the window, to receive input events.

  • The system_server process sends the touch event to the input event socket for the targeted window.
  • The app receives the touch event on its listening socket, stores it in a queue (ViewRootImpl.QueuedInputEvent) and schedules a Choreographer frame to consume input events.

💡 Did you know?

The system_server process detects when an input event stays in the queue for more than 5 seconds, and that's when it knows it should show an Application Not Responding (ANR) dialog.

  • When the Choreographer frame triggers, the touch event is dispatched to the root view of a window, which then dispatches it through its view hierarchy.
  • The tapped view receives the MotionEvent.UP touch event and posts a click listener callback (source). This lets other visual state of the view update before click actions start.
  • Then finally when the main thread runs that posted callback the view click listener is called.

As you can see, quite a lot happens from when the finger leaves the touch screen to when the click listener is called. Every motion event includes the time at which the event occurred (MotionEvent.getEventTime()). If we could get access to that MotionEvent.UP event leading to a click we could measure the true start of the Tap Response Time.

📱 Traversal and rendering

What does this do?

findNavController().navigate(R.id.action_title_to_about)
Enter fullscreen mode Exit fullscreen mode
  • In most apps, the above code starts a fragment transaction. That transaction may be immediate (commitNow()) or posted (commit()).
  • When the transaction execute, the view hierarchy is updated and a layout traversal is scheduled.
  • When the layout traversal executes, a new frame is drawn into a surface.
  • It is then composited with frames from other windows and sent to the display. To learn more, watch this talk from Romain Guy and Chet Haase: Drawn out: How Android renders.

Ideally we'd like to know exactly when a change to the view hierarchy becomes actually visible on the display. Unfortunately, as far as I know there's no Java API for that, so we'll have to get creative.

🤔 What now?

We've seen that the naive approach to measuring Tap Response Time isn't accurate and doesn't scale. We need to build something better!

The good news is, I've implemented exactly that, on top of Jetpack Navigation. The bad news is, you'll have to wait for me to publish Part 2!. The even better news is, you can read about it in Part 2: Tap Response Time: Jetpack Navigation 🗺.

⚖️ This work is licensed under a Creative Commons Attribution 4.0 International License.

Discussion (1)

pic
Editor guide
Collapse
mr3ytheprogrammer profile image
M R 3 Y

Thanks, Py! I was always curious to learn about motion events/listeners internals