Android L's Ripple Effect - Touch Feedback for Buttons - Using XML

AndroidXmlAndroid 5.0-LollipopMaterial DesignRippledrawable

Android Problem Overview


I am trying to understand how to implement the "Ripple Effect - Touch Feedback" for buttons and other views. I looked at the questions related to Ripple touch effect on SO and got some insight into it. I was able to successfully get the ripple effect using this java code.

import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RadialGradient;
import android.graphics.Region;
import android.graphics.Shader;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.animation.AccelerateInterpolator;
import android.widget.Button;

public class MyButton extends Button {

	private float mDownX;
	private float mDownY;

	private float mRadius;

	private Paint mPaint;

	public MyButton(final Context context) {
		super(context);
		init();
	}

	public MyButton(final Context context, final AttributeSet attrs) {
		super(context, attrs);
		init();
	}

	public MyButton(final Context context, final AttributeSet attrs,
			final int defStyle) {
		super(context, attrs, defStyle);
		init();
	}

	private void init() {
		mPaint = new Paint();
		mPaint.setAlpha(100);
	}

	@Override
	public boolean onTouchEvent(@NonNull final MotionEvent event) {
		if (event.getActionMasked() == MotionEvent.ACTION_UP) {
			mDownX = event.getX();
			mDownY = event.getY();

			ObjectAnimator animator = ObjectAnimator.ofFloat(this, "radius", 0,
					getWidth() * 3.0f);
			animator.setInterpolator(new AccelerateInterpolator());
			animator.setDuration(400);
			animator.start();
		}
		return super.onTouchEvent(event);
	}

	public void setRadius(final float radius) {
		mRadius = radius;
		if (mRadius > 0) {
			RadialGradient radialGradient = new RadialGradient(mDownX, mDownY,
					mRadius * 3, Color.TRANSPARENT, Color.BLACK,
					Shader.TileMode.MIRROR);
			mPaint.setShader(radialGradient);
		}
		invalidate();
	}

	private Path mPath = new Path();
	private Path mPath2 = new Path();

	@Override
	protected void onDraw(@NonNull final Canvas canvas) {
		super.onDraw(canvas);

		mPath2.reset();
		mPath2.addCircle(mDownX, mDownY, mRadius, Path.Direction.CW);

		canvas.clipPath(mPath2);

		mPath.reset();
		mPath.addCircle(mDownX, mDownY, mRadius / 3, Path.Direction.CW);

		canvas.clipPath(mPath, Region.Op.DIFFERENCE);

		canvas.drawCircle(mDownX, mDownY, mRadius, mPaint);
	}
}

But, i want to use XML approach. How do i achieve this? I have looked at this and this, but i am not yet that comfortable with styles, so i am finding it difficult to achieve the ripple effect.

I have a button with the following XML code:

 <Button
            android:id="@+id/button_email"
            android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_weight="0.50"
            android:gravity="center"
            android:text="@string/email" />

How do i get ripple effect for this button. If someone can guide me, I will be thankful.

[EDIT] Adding ripple.xml and background.xml, as mentioned in one of the links above. I have created a drawable-v21 folder in res and added the below files there.

ripple.xml

<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="@android:color/black" >
    <item android:drawable="@drawable/background">
    </item>
</ripple>

background.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
    <solid android:color="@android:color/darker_gray" />
</shape>

I added the ripple as background for my button, here is the xml for my button now..

<Button
    android:id="@+id/button_email"
    android:layout_width="0dip"
    android:layout_height="wrap_content"
    android:layout_weight="0.50"
    android:gravity="center"
    android:background="@drawable/ripple"
    android:text="@string/email" />

When i run the application i get a ResourceNotFoundException. Here is the logcat trace..

07-21 17:03:39.043: E/AndroidRuntime(15710): FATAL EXCEPTION: main
07-21 17:03:39.043: E/AndroidRuntime(15710): Process: com.xx.xxx, PID: 15710
07-21 17:03:39.043: E/AndroidRuntime(15710): android.view.InflateException: Binary XML file line #60: Error inflating class <unknown>
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.view.LayoutInflater.createView(LayoutInflater.java:620)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at com.android.internal.policy.impl.PhoneLayoutInflater.onCreateView(PhoneLayoutInflater.java:56)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.view.LayoutInflater.onCreateView(LayoutInflater.java:669)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:694)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.view.LayoutInflater.rInflate(LayoutInflater.java:755)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.view.LayoutInflater.rInflate(LayoutInflater.java:758)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.view.LayoutInflater.inflate(LayoutInflater.java:492)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.view.LayoutInflater.inflate(LayoutInflater.java:397)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at com.xx.xxx.BusinessAdapter.onCreateViewHolder(BusinessAdapter.java:106)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at com.xx.xxx.BusinessAdapter.onCreateViewHolder(BusinessAdapter.java:1)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.support.v7.widget.RecyclerView$Adapter.createViewHolder(RecyclerView.java:2915)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:2511)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.support.v7.widget.LinearLayoutManager$RenderState.next(LinearLayoutManager.java:1425)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.support.v7.widget.LinearLayoutManager.fill(LinearLayoutManager.java:999)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.support.v7.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:524)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.support.v7.widget.RecyclerView.dispatchLayout(RecyclerView.java:1461)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.support.v7.widget.RecyclerView.onLayout(RecyclerView.java:1600)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.view.View.layout(View.java:14817)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.view.ViewGroup.layout(ViewGroup.java:4631)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.widget.FrameLayout.onLayout(FrameLayout.java:388)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.view.View.layout(View.java:14817)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.view.ViewGroup.layout(ViewGroup.java:4631)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.widget.FrameLayout.onLayout(FrameLayout.java:388)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.view.View.layout(View.java:14817)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.view.ViewGroup.layout(ViewGroup.java:4631)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at com.android.internal.widget.ActionBarOverlayLayout.onLayout(ActionBarOverlayLayout.java:374)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.view.View.layout(View.java:14817)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.view.ViewGroup.layout(ViewGroup.java:4631)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.widget.FrameLayout.onLayout(FrameLayout.java:388)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.view.View.layout(View.java:14817)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.view.ViewGroup.layout(ViewGroup.java:4631)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:1983)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1740)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:996)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:5600)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.view.Choreographer$CallbackRecord.run(Choreographer.java:761)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.view.Choreographer.doCallbacks(Choreographer.java:574)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.view.Choreographer.doFrame(Choreographer.java:544)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:747)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.os.Handler.handleCallback(Handler.java:733)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.os.Handler.dispatchMessage(Handler.java:95)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.os.Looper.loop(Looper.java:136)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.app.ActivityThread.main(ActivityThread.java:5001)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at java.lang.reflect.Method.invokeNative(Native Method)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at java.lang.reflect.Method.invoke(Method.java:515)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:785)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:601)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at dalvik.system.NativeStart.main(Native Method)
07-21 17:03:39.043: E/AndroidRuntime(15710): Caused by: java.lang.reflect.InvocationTargetException
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at java.lang.reflect.Constructor.constructNative(Native Method)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	at android.view.LayoutInflater.createView(LayoutInflater.java:594)
07-21 17:03:39.043: E/AndroidRuntime(15710): 	... 50 more
07-21 17:03:39.043: E/AndroidRuntime(15710): Caused by: android.content.res.Resources$NotFoundException: Resource is not a Drawable (color or path): TypedValue{t=0x1/d=0x7f020075 a=-1 r=0x

Android Solutions


Solution 1 - Android

UPDATE Material Components:

With the Material Components Library it is very easy to apply a ripple.
Just use the MaterialButton and the app:rippleColor attribute:

<com.google.android.material.button.MaterialButton
    app:rippleColor="@color/my_selector"
    ../>

With a selector like this:

<selector xmlns:android="http://schemas.android.com/apk/res/android">

  <item android:alpha="..." android:color="?attr/colorOnPrimary" android:state_pressed="true"/>
  <item android:alpha="..." android:color="?attr/colorOnPrimary" android:state_focused="true" android:state_hovered="true"/>
  <item android:alpha="..." android:color="?attr/colorOnPrimary" android:state_focused="true"/>
  <item android:alpha="..." android:color="?attr/colorOnPrimary" android:state_hovered="true"/>
  <item android:alpha="..." android:color="?attr/colorOnPrimary"/>

</selector>

Old answer
You can do something like this:

<Button
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:background="@drawable/ripple"
      
    />

Where the ripple.xml is:

<ripple xmlns:android="http://schemas.android.com/apk/res/android" 
                      android:color="?android:colorControlHighlight">
        <item android:id="@android:id/mask">
            <shape android:shape="oval">
                <solid android:color="?android:colorAccent" />
            </shape>
        </item>
 </ripple>

Solution 2 - Android

Just put ?attr/selectableItemBackground in the background of button for API 21+ , like below:

<Button
        android:layout_width="match_parent"
        android:layout_height="70dp"
        android:background="?attr/selectableItemBackground"
        android:text="Button" />

Solution 3 - Android

For lollipop(API>21) make file as btn_ripple_effect.xml in drawable and put below code

<?xml version="1.0" encoding="utf-8"?>

<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:color="?android:colorAccent"
    tools:targetApi="lollipop">
    <item android:drawable="@color/cancel_btn_clr" /> <!-- default -->
    <item android:id="@android:id/mask">
        <shape android:shape="rectangle">
            <solid android:color="?android:colorAccent" />
        </shape>
    </item>
</ripple>

For pre lollipop (API<21)make file as btn_ripple_effect.xml in drawable-v21 folder and put below code

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:state_pressed="true">
        <shape>
            <solid android:color="@color/colorAccent"></solid>
        </shape>
    </item>

    <item>
        <shape>
            <solid android:color="@color/cancel_btn_clr"></solid>
        </shape>
    </item>

</selector>

Solution 4 - Android

Slight addition to above answer: Note that the mask color is not used in any way.

You can do more complicated things with ripple as well. For example, if you wanted a border on your ripple button you can use it like a layer-list.

<ripple
    xmlns:android="http://schemas.android.com/apk/res/android" 
    android:color="?android:colorControlHighlight">

    <!-- Note: <ripple> acts like a layer-list -->
    <item android:id="@android:id/mask">
        <shape android:shape="oval">
            <!-- This color is not displayed in any way -->
            <solid android:color="@android:color/black" />
        </shape>
    </item>

    <!-- This is the border -->
    <item>
        <shape android:shape="rectangle">
            <corners android:radius="3dp"/>
            <!-- Use your border color in place of #f00 -->
            <stroke android:width="1dp" android:color="#f00"/>
        </shape>
    </item>
 </ripple>

Note that the element with id @android:id/mask is only used to show where the ripple effect will stop at. If you wanted it to cover the whole button, you could change the android:shape to be rectangle. You can imagine doing many more interesting things with this as well!

Also make sure to have a backup drawable for devices that aren't 21 yet or the app will crash on old devices.

Solution 5 - Android

The best way to use this in android:foreground, because it allows you use own background also.

>android:foreground="?android:attr/selectableItemBackground"

Example:

<android.support.v7.widget.AppCompatButton
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:foreground="?android:attr/selectableItemBackground"
    android:background="@color/button.normal"
    android:textColor="@color/white"/>

Solution 6 - Android

I was researching ripple effect as it was something I wanted to apply to a few buttons in my application and happened across your post. While your question is searching for an answer as to how to add the ripple effect using XML that was actually something I was trying to avoid as when trying to add that attribute you see it requires v21.

If you are targeting lower than v21 than that new class extending Button (or ImageButton, etc.) will avoid complaints from the compiler.

As there was no explanation on how to implement the custom class above I thought I would fill in. All you need to do is create the new class and then in the XML change "Button" to "the.package.name.MyButton".

From:

 <Button
    android:id="@+id/Button"     

    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

To:

 <the.package.name.MyButton
   android:id="@+id/Button"     

    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

That's it. Now your button when pressed will have a ripple contained within its bounds.

I like this approach I just wish the ripple would extend pass the bounds. For a small button this ripple effect really highlights how square or rectangular the button really is. Visually it would be more satisfying if the ripple just continued until it reached its full radius.

Solution 7 - Android

You can add clickable as true and background or foreround as ?attr/selectableItemBackground attributes to the view:

<Button
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:text="Button"
     android:clickable="true"
     android:background="?attr/selectableItemBackground"
     android:textColor="@android:color/white"/>

If in case your view already has a background filled with something, you could fill your foreground with selectableItemBackground

<Button
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:text="Button"
     android:clickable="true"
     android:foreground="?attr/selectableItemBackground"
     android:background="@color/colorPrimary"
     android:textColor="@android:color/white"/>

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionVamsi ChallaView Question on Stackoverflow
Solution 1 - AndroidGabriele MariottiView Answer on Stackoverflow
Solution 2 - AndroidPacific P. RegmiView Answer on Stackoverflow
Solution 3 - Androiduser5439728View Answer on Stackoverflow
Solution 4 - AndroidWilliamView Answer on Stackoverflow
Solution 5 - AndroidMd Imran ChoudhuryView Answer on Stackoverflow
Solution 6 - AndroidJason MarksView Answer on Stackoverflow
Solution 7 - AndroidbackslashNView Answer on Stackoverflow