One of the most commonly overlooked parts of building Android applications is touch feedback. While some views like Buttons have touch feedback built into them, thats not the case when using custom views.
The Android Design Guidelines explicitly talk about the UX part of touch feedback and it was also discussed a while back on the Android Design In Action YouTube series:
The problem is that there are no clean standard hooks in the Android framework to implement these. The only implementations I have found have been:
- FrameLayouts have a foreground property that can be used
- ViewGroups like the ListView that extend the AbsListView class have a property called “drawSelectorOnTop” that can be used to draw the background selector on top of your custom item renderer.
Given the lack of default apis in most of the common layouts, one approach that you jump to (like I did) was to listen to touch events and change background on touch down and up. This is hard to implement perfectly because a touch event can be canceled in a bunch of ways. For example, a user may be swiping across your view and you may still end up showing touch feedback which would be wrong. In our app, we often ended up with views stuck in an incorrect state.
The Android framework’s selector system (or rather the StateListDrawable class) handles a bunch of these situations appropriately already. The best approach therefore is to create a selector and the apply that to your view. Setting that selector as your view’s background though will draw that under all the views on top which doesn’t work either. To fix that, the best approach is overriding the dispatchDraw method and then after calling super, calling your new drawable’s draw method within the bounds of the View, something like:
@Override
protected void dispatchDraw(Canvas canvas){
super.dispatchDraw(canvas);
touchFeedbackDrawable.setBounds(0,0,getWidth(), getHeight());
touchFeedbackDrawable.draw(canvas);
}
The one gotcha here is that the base ViewGroup does not invalidate custom drawable invalidated automatically, so this is something you have to do by overriding a different method:
@Override
protected void drawableStateChanged() {
if (touchFeedbackDrawable != null) {
touchFeedbackDrawable.setState(getDrawableState());
invalidate();
}
super.drawableStateChanged();
}
Props to Rich Ratmansky for solving this quirk.
The final result looks something like this:
I have created a sample project to show the experience. You can find the project on Github. This works well, but as you can see it does require a custom ViewGroup class. If anyone from the Android framework team is reading this, having a “touchFeedbackDrawable” property would probably be really useful extension to the framework.
[Update] Had an interesting discussion on this on Google+, whether touch feedback should be shown in a view’s background or foreground. The consensus seems to be background as long as the background is fairly prominent/visible. For opaque content like say images, foreground feedback is pretty much required.
Thanks for this page.
Instead of using a selector with state_pressed, you could also call invalidate() within setPressed() and check isPressed() within dispatchDraw() to draw the overlay.
LikeLike