Como obter animação ripple usando a biblioteca de suporte?


171

Estou tentando adicionar uma animação de ondulação ao clicar no botão. Eu gostei abaixo, mas requer minSdKVersion para 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ão

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

Quero torná-lo compatível com a biblioteca de design.

Como isso pode ser feito?

Respostas:


380

Configuração básica de ondulação

  • Ondulações contidas na visualização.
    android:background="?selectableItemBackground"

  • Ondulações que se estendem além dos limites da vista:
    android:background="?selectableItemBackgroundBorderless"

    Dê uma olhada aqui para resolver ?(attr)referências xml no código Java.

Biblioteca de Suporte

  • Usar ?attr:(ou ?abreviar) em vez de ?android:attrreferenciar a biblioteca de suporte , está disponível novamente na API 7.

Ondulações com imagens / fundos

  • Para ter uma imagem ou plano de fundo e sobreposição de ondulação, a solução mais fácil é agrupar o Viewem um FrameLayoutcom a ondulação definida com setForeground()ou setBackground().

Honestamente, não há uma maneira limpa de fazer isso de outra maneira.


38
Isso não adicionar suporte ondulação de versões anteriores ao 21.
AndroidDev

21
Pode não adicionar suporte a ondulações, mas esta solução é prejudicada. Isso realmente resolveu o problema específico que eu estava tendo. Eu queria um efeito cascata em L e uma seleção simples na versão anterior do Android.
Dave Jensen

4
@AndroidDev, @Dave Jensen: Na verdade, usando o em ?attr:vez de fazer ?android:attrreferência à biblioteca de suporte da v7, que, supondo que você o use, oferece compatibilidade retroativa com a API 7. Consulte: developer.android.com/tools/support-library/features. html # v7
Ben De La Haye

14
E se eu também quiser ter a cor de fundo?
Stanley Santoso

9
O efeito cascata NÃO se destina à API <21. O cascata é um efeito de clique do design do material. A perspectiva da equipe de design do Google não é mostrada em dispositivos pré-pirulito. o pré-lolipop tem seus próprios efeitos de clique (o padrão é capa azul claro). A resposta oferecida sugere usar o efeito de clique padrão do sistema. Se você deseja personalizar as cores do efeito de clique, é necessário criar um drawable e colocá-lo em res / drawable-v21 para o efeito de clique ripple (com o drawable <ripple>) e em res / drawable para não- efeito clique ondulação (com <selector> drawable geralmente)
nbtk

55

Anteriormente, votei para encerrar esta questão como fora de tópico, mas, na verdade, mudei de idéia, pois é um efeito visual bastante agradável que, infelizmente, ainda não faz parte da biblioteca de suporte. Provavelmente aparecerá em atualizações futuras, mas não há prazo anunciado.

Felizmente, existem poucas implementações personalizadas já disponíveis:

incluindo conjuntos de widgets com temas Materlial compatíveis com versões mais antigas do Android:

para que você possa experimentar um desses ou o google para outros "widgets de material" ou mais ...


12
Agora isso faz parte da biblioteca de suporte, veja minha resposta.
Ben De La Haye

Obrigado! Eu usei a segunda lib , a primeira era muito lenta em telefones lentos.
Ferran Maylinch 25/11

27

Eu fiz uma classe simples que cria botões de ondulação, eu nunca precisei dela no final, então não é a melhor, mas aqui 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 muitas pessoas estão procurando algo assim, criei uma classe que pode fazer com que outras visualizações tenham o efeito cascata:

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

caso contrário, if (clickListener! = null) {clickListener.onClick (thisRippleView); }
Volodymyr Kulyk 28/09/16

Simples de implementar ... plug & play :)
Ranjith Kumar

Estou recebendo ClassCastException se eu usar essa classe em cada exibição de um RecyclerView.
81117 Ali_Waris

1
@Ali_Waris A biblioteca de suporte pode lidar com ondulações hoje em dia, mas para corrigir isso, tudo o que você precisa fazer é, em vez de usar addRippleToViewpara adicionar o efeito cascata. Sim fazer cada exibição no RecyclerViewumRippleViewCreator
Nicolas Tyler

17

Às vezes, você tem um plano de fundo personalizado; nesses casos, uma solução melhor é usar android:foreground="?selectableItemBackground"


2
Sim, mas ele funciona em API> = 23 ou em dispositivos com 21 API, mas apenas em CardView ou FrameLayout
Skullper

17

É muito simples ;-)

Primeiro, você deve criar dois arquivos desenháveis, um para a versão antiga da API e outro para a versão mais recente, é claro! Se você criar o arquivo extraível para a versão mais recente da API, o Android Studio sugere que você crie um antigo automaticamente. e, finalmente, defina esse desenho para sua visualização em segundo plano.

Exemplo de drawable para a nova versão da 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>

Exemplo de drawable para a versão antiga da 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 obter mais informações sobre o ripple drawable, visite: https://developer.android.com/reference/android/graphics/drawable/RippleDrawable.html


1
É realmente muito simples!
Aditya S.

Esta solução deve ser definitivamente muito mais votada! Obrigado.
JerabekJakub

0

Às vezes, essa linha pode ser usada em qualquer layout ou 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">
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.