I've been experimenting with the ripple animation in my latest side project. I'm having some trouble finding an "elegant" solution to using it in certain situations for touch events. Namely with images, especially in list, grid, and recycle views. The animation almost always seems to animate behind the view, not the on top of it. This is a none issue in Buttons and TextViews but if you have a GridView of images, the ripple appears behind or below the actual image. Obviously this is not what I want, and while there are solutions that I consider to be a work around, I'm hoping there is something simpler i'm just unaware of.
I use the following code to achieve a custom grid view with images. I'll give full code CLICK HERE so you can follow along if you choose.
Now just the important stuff. In order to get my image to animate on touch I need this
button_ripple.xml
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/cream_background">
<item>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Pressed -->
<item
android:drawable="@color/button_selected"
android:state_pressed="true"/>
<!-- Selected -->
<item
android:drawable="@color/button_selected"
android:state_selected="true"/>
<!-- Focus -->
<item
android:drawable="@color/button_selected"
android:state_focused="true"/>
<!-- Default -->
<item android:drawable="@color/transparent"/>
</selector>
</item>
</ripple>
custom_grid.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="horizontal">
<ImageView
android:id="@+id/sceneGridItem"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/button_ripple"
android:gravity="center_horizontal"/>
</LinearLayout>
activity_main.xml
<GridView
android:id="@+id/sceneGrid"
android:layout_marginTop="15dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:verticalSpacing="15dp"
android:numColumns="5" />
The line where all magic and problems occur is when I set the background. While this does in fact give me a ripple animation on my imageview, it animates behind the imageview. I want the animation to appear on top of the image. So I tried a few different things like
Setting the entire grid background to button_ripple.
<GridView
android:id="@+id/sceneGrid"
android:layout_marginTop="15dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:verticalSpacing="15dp"
android:background="@drawable/button_ripple"
android:numColumns="5" />
It does exactly what you'd think, now the entire grid has a semi transparent background and no matter what image i press the entire grid animates from the center of the grid. While this is kind of cool, its not what I want.
Setting the root/parent background to button_ripple.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:background="@drawable/button_ripple"
android:orientation="horizontal">
The area is now larger and fills the entire cell of the grid (not just the image), however it doesn't bring it to the front.
Changing custom_grid.xml to a RelativeLayout and putting two ImageViews on top of each other
custom_grid.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/gridItem"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true" />
<ImageView
android:id="@+id/gridItemOverlay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:background="@drawable/button_ripple" />
</RelativeLayout>
CustomGridAdapter.java
....
gridItemOverLay = (ImageView) findViewById(R.id.gridItemOverlay);
gridItemOverlay.bringToFront();
This works. Now the bottom ImageView contains my image, and the top animates, giving the illusion of a ripple animation on top of my image. Honestly though this is a work around. I feel like this is not how it was intended. So I ask you fine people, is there a better way or even a different way?
The touch feedback in Android is a must whenever the user clicks on the item or button ripple effect when clicking on the same, gives confidence to the user that the button has been clicked so that they can wait for the next interaction of the app.
As you've noticed, ripples are used subtle indications of touch feedback, hence why they do not use colorPrimary or colorAccent by default. This is consistent with the changes made in Android 4.4 (Kitkat) which made the default selector colors neutral by default.
You can do this with a single image using something like this: //get the image view ImageView imageView = (ImageView)findViewById(R. id. ImageView); //set the ontouch listener imageView.
I liked android developer's answer so I decided to investigate how to do step 2 of his solution in code.
You need to get this piece of code from Jake Wharton here : https://gist.github.com/JakeWharton/0a251d67649305d84e8a
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.ImageView;
public class ForegroundImageView extends ImageView {
private Drawable foreground;
public ForegroundImageView(Context context) {
this(context, null);
}
public ForegroundImageView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ForegroundImageView);
Drawable foreground = a.getDrawable(R.styleable.ForegroundImageView_android_foreground);
if (foreground != null) {
setForeground(foreground);
}
a.recycle();
}
/**
* Supply a drawable resource that is to be rendered on top of all of the child
* views in the frame layout.
*
* @param drawableResId The drawable resource to be drawn on top of the children.
*/
public void setForegroundResource(int drawableResId) {
setForeground(getContext().getResources().getDrawable(drawableResId));
}
/**
* Supply a Drawable that is to be rendered on top of all of the child
* views in the frame layout.
*
* @param drawable The Drawable to be drawn on top of the children.
*/
public void setForeground(Drawable drawable) {
if (foreground == drawable) {
return;
}
if (foreground != null) {
foreground.setCallback(null);
unscheduleDrawable(foreground);
}
foreground = drawable;
if (drawable != null) {
drawable.setCallback(this);
if (drawable.isStateful()) {
drawable.setState(getDrawableState());
}
}
requestLayout();
invalidate();
}
@Override protected boolean verifyDrawable(Drawable who) {
return super.verifyDrawable(who) || who == foreground;
}
@Override public void jumpDrawablesToCurrentState() {
super.jumpDrawablesToCurrentState();
if (foreground != null) foreground.jumpToCurrentState();
}
@Override protected void drawableStateChanged() {
super.drawableStateChanged();
if (foreground != null && foreground.isStateful()) {
foreground.setState(getDrawableState());
}
}
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (foreground != null) {
foreground.setBounds(0, 0, getMeasuredWidth(), getMeasuredHeight());
invalidate();
}
}
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (foreground != null) {
foreground.setBounds(0, 0, w, h);
invalidate();
}
}
@Override public void draw(Canvas canvas) {
super.draw(canvas);
if (foreground != null) {
foreground.draw(canvas);
}
}
}
This is the attrs.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ForegroundImageView">
<attr name="android:foreground"/>
</declare-styleable>
</resources>
Now, create your ForegroundImageView like so in your layout.xml:
<com.example.ripples.ForegroundImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:foreground="?android:selectableItemBackground"
android:src="@drawable/apples"
android:id="@+id/image" />
The image will now ripple.
Instead of trying to add the ripple to each individual view in the adapter, you can just add it at the GridView level like this:
<GridView
android:id="@+id/gridview"
...
android:drawSelectorOnTop="true"
android:listSelector="@drawable/your_ripple_drawable"/>
Maybe try one of these solutions (got the tip from here) :
Wrap the drawable in a RippleDrawable
² before setting it on the ImageView
:
Drawable image = …
RippleDrawable rippledImage = new RippleDrawable(
ColorStateList.valueOf(rippleColor), image, null);
imageView.setImageDrawable(rippledImage);
Extend ImageView
and add a foreground attribute to it (like FrameLayout
has³). See this example⁴ from +Chris Banes of adding it to a LinearLayout
. If you do this then make sure you pass through the touch co-ordinates so that the ripple starts from the correct point:
@Override
public void drawableHotspotChanged(float x, float y) {
super.drawableHotspotChanged(x, y);
if (foreground != null) {
foreground.setHotspot(x, y);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With