Jak uzyskać animację tętnienia za pomocą biblioteki wsparcia?


171

Próbuję dodać animację tętnienia po kliknięciu przycisku. Podobało mi się poniżej, ale wymaga minSdKVersion do 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>

Przycisk

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

Chcę, aby był on wstecznie zgodny z biblioteką projektów.

Jak to zrobić?

Odpowiedzi:


380

Podstawowa konfiguracja tętnienia

  • Echa zawarte w widoku.
    android:background="?selectableItemBackground"

  • Fale, które wykraczają poza granice widoku:
    android:background="?selectableItemBackgroundBorderless"

    Zajrzyj tutaj, aby znaleźć rozwiązanie ?(attr)odwołań xml w kodzie Java.

Biblioteka wsparcia

  • Używanie ?attr:(lub ?skrótu) zamiast ?android:attrodwołań do biblioteki obsługi , więc jest dostępne z powrotem do API 7.

Echa z obrazami / tłami

  • Aby mieć obraz lub tło i nakładające się ripple, najłatwiejszym rozwiązaniem jest zawinięcie Vieww a FrameLayoutz falowaniem ustawionym za pomocą setForeground()lub setBackground().

Szczerze mówiąc, inaczej nie można tego zrobić.


38
Nie dodaje to obsługi ripple do wersji wcześniejszych niż 21.
AndroidDev

21
Może nie dodawać wsparcia dla ripple, ale to rozwiązanie ładnie się degraduje. To faktycznie rozwiązało konkretny problem, który miałem. Chciałem uzyskać efekt tętnienia na L i prosty wybór we wcześniejszej wersji Androida.
Dave Jensen

4
@AndroidDev, @Dave Jensen: Właściwie, używając ?attr:zamiast ?android:attrodniesień biblioteki obsługi wersji 7, która, zakładając, że jej używasz, zapewnia wsteczną kompatybilność z API 7. Zobacz: developer.android.com/tools/support-library/features. html # v7
Ben De La Haye

14
A jeśli chcę mieć również kolor tła?
stanley santoso

9
Efekt Ripple NIE jest przeznaczony dla API <21. Ripple jest efektem kliknięcia materiału. Perspektywa zespołu projektowego Google nie jest wyświetlana na urządzeniach sprzed wersji Lollipop. pre-lolipop mają własne efekty kliknięcia (domyślnie jasnoniebieska okładka). Zaproponowana odpowiedź sugeruje użycie domyślnego efektu kliknięcia systemu. Jeśli chcesz dostosować kolory efektu kliknięcia, musisz utworzyć rysunek do rysowania i umieścić go w rozdzielczości res / drawable-v21, aby uzyskać efekt kliknięcia ripple (z rysunkiem <ripple> drawable) i res / drawable dla non- efekt kliknięcia ripple (zwykle z możliwością rysowania <selector>)
nbtk

55

Wcześniej głosowałem za zamknięciem tego pytania jako nie na temat, ale tak naprawdę zmieniłem zdanie, ponieważ jest to całkiem niezły efekt wizualny, który niestety nie jest jeszcze częścią biblioteki wsparcia. Najprawdopodobniej pojawi się w przyszłej aktualizacji, ale nie podano ram czasowych.

Na szczęście dostępnych jest już kilka niestandardowych implementacji:

w tym zestawy widżetów o tematyce Materlial zgodne ze starszymi wersjami Androida:

więc możesz wypróbować jeden z tych lub Google, aby znaleźć inne „materialne widżety” lub coś takiego ...


12
To jest teraz część biblioteki wsparcia, zobacz moją odpowiedź.
Ben De La Haye

Dzięki! Użyłem drugiej biblioteki , pierwsza była zbyt wolna w wolnych telefonach.
Ferran Maylinch

27

Zrobiłem prostą klasę, która tworzy przyciski ripple, ostatecznie nigdy jej nie potrzebowałem, więc nie jest najlepsza, ale oto jest:

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

EDYTOWAĆ

Ponieważ wielu ludzi szuka czegoś takiego, stworzyłem klasę, która może sprawić, że inne widoki będą miały efekt falowania:

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

Prosty do wdrożenia ... plug & play :)
Ranjith Kumar

Otrzymuję ClassCastException, jeśli używam tej klasy w każdym widoku RecyclerView.
Ali_Waris

1
@Ali_Waris Biblioteka wsparcia może obecnie radzić sobie z falami, ale aby to naprawić, wystarczy, że zamiast addRippleToViewdodawać efekt ripple. Raczej zrób każdy widok w RecyclerViewaRippleViewCreator
Nicolas Tyler

17

Czasami masz niestandardowe tło, w takich przypadkach lepszym rozwiązaniem jest użycie android:foreground="?selectableItemBackground"


2
Tak, ale działa na API> = 23 lub na urządzeniach z 21 API, ale tylko w CardView lub FrameLayout
Skullper

17

To jest bardzo proste ;-)

Najpierw musisz utworzyć dwa pliki do rysowania, jeden dla starej wersji interfejsu API, a drugi dla najnowszej wersji. Oczywiście! jeśli utworzysz plik do rysowania dla najnowszej wersji interfejsu API Android Studio sugeruje automatyczne utworzenie starego. i wreszcie ustaw to do rysowania na swój widok tła.

Przykład do rysowania dla nowej wersji 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>

Próbka do rysowania dla starej wersji 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>

Aby uzyskać więcej informacji na temat ripple drawable, odwiedź tę stronę: https://developer.android.com/reference/android/graphics/drawable/RippleDrawable.html


1
To naprawdę bardzo proste!
Aditya S.

To rozwiązanie powinno być zdecydowanie bardziej przychylne! Dziękuję Ci.
JerabekJakub

0

czasami będzie można użyć tej linii w dowolnym układzie lub komponentach.

 android:background="?attr/selectableItemBackground"

Tak jak.

 <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">
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.