Comment réaliser une animation ondulée à l'aide de la bibliothèque de support?

171

J'essaye d'ajouter une animation d'ondulation au clic de bouton. J'ai aimé ci-dessous mais cela nécessite 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>

Bouton

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

Je veux le rendre rétrocompatible avec la bibliothèque de conception.

Comment cela peut-il être fait?

N Sharma
la source

Réponses:

380

Configuration de base de l'ondulation

  • Ondulations contenues dans la vue.
    android:background="?selectableItemBackground"

  • Des ondulations qui dépassent les limites de la vue:
    android:background="?selectableItemBackgroundBorderless"

    Jetez un œil ici pour résoudre les ?(attr)références xml dans le code Java.

Bibliothèque de support

  • Utiliser ?attr:(ou le ?raccourci) au lieu de faire ?android:attrréférence à la bibliothèque de support , est donc disponible de nouveau à l'API 7.

Ondulations avec images / arrière-plans

  • Pour avoir une image ou un arrière-plan et une ondulation de superposition, la solution la plus simple consiste à envelopper le Viewdans un FrameLayoutavec l'ondulation définie avec setForeground()ou setBackground().

Honnêtement, il n'y a aucun moyen propre de faire cela autrement.

Ben De La Haye
la source
38
Cela n'ajoute pas la prise en charge des ondulations aux versions antérieures à 21.
AndroidDev
21
Cela n'ajoute peut-être pas de prise en charge des ondulations, mais cette solution se dégrade bien. Cela a en fait résolu le problème particulier que j'avais. Je voulais un effet d'entraînement sur L et une simple sélection sur la version précédente d'Android.
Dave Jensen
4
@AndroidDev, @Dave Jensen: En fait, en utilisant au ?attr:lieu de faire ?android:attrréférence à la bibliothèque de support v7, qui, en supposant que vous l'utilisez, vous offre une rétrocompatibilité avec l'API 7. Voir: developer.android.com/tools/support-library/features. html # v7
Ben De La Haye
14
Et si je veux aussi avoir une couleur d'arrière-plan?
stanley santoso
9
L'effet d'ondulation n'est PAS destiné à l'API <21. L'effet d'ondulation est un effet de clic de la conception matérielle. La perspective de l'équipe de conception de Google ne le montre pas sur les appareils pré-sucette. pré-lolipop ont leurs propres effets de clic (par défaut, couverture bleu clair). La réponse proposée suggère d'utiliser l'effet de clic par défaut du système. Si vous souhaitez personnaliser les couleurs de l'effet de clic, vous devez créer un dessinable et le placer à res / drawable-v21 pour l'effet de clic ondulé (avec le <ripple> drawable), et à res / drawable pour non- effet de clic ondulé (avec <selector> dessinable généralement)
nbtk
55

J'avais auparavant voté pour clore cette question comme hors sujet mais en fait j'ai changé d'avis car c'est un effet visuel assez sympa qui, malheureusement, ne fait pas encore partie de la bibliothèque de support. Il apparaîtra très probablement dans la prochaine mise à jour, mais aucun délai n'est annoncé.

Heureusement, peu d'implémentations personnalisées sont déjà disponibles:

y compris les ensembles de widgets sur le thème du matériel compatibles avec les anciennes versions d'Android:

afin que vous puissiez essayer l'un de ceux-ci ou google pour d'autres "widgets matériels" ou plus ...

Marcin Orlowski
la source
12
Cela fait maintenant partie de la bibliothèque de support, voir ma réponse.
Ben De La Haye
Merci! J'ai utilisé la deuxième bibliothèque , la première était trop lente dans les téléphones lents.
Ferran Maylinch
27

J'ai fait une classe simple qui fait des boutons ondulés, je n'en ai jamais eu besoin à la fin donc ce n'est pas le meilleur, mais la voici:

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

ÉDITER

Comme beaucoup de gens recherchent quelque chose comme ça, j'ai créé une classe qui peut faire en sorte que d'autres vues aient un effet d'entraînement:

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);
    }
}
Nicolas Tyler
la source
else if (clickListener! = null) {clickListener.onClick (thisRippleView); }
Volodymyr Kulyk
Simple à mettre en œuvre ... plug & play :)
Ranjith Kumar
J'obtiens ClassCastException si j'utilise cette classe sur chaque vue d'un RecyclerView.
Ali_Waris
1
@Ali_Waris La bibliothèque de support peut gérer les ondulations de nos jours, mais pour résoudre ce problème, tout ce que vous avez à faire est, au lieu d'utiliser addRippleToViewpour ajouter l'effet d'entraînement. Plutôt faire chaque vue dans le RecyclerViewaRippleViewCreator
Nicolas Tyler
17

Parfois, vous avez un arrière-plan personnalisé, dans ce cas, une meilleure solution est d'utiliser android:foreground="?selectableItemBackground"

Kenny Orellana
la source
2
Oui, mais cela fonctionne sur API> = 23 ou sur les appareils avec 21 API, mais uniquement dans CardView ou FrameLayout
Skullper
17

C'est très simple ;-)

Vous devez d'abord créer deux fichiers pouvant être dessinés, un pour l'ancienne version de l'API et un autre pour la dernière version, bien sûr! si vous créez le fichier dessinable pour la dernière version de l'api, android studio vous suggère de créer automatiquement l'ancien. et définissez enfin ce dessinable sur votre vue d'arrière-plan.

Exemple de dessin pour la nouvelle version de l'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>

Exemple de dessin pour l'ancienne version de l'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>

Pour plus d'informations sur Ripple Drawable, visitez simplement ceci: https://developer.android.com/reference/android/graphics/drawable/RippleDrawable.html

Amintabar
la source
1
C'est vraiment très simple!
Aditya S.
Cette solution devrait être nettement plus votée! Je vous remercie.
JerabekJakub
0

cette ligne sera parfois utilisable sur n'importe quelle mise en page ou composants.

 android:background="?attr/selectableItemBackground"

Comme en.

 <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">
Jatin Mandanka
la source