지원 라이브러리를 사용하여 리플 애니메이션을 얻는 방법은 무엇입니까?


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"

    Java 코드에서 XML 참조 를 해결 하려면 여기 를 살펴보십시오 ?(attr).

지원 라이브러리

  • 사용 ?attr:(또는 ?대신 속기) ?android:attr참조합니다 지원 라이브러리를 , 그래서 API 7 사용할 수 돌아왔다.

이미지 / 배경이있는 잔물결

  • 이미지 또는 배경과 오버레이 리플을 갖기 위해 가장 쉬운 해결책은 또는로 설정된 리플을 사용하여 를 감싸는 것 View입니다 .FrameLayoutsetForeground()setBackground()

솔직히 그렇지 않으면 이것을하는 확실한 방법이 없습니다.


38
21 이전 버전에는 리플 지원이 추가되지 않습니다.
AndroidDev

21
잔물결 지원을 추가하지 않을 수도 있지만이 솔루션은 훌륭하게 저하됩니다. 이것은 실제로 내가 가진 특정 문제를 해결했습니다. L에 대한 파급 효과와 이전 버전의 Android에서 간단한 선택을 원했습니다.
Dave Jensen

4
@AndroidDev, @Dave Jensen : 실제로 v7 지원 라이브러리 ?attr:?android:attr참조 대신 사용하면이를 사용한다고 가정하면 API 7에 대한 하위 호환성을 제공합니다. developer.android.com/tools/support-library/features를
Ben De La Haye

14
배경색도 갖고 싶다면 어떻게해야합니까?
stanley santoso

9
잔물결 효과는 API <21을위한 것이 아닙니다. 잔물결은 재료 디자인의 클릭 효과입니다. 롤리팝 이전 기기에는 Google 디자인 팀의 관점이 표시되지 않습니다. 사전 롤리팝에는 자체 클릭 효과가 있습니다 (기본값은 연한 파란색 표지). 제공된 답변은 시스템의 기본 클릭 효과를 사용하도록 제안합니다. 클릭 효과의 색상을 사용자 정의하려면 드로어 블을 만들어 res / drawable-v21에 배치하고 리플 클릭 효과 (<ripple> 드로어 블 포함)를 위해 res / drawable-v21에 배치하고, 리플 클릭 효과 (일반적으로 <selector> 드로어 블 사용)
nbtk

55

나는 이전 에이 질문을 주 제외로 닫으려고 투표했지만 실제로 불행히도 아직 지원 라이브러리의 일부가 아닌 매우 훌륭한 시각 효과이기 때문에 마음을 바꿨습니다. 향후 업데이트에 표시 될 가능성이 높지만 발표 된 시간은 없습니다.

다행히도 사용 가능한 사용자 지정 구현은 거의 없습니다.

이전 버전의 Android와 호환되는 Materlial 테마 위젯 세트 포함 :

그래서 당신은 다른 "재료 위젯"등을 위해 이것들 또는 구글 중 하나를 시도 할 수 있습니다 ...


12
이것은 이제 지원 라이브러리의 일부입니다. 제 답변을 참조하십시오.
Ben De La Haye

감사! 나는 두 번째 lib를 사용했다 . 첫 번째는 느린 전화에서 너무 느렸다.
Ferran Maylinch

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

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

간단한 구현 ... 플러그 앤 플레이 :)
Ranjith Kumar

RecyclerView의 각보기 에서이 클래스를 사용하면 ClassCastException이 발생합니다.
Ali_Waris

1
@Ali_Waris 요즘에는 지원 라이브러리에서 잔물결을 처리 할 수 ​​있지만이 문제를 해결 addRippleToView하려면 잔물결 효과를 추가하는 대신 사용 하면됩니다. 오히려 각 뷰하게 RecyclerViewRippleViewCreator
니콜라스 타일러

17

때로는 사용자 정의 배경이 있습니다.이 경우 더 나은 솔루션이 사용됩니다. android:foreground="?selectableItemBackground"


2
예, 그러나 API> = 23 또는 21 API를 가진 장치에서 작동하지만 CardView 또는 FrameLayout
Skullper

17

매우 간단합니다 ;-)

먼저, 이전 API 버전 용 파일 하나와 최신 버전 용 파일 하나를 만들어야합니다. 물론! 최신 API 버전 Android Studio 용 드로어 블 파일을 만들면 오래된 파일을 자동으로 만들 것을 제안합니다. 마지막 으로이 드로어 블을 배경보기로 설정하십시오.

새 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>

이전 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
정말 간단합니다!
Aditya S.

이 솔루션은 확실히 더 많이지지되어야합니다! 감사합니다.
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">
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.