Как добиться анимации ряби, используя библиотеку поддержки?


171

Я пытаюсь добавить анимацию пульсации при нажатии кнопки. Я сделал, как показано ниже, но это требует 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>

кнопка

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

Я хочу сделать его обратно совместимым с библиотекой дизайна.

Как это можно сделать?

Ответы:


380

Основные настройки пульсации

  • Рябь содержится в представлении.
    android:background="?selectableItemBackground"

  • Рябь, выходящая за границы представления:
    android:background="?selectableItemBackgroundBorderless"

    Посмотрите здесь для разрешения ?(attr)ссылок XML в коде Java.

Библиотека поддержки

  • Использование ?attr:(или ?сокращение) вместо ?android:attrссылки на библиотеку поддержки , поэтому доступно обратно в API 7.

Рябь с изображениями / фонами

  • Чтобы получить изображение или фон с наложением ряби, самое простое решение - обернуть их Viewв FrameLayoutнабор пульсаций с помощью setForeground()или setBackground().

Честно говоря, нет другого способа сделать это иначе.


38
Это не добавляет поддержку пульсации в версии до 21.
AndroidDev

21
Это может не добавить поддержку пульсации, но это решение хорошо ухудшается. Это фактически решило конкретную проблему, с которой я столкнулся. Я хотел волновой эффект на L и простой выбор на предыдущей версии Android.
Дэйв Дженсен

4
@AndroidDev, @Dave Jensen: На самом деле, используя ?attr:вместо ?android:attrссылок библиотеку поддержки v7, которая, если вы используете ее, обеспечивает обратную совместимость с API 7. См. Developer.android.com/tools/support-library/features. html # v7
Бен Де Ла Хэй

14
Что если я тоже хочу иметь цвет фона?
Стэнли Сантозо

9
Эффект Ripple НЕ предназначен для API <21. Ripple - это эффект щелчка дизайна материала. Перспектива Google Design Team не показывает это на устройствах перед леденцом на палочке. у pre-lolipop есть свои собственные эффекты щелчка (по умолчанию светло-синяя обложка). Предложенный ответ предлагает использовать стандартный клик-эффект системы. Если вы хотите настроить цвета эффекта клика, вам нужно сделать drawable и поместить его в res / drawable-v21 для эффекта щелчка пульсации (с <ripple> drawable) и в res / drawable для волновой эффект щелчка (обычно с <selector> отрисовываемым)
nbtk

55

Ранее я голосовал за то, чтобы закрыть этот вопрос как не по теме, но на самом деле я передумал, поскольку это довольно приятный визуальный эффект, который, к сожалению, еще не является частью библиотеки поддержки. Скорее всего, он появится в будущем обновлении, но сроки пока не объявлены.

К счастью, уже есть несколько пользовательских реализаций:

включая тематические наборы виджетов, совместимые со старыми версиями Android:

так что вы можете попробовать один из них или Google для других "виджетов материала" или около того ...


12
Теперь это часть библиотеки поддержки, смотрите мой ответ.
Бен Де Ла Хэй

Спасибо! Я использовал второй lib , первый был слишком медленным в медленных телефонах.
Ферран Майлинч

27

Я сделал простой класс, который делает кнопки пульсации, в конце концов он мне никогда не был нужен, поэтому он не самый лучший, но вот он:

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

РЕДАКТИРОВАТЬ

Так как многие люди ищут что-то подобное, я создал класс, который может заставить другие представления иметь волновой эффект:

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

иначе if (clickListener! = null) {clickListener.onClick (thisRippleView); }
Владимир Кулик,

Простота реализации ... подключи и играй :)
Ранджит Кумар

Я получаю ClassCastException, если я использую этот класс для каждого представления RecyclerView.
Ali_Waris

1
@Ali_Waris В эти дни библиотека поддержки может справляться с рябью, но чтобы исправить это, все, что вам нужно сделать, это вместо использования addRippleToViewдобавления эффекта ряби. Скорее всего, чтобы каждый вид в RecyclerViewаRippleViewCreator
Николас Тайлер

17

Иногда у вас есть собственный фон, в этом случае лучше использовать android:foreground="?selectableItemBackground"


2
Да, но он работает на API> = 23 или на устройствах с 21 API, но только в CardView или FrameLayout
Skullper

17

Это очень просто ;-)

Сначала вы должны создать два нарисованных файла: один для старой версии API, а другой для самой новой версии. Конечно! Если вы создаете файл для рисования для новейшей версии API, Android Studio предлагает автоматически создать старый файл. и, наконец, установите его в фоновом режиме.

Образец drawable для новой версии 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>

Пример drawable для старой версии 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>

Для получения дополнительной информации о рисовании пульсации просто посетите это: https://developer.android.com/reference/android/graphics/drawable/RippleDrawable.html


1
Это действительно очень просто!
Адитья С.

Это решение, безусловно, должно быть гораздо больше проголосовало! Спасибо.
JerabekJakub

0

иногда можно использовать эту строку на любом макете или компонентах.

 android:background="?attr/selectableItemBackground"

Как.

 <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">
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.