Everyone has already seen the android cards touch feedback. The selector is drawn in the foreground instead of in the background (as we usually implement it). This effect is in fact pretty simple to implement, and is already implemented in some cases.
Here is a screenshot of the press state effect in the Google Play app:
I. If your view is a FrameLayout
This is the easiest way to add a foreground selector because there is a method for that! Indeed, you just have to pass your selector as android:foreground
in your xml or programmatically, calling setForeground(Drawable)
.
II. If your view is not a FrameLayout
Don’t worry, this is pretty simple. Basically, we just have to set right state to the selector (pressed, focused, etc.), set the bounds and draw it after the view itself. In that way, the selector will be drawn after, and as a consequence, over the view.
Changing the state
In the View
class, a method is called each time the state of the view changes. This method is drawableStateChanged()
(DOC HERE).
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
mForegroundSelector.setState(getDrawableState());
//redraw
invalidate();
}
Updating the drawable bounds
A method is called each time the size of the view changes. This method is onSizeChanged(int, int, int, int)
(DOC HERE).
@Override
protected void onSizeChanged(int width, int height, int oldwidth, int oldheight) {
super.onSizeChanged(width, height, oldwidth, oldheight);
mForegroundSelector.setBounds(0, 0, width, height);
}
Drawing the selector
There is 2 cases:
1. Your view is a not a ViewGroup
The selector has to be drawn after calling onDraw(Canvas canvas)
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mForegroundSelector.draw(canvas);
}
2. Your view is a ViewGroup
The selector has to be drawn after all his children, that means after calling dispatchDraw(Canvas canvas)
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
mForegroundSelector.draw(canvas);
}
Drawing an animated Drawable
If your drawable is animated, there is a bit more to do. Let’s suppose that we have a selector with the attribute android:exitFadeDuration
. That means when the selector changes its state, the old state will fade out.
We first have to move the draw method of the selector from onDraw()
(for views) or dispatchDraw()
(for ViewGroups) to the draw(Canvas)
method, just like this:
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
mForegroundDrawable.draw(canvas);
}
Then we have to Override jumpDrawablesToCurrentState
to indicate our selector to do transition animations between states, and verifyDrawable
to indicate the view we are displaying our own drawable.
@Override
protected boolean verifyDrawable(Drawable who) {
return super.verifyDrawable(who) || (who == mForegroundDrawable);
}
@TargetApi(11)
@Override
public void jumpDrawablesToCurrentState() {
super.jumpDrawablesToCurrentState();
mForegroundDrawable.jumpToCurrentState();
}
But what is doing jumpToCurrentState()? Let’s see a bit of source code, in the DrawableContainer
class.
@Override
public void jumpToCurrentState() {
boolean changed = false;
if (mLastDrawable != null) {
mLastDrawable.jumpToCurrentState();
mLastDrawable = null;
changed = true;
}
if (mCurrDrawable != null) {
mCurrDrawable.jumpToCurrentState();
mCurrDrawable.mutate().setAlpha(mAlpha);
}
if (mExitAnimationEnd != 0) {
mExitAnimationEnd = 0;
changed = true;
}
if (mEnterAnimationEnd != 0) {
mEnterAnimationEnd = 0;
changed = true;
}
if (changed) {
invalidateSelf();
}
}
We can notice that jumpToCurrentState()
calls invalidateSelf()
. And here is the invalidateSelf()
method source code:
/**
* Use the current {@link Callback} implementation to have this Drawable
* redrawn. Does nothing if there is no Callback attached to the
* Drawable.
*
* @see Callback#invalidateDrawable
* @see #getCallback()
* @see #setCallback(android.graphics.drawable.Drawable.Callback)
*/
public void invalidateSelf() {
final Callback callback = getCallback();
if (callback != null) {
callback.invalidateDrawable(this);
}
}
We clearly see that if the callback is not set, the drawable won’t be redrawn. So let’s set a callback when we init our selector.
private void init(Context context) {
mForegroundDrawable = getResources().getDrawable(R.drawable.myselector);
//set a callback, or the selector won't be animated
mForegroundDrawable.setCallback(this);
}
Your selector should fade out now!
Pivot position
If you tried to use a foreground Ripple
drawable, you probably noticed that wherever you tap on your view, the animation will always start from the center.
The easiest way to manage this is to override the drawableHotspotChanged method (Api 21+):
@Override
public void drawableHotspotChanged(float x, float y) {
super.drawableHotspotChanged(x, y);
if (mForegroundDrawable != null) {
mForegroundDrawable.setHotspot(x, y);
}
}
Thanks to +Alex Lockwood for this :)
EXTRA
Retrieve the default background, and set it as the foreground
You can get the default background selector of your theme and set it as your foreground selector if you want:
TypedArray a = getContext().obtainStyledAttributes(new int[]{android.R.attr.selectableItemBackground});
mForegroundDrawable = a.getDrawable(0);
if (mForegroundDrawable != null) {
mForegroundDrawable.setCallback(this);
}
a.recycle();
Conclusion
I made here a basic example of how to add a foreground selector to a custom view, with the main methods. To see a complete implementation (Tests if the drawable is stateful, paddings, etc.), you can look at the source code of FrameLayout.