Wie kann eine Ripple-Animation mithilfe der Support-Bibliothek erzielt werden?


171

Ich versuche, beim Klicken auf die Schaltfläche eine Wellenanimation hinzuzufügen. Ich mochte unten, aber es erfordert minSdKVersion bis 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>

Taste

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

Ich möchte es abwärtskompatibel mit der Designbibliothek machen.

Wie kann das gemacht werden?

Antworten:


380

Grundlegendes Ripple-Setup

  • In der Ansicht enthaltene Wellen.
    android:background="?selectableItemBackground"

  • Wellen, die über die Grenzen der Ansicht hinausgehen:
    android:background="?selectableItemBackgroundBorderless"

    Werfen Sie einen Blick hier für die Lösung ?(attr)xml Referenzen in Java - Code.

Support-Bibliothek

  • Die Verwendung ?attr:(oder der ?Kurzform) anstelle von ?android:attrVerweisen auf die Unterstützungsbibliothek ist also wieder für API 7 verfügbar.

Wellen mit Bildern / Hintergründen

  • Um ein Bild oder einen Hintergrund und eine überlagerte Welligkeit zu erhalten, ist es am einfachsten, die mit oder eingestellte Welligkeit Viewin a zu wickeln .FrameLayoutsetForeground()setBackground()

Ehrlich gesagt gibt es keinen sauberen Weg, dies anders zu machen.


38
Dies fügt Versionen vor 21 keine Ripple-Unterstützung hinzu.
AndroidDev

21
Möglicherweise wird keine Welligkeitsunterstützung hinzugefügt, aber diese Lösung verschlechtert sich gut. Dies löste tatsächlich das spezielle Problem, das ich hatte. Ich wollte einen Ripple-Effekt auf L und eine einfache Auswahl auf früheren Android-Versionen.
Dave Jensen

4
@AndroidDev, @Dave Jensen: Wenn Sie ?attr:anstelle von ?android:attrVerweisen die v7-Unterstützungsbibliothek verwenden, erhalten Sie, sofern Sie sie verwenden, eine Abwärtskompatibilität mit API 7. Siehe: developer.android.com/tools/support-library/features. html # v7
Ben De La Haye

14
Was ist, wenn ich auch Hintergrundfarbe haben möchte?
Stanley Santoso

9
Der Ripple-Effekt ist NICHT für API <21 gedacht. Der Ripple-Effekt ist ein Klick-Effekt des Materialdesigns. Die Perspektive des Google Design Teams wird auf Pre-Lollipop-Geräten nicht angezeigt. Pre-Lolipop haben ihre eigenen Klickeffekte (standardmäßig hellblaue Abdeckung). Die angebotene Antwort schlägt vor, den Standard-Klickeffekt des Systems zu verwenden. Wenn Sie die Farben des Klick-Effekts anpassen möchten, müssen Sie ein Zeichenelement erstellen und es bei res / drawable-v21 für den Ripple-Klick-Effekt (mit dem Zeichen <ripple>) und bei res / drawable für Nicht-Klick-Effekte platzieren. Ripple-Click-Effekt (mit <selector> normalerweise zeichnbar)
nbtk

55

Früher habe ich dafür gestimmt, diese Frage als nicht zum Thema gehörend zu schließen, aber tatsächlich habe ich meine Meinung geändert, da dies ein sehr schöner visueller Effekt ist, der leider noch nicht Teil der Support-Bibliothek ist. Es wird höchstwahrscheinlich in zukünftigen Updates angezeigt, aber es wird kein Zeitrahmen angekündigt.

Glücklicherweise sind bereits einige benutzerdefinierte Implementierungen verfügbar:

einschließlich Materetial-Widget-Sets, die mit älteren Android-Versionen kompatibel sind:

Sie können also eines davon ausprobieren oder bei Google nach anderen "Material-Widgets" suchen oder so ...


12
Dies ist jetzt Teil der Support-Bibliothek, siehe meine Antwort.
Ben De La Haye

Vielen Dank! Ich habe die zweite Bibliothek verwendet , die erste war bei langsamen Telefonen zu langsam.
Ferran Maylinch

27

Ich habe eine einfache Klasse gemacht, die Ripple-Buttons macht. Ich habe sie am Ende nie gebraucht, also ist sie nicht die beste. Aber hier ist sie:

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

BEARBEITEN

Da viele Leute nach so etwas suchen, habe ich eine Klasse erstellt, in der andere Ansichten den Welleneffekt haben können:

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

sonst wenn (clickListener! = null) {clickListener.onClick (thisRippleView); }
Volodymyr Kulyk

Einfach zu implementieren ... Plug & Play :)
Ranjith Kumar

Ich erhalte eine ClassCastException, wenn ich diese Klasse für jede Ansicht einer RecyclerView verwende.
Ali_Waris

1
@Ali_Waris Die Support-Bibliothek kann heutzutage mit Wellen umgehen, aber um dies zu beheben, müssen Sie nur den Welleneffekt addRippleToViewhinzufügen , anstatt ihn zu verwenden . Machen Sie lieber jede Ansicht in der RecyclerViewaRippleViewCreator
Nicolas Tyler

17

Manchmal haben Sie einen benutzerdefinierten Hintergrund. In diesem Fall wird eine bessere Lösung verwendet android:foreground="?selectableItemBackground"


2
Ja, aber es funktioniert auf API> = 23 oder auf Geräten mit 21 API, aber nur in CardView oder FrameLayout
Skullper

17

Es ist sehr einfach ;-)

Zuerst müssen Sie zwei zeichnbare Dateien erstellen, eine für die alte API-Version und eine für die neueste Version. Natürlich! Wenn Sie die zeichnbare Datei für die neueste API-Version von Android Studio erstellen, empfehlen wir Ihnen, die alte automatisch zu erstellen. und stellen Sie dieses Zeichen schließlich auf Ihre Hintergrundansicht ein.

Beispiel für eine neue API-Version (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>

Beispiel für eine alte API-Version (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>

Weitere Informationen zu Ripple Drawable finden Sie unter: https://developer.android.com/reference/android/graphics/drawable/RippleDrawable.html


1
Es ist wirklich sehr einfach!
Aditya S.

Diese Lösung sollte definitiv viel besser bewertet werden! Danke dir.
JerabekJakub

0

Manchmal kann diese Zeile für jedes Layout oder jede Komponente verwendet werden.

 android:background="?attr/selectableItemBackground"

Wie als.

 <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">
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.