Come realizzare l'animazione a catena usando la libreria di supporto?


171

Sto cercando di aggiungere un'animazione a catena sul clic del pulsante. Mi è piaciuto di seguito, ma richiede 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>

Pulsante

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

Voglio renderlo retrocompatibile con la libreria di design.

Come si può fare?

Risposte:


380

Impostazione di base dell'ondulazione

  • Increspature contenute nella vista.
    android:background="?selectableItemBackground"

  • Increspature che si estendono oltre i limiti della vista:
    android:background="?selectableItemBackgroundBorderless"

    Dai un'occhiata qui per la risoluzione dei ?(attr)riferimenti XML nel codice Java.

Libreria di supporto

  • Utilizzando ?attr:(o la ?scorciatoia) invece di ?android:attrriferimenti alla libreria di supporto , è quindi disponibile di nuovo all'API 7.

Increspature con immagini / sfondi

  • Per avere un'immagine o uno sfondo e l'ondulazione sovrapposta, la soluzione più semplice è quella di avvolgere Viewin a FrameLayoutcon l'ondulazione impostata con setForeground()o setBackground().

Onestamente non esiste un modo pulito di farlo altrimenti.


38
Questo non aggiunge il supporto dell'ondulazione alle versioni precedenti alla 21.
AndroidDev

21
Potrebbe non aggiungere il supporto per le ondulazioni, ma questa soluzione si degrada bene. Questo in realtà ha risolto il problema particolare che stavo avendo. Volevo un effetto a catena su L e una semplice selezione sulla versione precedente di Android.
Dave Jensen,

4
@AndroidDev, @Dave Jensen: In realtà, usando i riferimenti ?attr:invece della ?android:attrlibreria di supporto v7, che, supponendo che tu lo usi, ti dà retrocompatibilità con l'API 7. Vedi: developer.android.com/tools/support-library/features. html # v7
Ben De La Haye,

14
E se volessi avere anche il colore di sfondo?
Stanley Santoso,

9
L'effetto increspatura NON è pensato per l'API <21. L'increspatura è un effetto clic del design del materiale. La prospettiva del team di progettazione di Google non viene mostrata sui dispositivi pre-lecca-lecca. pre-lolipop ha i propri effetti clic (per impostazione predefinita, copertina azzurra). La risposta offerta suggerisce di utilizzare l'effetto clic predefinito del sistema. Se si desidera personalizzare i colori dell'effetto clic, è necessario creare un disegno e posizionarlo su res / drawable-v21 per l'effetto clic increspatura (con <ripple> disegnabile) e su res / drawable per non- effetto clic a
catena

55

In precedenza ho votato per chiudere questa domanda come fuori tema, ma in realtà ho cambiato idea in quanto si tratta di un bell'effetto visivo che, sfortunatamente, non fa ancora parte della libreria di supporto. Molto probabilmente verrà visualizzato in futuro aggiornamento, ma non è stato annunciato alcun lasso di tempo.

Fortunatamente ci sono già alcune implementazioni personalizzate disponibili:

compresi i set di widget a tema Materlial compatibili con le versioni precedenti di Android:

così puoi provare uno di questi o google per altri "widget materiali" o così ...


12
Questo fa ora parte della libreria di supporto, vedi la mia risposta.
Ben De La Haye,

Grazie! Ho usato la seconda libreria , la prima era troppo lenta nei telefoni lenti.
Ferran Maylinch,

27

Ho fatto una classe semplice che crea pulsanti a catena, alla fine non ne ho mai avuto bisogno, quindi non è il migliore, ma eccolo qui:

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

MODIFICARE

Dato che molte persone sono alla ricerca di qualcosa del genere, ho creato una classe che può far sì che altre viste abbiano l'effetto a catena:

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

else if (clickListener! = null) {clickListener.onClick (thisRippleView); }
Volodymyr Kulyk,

Semplice da implementare ... plug & play :)
Ranjith Kumar,

Ricevo ClassCastException se uso questa classe su ogni vista di RecyclerView.
Ali_Waris

1
@Ali_Waris La libreria di supporto può gestire le increspature in questi giorni, ma per risolvere questo problema tutto ciò che devi fare è, invece di utilizzare addRippleToViewper aggiungere l'effetto a catena. Piuttosto, dai un'occhiata a RecyclerViewaRippleViewCreator
Nicolas Tyler l'

17

A volte hai uno sfondo personalizzato, in tal caso una soluzione migliore è usare android:foreground="?selectableItemBackground"


2
Sì, ma funziona su API> = 23 o su dispositivi con 21 API, ma solo in CardView o FrameLayout
Skullper

17

È molto semplice ;-)

Per prima cosa devi creare due file disegnabili uno per la vecchia versione di API e un altro per la versione più recente, ovviamente! se crei il file disegnabile per la più recente versione di api android studio ti suggerisce di crearne uno vecchio automaticamente. e infine imposta questo disegno sulla tua vista di sfondo.

Esempio di disegno per la nuova versione di 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>

Esempio di disegno per la vecchia versione di 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>

Per maggiori informazioni sul ripple drawable, visita questo: https://developer.android.com/reference/android/graphics/drawable/RippleDrawable.html


1
È davvero molto semplice!
Aditya S.

Questa soluzione dovrebbe essere decisamente molto più votata! Grazie.
JerabekJakub,

0

a volte sarà utilizzabile questa linea su qualsiasi layout o componente.

 android:background="?attr/selectableItemBackground"

Come

 <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">
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.