如何使用支持库实现水波纹动画?

184

我想在按钮点击时添加涟漪动画。我尝试了下面的代码,但它要求minSdkVersion为21。

ripple.xml

<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="?android:colorControlHighlight">
    <item>
        <shape android:shape="rectangle">
            <solid android:color="?android:colorAccent" />
        </shape>
    </item>
</ripple>
<com.devspark.robototextview.widget.RobotoButton
    android:id="@+id/loginButton"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/ripple"
    android:text="@string/login_button" />

我希望它能够向后兼容设计库。

这该怎么做?

6个回答

404

基本涟漪设置

  • 视图内的涟漪:
    android:background="?selectableItemBackground"

  • 扩展到视图边界之外的涟漪:
    android:background="?selectableItemBackgroundBorderless"

    请参阅此处以解决Java代码中的?(attr) xml引用。

支持库

  • 使用?attr:(或?简写)而不是?android:attr引用支持库,因此可向后兼容到API 7。

带图片/背景的涟漪

  • 要在图像或背景上叠加涟漪,最简单的解决方案是将View包装在FrameLayout中,并将涟漪设置为setForeground()setBackground()

老实说,否则没有更好的方法。


39
这不会为21版本之前的版本添加Ripple支持。 - Johann
21
这个解决方案可能不支持水波纹效果,但是能够很好地降级。它实际上解决了我遇到的特定问题。我想在L版本上使用水波纹效果,在之前的Android版本上只需要一个简单的选择。 - Dave Jensen
4
@AndroidDev, @Dave Jensen:实际上,使用?attr:而不是?android:attr引用了v7支持库,假设您使用它,则可以向后兼容到API 7。请参见:http://developer.android.com/tools/support-library/features.html#v7 - Ben De La Haye
15
如果我也想要背景颜色怎么办? - stanley santoso
10
"Ripple effect is NOT meant to be for API < 21" 的意思是,涟漪效应不适用于 Android 版本低于 21 的 API。涟漪是 Material Design 中的一种点击效果,Google 设计团队的观点是不要在早期版本的设备上显示它。早期版本的设备有自己的点击效果(通常为浅蓝色覆盖),建议使用系统默认的点击效果。如果您想自定义点击效果的颜色,需要创建一个可绘制对象,并将其放置在“res/drawable-v21”目录下以实现涟漪点击效果(使用 <ripple> 可绘制对象),并将其放置在“res/drawable”目录下以实现非涟漪点击效果(通常使用 <selector> 可绘制对象)。" - nbtk
显示剩余7条评论

59

我曾经投票将此问题关闭为不相关的,但实际上我改变了主意,因为这是一种非常不错的视觉效果,不幸的是,它还没有成为支持库的一部分。它很可能会在未来的更新中显示,但目前没有宣布时间表。

幸运的是,已经有一些自定义的实现可用:

其中包括与旧版Android兼容的Material主题小部件集:

所以您可以尝试其中之一或搜索其他“材料小部件”等等...


12
这现在是支持库的一部分,请参考我的回答。 - Ben De La Haye
谢谢!我使用了第二个库,因为第一个在慢手机上太慢了。 - Ferran Maylinch

28

我写了一个简单的类来制作涟漪按钮,但最终我并没有用到它,所以它并不是最好的。不过,这就是它:

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.Button;

public class RippleView extends Button
{
    private float duration = 250;

    private float speed = 1;
    private float radius = 0;
    private Paint paint = new Paint();
    private float endRadius = 0;
    private float rippleX = 0;
    private float rippleY = 0;
    private int width = 0;
    private int height = 0;
    private OnClickListener clickListener = null;
    private Handler handler;
    private int touchAction;
    private RippleView thisRippleView = this;

    public RippleView(Context context)
    {
        this(context, null, 0);
    }

    public RippleView(Context context, AttributeSet attrs)
    {
        this(context, attrs, 0);
    }

    public RippleView(Context context, AttributeSet attrs, int defStyleAttr)
    {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init()
    {
        if (isInEditMode())
            return;

        handler = new Handler();
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(Color.WHITE);
        paint.setAntiAlias(true);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh)
    {
        super.onSizeChanged(w, h, oldw, oldh);
        width = w;
        height = h;
    }

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

        if(radius > 0 && radius < endRadius)
        {
            canvas.drawCircle(rippleX, rippleY, radius, paint);
            if(touchAction == MotionEvent.ACTION_UP)
                invalidate();
        }
    }

    @Override
    public boolean onTouchEvent(@NonNull MotionEvent event)
    {
        rippleX = event.getX();
        rippleY = event.getY();

        switch(event.getAction())
        {
            case MotionEvent.ACTION_UP:
            {
                getParent().requestDisallowInterceptTouchEvent(false);
                touchAction = MotionEvent.ACTION_UP;

                radius = 1;
                endRadius = Math.max(Math.max(Math.max(width - rippleX, rippleX), rippleY), height - rippleY);
                speed = endRadius / duration * 10;
                handler.postDelayed(new Runnable()
                {
                    @Override
                    public void run()
                    {
                        if(radius < endRadius)
                        {
                            radius += speed;
                            paint.setAlpha(90 - (int) (radius / endRadius * 90));
                            handler.postDelayed(this, 1);
                        }
                        else
                        {
                            clickListener.onClick(thisRippleView);
                        }
                    }
                }, 10);
                invalidate();
                break;
            }
            case MotionEvent.ACTION_CANCEL:
            {
                getParent().requestDisallowInterceptTouchEvent(false);
                touchAction = MotionEvent.ACTION_CANCEL;
                radius = 0;
                invalidate();
                break;
            }
            case MotionEvent.ACTION_DOWN:
            {
                getParent().requestDisallowInterceptTouchEvent(true);
                touchAction = MotionEvent.ACTION_UP;
                endRadius = Math.max(Math.max(Math.max(width - rippleX, rippleX), rippleY), height - rippleY);
                paint.setAlpha(90);
                radius = endRadius/4;
                invalidate();
                return true;
            }
            case MotionEvent.ACTION_MOVE:
            {
                if(rippleX < 0 || rippleX > width || rippleY < 0 || rippleY > height)
                {
                    getParent().requestDisallowInterceptTouchEvent(false);
                    touchAction = MotionEvent.ACTION_CANCEL;
                    radius = 0;
                    invalidate();
                    break;
                }
                else
                {
                    touchAction = MotionEvent.ACTION_MOVE;
                    invalidate();
                    return true;
                }
            }
        }

        return false;
    }

    @Override
    public void setOnClickListener(OnClickListener l)
    {
        clickListener = l;
    }
}

编辑

由于许多人正在寻找此类内容,因此我创建了一个类,可以使其他视图具有涟漪效果:

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;

public class RippleViewCreator extends FrameLayout
{
    private float duration = 150;
    private int frameRate = 15;

    private float speed = 1;
    private float radius = 0;
    private Paint paint = new Paint();
    private float endRadius = 0;
    private float rippleX = 0;
    private float rippleY = 0;
    private int width = 0;
    private int height = 0;
    private Handler handler = new Handler();
    private int touchAction;

    public RippleViewCreator(Context context)
    {
        this(context, null, 0);
    }

    public RippleViewCreator(Context context, AttributeSet attrs)
    {
        this(context, attrs, 0);
    }

    public RippleViewCreator(Context context, AttributeSet attrs, int defStyleAttr)
    {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init()
    {
        if (isInEditMode())
            return;

        paint.setStyle(Paint.Style.FILL);
        paint.setColor(getResources().getColor(R.color.control_highlight_color));
        paint.setAntiAlias(true);

        setWillNotDraw(true);
        setDrawingCacheEnabled(true);
        setClickable(true);
    }

    public static void addRippleToView(View v)
    {
        ViewGroup parent = (ViewGroup)v.getParent();
        int index = -1;
        if(parent != null)
        {
            index = parent.indexOfChild(v);
            parent.removeView(v);
        }
        RippleViewCreator rippleViewCreator = new RippleViewCreator(v.getContext());
        rippleViewCreator.setLayoutParams(v.getLayoutParams());
        if(index == -1)
            parent.addView(rippleViewCreator, index);
        else
            parent.addView(rippleViewCreator);
        rippleViewCreator.addView(v);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh)
    {
        super.onSizeChanged(w, h, oldw, oldh);
        width = w;
        height = h;
    }

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

        if(radius > 0 && radius < endRadius)
        {
            canvas.drawCircle(rippleX, rippleY, radius, paint);
            if(touchAction == MotionEvent.ACTION_UP)
                invalidate();
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event)
    {
        return true;
    }

    @Override
    public boolean onTouchEvent(@NonNull MotionEvent event)
    {
        rippleX = event.getX();
        rippleY = event.getY();

        touchAction = event.getAction();
        switch(event.getAction())
        {
            case MotionEvent.ACTION_UP:
            {
                getParent().requestDisallowInterceptTouchEvent(false);

                radius = 1;
                endRadius = Math.max(Math.max(Math.max(width - rippleX, rippleX), rippleY), height - rippleY);
                speed = endRadius / duration * frameRate;
                handler.postDelayed(new Runnable()
                {
                    @Override
                    public void run()
                    {
                        if(radius < endRadius)
                        {
                            radius += speed;
                            paint.setAlpha(90 - (int) (radius / endRadius * 90));
                            handler.postDelayed(this, frameRate);
                        }
                        else if(getChildAt(0) != null)
                        {
                            getChildAt(0).performClick();
                        }
                    }
                }, frameRate);
                break;
            }
            case MotionEvent.ACTION_CANCEL:
            {
                getParent().requestDisallowInterceptTouchEvent(false);
                break;
            }
            case MotionEvent.ACTION_DOWN:
            {
                getParent().requestDisallowInterceptTouchEvent(true);
                endRadius = Math.max(Math.max(Math.max(width - rippleX, rippleX), rippleY), height - rippleY);
                paint.setAlpha(90);
                radius = endRadius/3;
                invalidate();
                return true;
            }
            case MotionEvent.ACTION_MOVE:
            {
                if(rippleX < 0 || rippleX > width || rippleY < 0 || rippleY > height)
                {
                    getParent().requestDisallowInterceptTouchEvent(false);
                    touchAction = MotionEvent.ACTION_CANCEL;
                    break;
                }
                else
                {
                    invalidate();
                    return true;
                }
            }
        }
        invalidate();
        return false;
    }

    @Override
    public final void addView(@NonNull View child, int index, ViewGroup.LayoutParams params)
    {
        //limit one view
        if (getChildCount() > 0)
        {
            throw new IllegalStateException(this.getClass().toString()+" can only have one child.");
        }
        super.addView(child, index, params);
    }
}

否则如果(clickListener != null){ clickListener.onClick(thisRippleView); } - Volodymyr Kulyk
简单易实现...即插即用 :) - Ranjithkumar
如果我在RecyclerView的每个视图上使用这个类,我会得到ClassCastException。 - Ali_Waris
1
@Ali_Waris 现在支持库可以处理涟漪效果,但要解决这个问题,你只需要使用addRippleToView添加涟漪效果,而是将RecyclerView中的每个视图都制作成一个RippleViewCreator - string.Empty

24

非常简单 ;-)

首先,您需要创建两个可绘制文件,一个用于旧的API版本,另一个用于最新的版本。当然!如果您为最新的API版本创建了可绘制文件,Android Studio会自动为您创建旧版本的文件。最后将此可绘制文件设置为您的背景视图。

新API版本的示例可绘制文件(res/drawable-v21/ripple.xml):

<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="?android:colorControlHighlight">
    <item>
        <shape android:shape="rectangle">
            <solid android:color="@color/colorPrimary" />
            <corners android:radius="@dimen/round_corner" />
        </shape>
    </item>
</ripple>

旧版本API的样本可绘制对象 (res/drawable/ripple.xml)

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="@color/colorPrimary" />
    <corners android:radius="@dimen/round_corner" />
</shape>

要了解有关涟漪可绘制的更多信息,请访问此网址:https://developer.android.com/reference/android/graphics/drawable/RippleDrawable.html


1
这真的非常简单! - Aditya
这个解决方案应该得到更多的赞!谢谢。 - JerabekJakub

24
有时候你会有一个自定义的背景,在这种情况下更好的解决方案是使用 android:foreground="?selectableItemBackground"

更新

如果你想要带有圆角和自定义背景的涟漪效果,可以使用以下内容:

background_ripple.xml

<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="@color/ripple_color">
<item android:id="@android:id/mask">
    <shape android:shape="rectangle">
        <solid android:color="@android:color/white" />
        <corners android:radius="5dp" />
    </shape>
</item>

<item android:id="@android:id/background">
    <shape android:shape="rectangle">
        <solid android:color="@android:color/white" />
        <corners android:radius="5dp" />
    </shape>
</item>

布局文件.xml

<Button
    android:id="@+id/btn_play"
    android:background="@drawable/background_ripple"
    app:backgroundTint="@color/colorPrimary"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/play_now" />

我在 API >= 21 上使用过这个


2
是的,但它适用于API >= 23或具有21 API的设备,但仅限于CardView或FrameLayout。 - Skullper
如果您的自定义背景有圆角,那么涟漪效果会看起来很丑。 - Pavel Poley
<color name="ripple_color">#1f000000</color>` 的意思是如果有人需要涟漪颜色。 - Narayanan Ramanathan

3
有时这条线可以在任何布局或组件上使用。
 android:background="?attr/selectableItemBackground"

就像这样。

 <RelativeLayout
                android:id="@+id/relative_ticket_checkin"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:background="?attr/selectableItemBackground">

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接