Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ripple effect on top of Image - Android

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?

like image 550
Jawascript Avatar asked Jan 08 '15 18:01

Jawascript


People also ask

What is ripple effect in Android?

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.

What is ripple color Android?

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.

How can I give an ImageView click effect like a button on Android?

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.


3 Answers

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.

like image 99
Simon Avatar answered Oct 06 '22 10:10

Simon


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"/>
like image 32
ajpolt Avatar answered Oct 06 '22 10:10

ajpolt


Maybe try one of these solutions (got the tip from here) :

  1. 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);
    
  2. 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);
        }
    }
    
like image 3
android developer Avatar answered Oct 06 '22 09:10

android developer