¿Cómo lograr la animación ondulada usando la biblioteca de soporte?


171

Estoy tratando de agregar una animación ondulada al hacer clic en el botón. Me gustó a continuación, pero requiere minSdKVersion a 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>

Botón

<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" />

Quiero que sea compatible con versiones anteriores de la biblioteca de diseño.

¿Cómo se puede hacer esto?

Respuestas:


380

Configuración básica de ondulación

  • Ondulaciones contenidas dentro de la vista.
    android:background="?selectableItemBackground"

  • Ondulaciones que se extienden más allá de los límites de la vista:
    android:background="?selectableItemBackgroundBorderless"

    Eche un vistazo aquí para resolver ?(attr)referencias xml en código Java.

Biblioteca de soporte

  • El uso ?attr:(o la ?abreviatura) en lugar de ?android:attrreferencias a la biblioteca de soporte , por lo que está disponible de nuevo a API 7.

Ondas con imágenes / fondos

  • Para tener una imagen o fondo y una superposición de ondas, la solución más fácil es envolver Viewen una FrameLayoutcon la ondulación establecida con setForeground()o setBackground().

Honestamente, no hay una manera limpia de hacer esto de otra manera.


38
Esto no agrega soporte de ondulación a versiones anteriores a 21.
AndroidDev

21
Es posible que no agregue soporte de ondulación, pero esta solución se degrada muy bien. Esto realmente resolvió el problema particular que estaba teniendo. Quería un efecto dominó en L y una selección simple en la versión anterior de Android.
Dave Jensen

44
@AndroidDev, @Dave Jensen: en realidad, usando la biblioteca de soporte v7 en ?attr:lugar de ?android:attrreferencias, la cual, suponiendo que la use, le da compatibilidad con la API 7. Ver: developer.android.com/tools/support-library/features. html # v7
Ben De La Haye

14
¿Qué pasa si también quiero tener color de fondo?
Stanley Santos

9
El efecto de ondulación NO está destinado a API <21. El efecto de ondulación es un efecto de clic del diseño de material. La perspectiva del equipo de diseño de Google no se muestra en dispositivos pre-lollipop. pre-lolipop tiene sus propios efectos de clic (el valor predeterminado es la cubierta azul claro). La respuesta ofrecida sugiere utilizar el efecto de clic predeterminado del sistema. Si desea personalizar los colores del efecto de clic, debe hacer un dibujo y colocarlo en res / drawable-v21 para el efecto de clic de ondulación (con el <ripple> dibujable), y en res / drawable para no efecto de clic de ondulación (con <selector> dibujable generalmente)
nbtk

55

Anteriormente voté para cerrar esta pregunta como fuera de tema, pero en realidad cambié de opinión ya que este es un efecto visual bastante agradable que, desafortunadamente, aún no forma parte de la biblioteca de soporte. Lo más probable es que aparezca en futuras actualizaciones, pero no hay un calendario anunciado.

Afortunadamente, hay algunas implementaciones personalizadas disponibles:

incluidos los conjuntos de widgets temáticos de Materlial compatibles con versiones anteriores de Android:

para que pueda probar uno de estos o google para otros "widgets materiales" o algo así ...


12
Esto ahora es parte de la biblioteca de soporte, vea mi respuesta.
Ben De La Haye

¡Gracias! Usé la segunda lib , la primera era demasiado lenta en teléfonos lentos.
Ferran Maylinch

27

Hice una clase simple que hace botones ondulados, nunca la necesité al final, así que no es la mejor, pero aquí está:

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;
    }
}

EDITAR

Como muchas personas buscan algo como esto, hice una clase que puede hacer que otras vistas tengan el efecto dominó:

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);
    }
}

si no (clickListener! = null) {clickListener.onClick (thisRippleView); }
Volodymyr Kulyk

Fácil de implementar ... plug & play :)
Ranjith Kumar

Recibo ClassCastException si uso esta clase en cada vista de un RecyclerView.
Ali_Waris

1
@Ali_Waris La biblioteca de soporte puede lidiar con las ondas en estos días, pero para solucionar esto, todo lo que tiene que hacer es, en lugar de usar addRippleToViewpara agregar el efecto de onda. Más bien haga que cada vista en la RecyclerViewaRippleViewCreator
Nicolas Tyler

17

A veces tienes un fondo personalizado, en esos casos, una mejor solución es usar android:foreground="?selectableItemBackground"


2
Sí, pero funciona en API> = 23 o en dispositivos con 21 API, pero solo en CardView o FrameLayout
Skullper

17

Es muy simple ;-)

Primero debe crear dos archivos dibujables, uno para la versión antigua de la API y otro para la versión más reciente, ¡por supuesto! si crea el archivo dibujable para la versión más reciente de api, android studio le sugiere que cree uno antiguo automáticamente. y finalmente establezca este dibujable en su vista de fondo.

Muestra dibujable para la nueva versión de la 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>

Muestra dibujable para la versión anterior de la 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>

Para obtener más información sobre Ripple Drawable, solo visite: https://developer.android.com/reference/android/graphics/drawable/RippleDrawable.html


1
¡Es realmente muy simple!
Aditya S.

¡Esta solución definitivamente debería ser mucho más votada! Gracias.
JerabekJakub

0

a veces se puede usar esta línea en cualquier diseño o componente.

 android:background="?attr/selectableItemBackground"

Como.

 <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">
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.