วิธีการบรรลุ Ripple animation โดยใช้ห้องสมุดสนับสนุน?


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

ห้องสมุดสนับสนุน

ระลอกด้วยรูปภาพ / พื้นหลัง

  • จะมีภาพหรือพื้นหลังและซ้อนทับระลอกทางออกที่ง่ายที่สุดคือการห่อ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
Ben De La Haye

14
ถ้าฉันต้องการมีสีพื้นหลังด้วยล่ะ
santoso สแตนลี่ย์

9
Ripple effect ไม่ได้มีไว้สำหรับ API <21 Ripple เป็นเอฟเฟกต์คลิกของการออกแบบวัสดุ มุมมองของทีมออกแบบของ Google จะไม่แสดงในอุปกรณ์พรีอมอมยิ้ม pre-lolipop มีเอฟเฟกต์การคลิกของตัวเอง (ค่าเริ่มต้นไปที่ปกสีฟ้าอ่อน) คำตอบที่เสนอจะแนะนำให้ใช้เอฟเฟกต์คลิกเริ่มต้นของระบบ หากคุณต้องการปรับแต่งสีของเอฟเฟกต์การคลิกคุณต้องทำการวาดได้และวางไว้ที่ res / drawable-v21 สำหรับเอฟเฟกต์การคลิกระลอก (ด้วย <ripple> drawable) และที่ res / drawable สำหรับที่ไม่ใช่ - เอฟเฟกต์การกระเพื่อมคลิก (โดยมี <selector> drawable ปกติ)
nbtk

55

ก่อนหน้านี้ฉันโหวตให้ปิดคำถามนี้เป็นนอกหัวข้อ แต่จริง ๆ แล้วฉันเปลี่ยนใจเพราะนี่เป็นเอฟเฟ็กต์ภาพที่ค่อนข้างดีซึ่งน่าเสียดายที่ยังไม่ได้เป็นส่วนหนึ่งของห้องสมุดสนับสนุน มันน่าจะปรากฏในการอัพเดทในอนาคต แต่ไม่มีการประกาศกรอบเวลา

โชคดีที่มีการปรับใช้แบบกำหนดเองสองสามตัวที่มีอยู่แล้ว:

รวมถึงชุดเครื่องมือธีม Materlial เข้ากันได้กับ Android รุ่นเก่า:

ดังนั้นคุณสามารถลองใช้หนึ่งในเหล่านี้หรือ google สำหรับ "เครื่องมือวัสดุ" อื่น ๆ หรือมากกว่านั้น ...


12
นี่เป็นส่วนหนึ่งของห้องสมุดสนับสนุนแล้วดูคำตอบของฉัน
เบ็นเดอลาเฮย์

ขอบคุณ! ฉันใช้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);
    }
}

อื่นถ้า (clickListener! = null) {clickListener.onClick (thisRippleView); }
Volodymyr Kulyk

ง่ายต่อการติดตั้ง ... plug & play :)
Ranjith Kumar

ฉันได้รับ ClassCastException ถ้าฉันใช้คลาสนี้ในแต่ละมุมมองของ RecyclerView
Ali_Waris

1
@Ali_Waris ไลบรารีการสนับสนุนสามารถจัดการกับ ripples วันนี้ แต่การแก้ไขสิ่งที่คุณต้องทำคือแทนที่จะใช้addRippleToViewเพื่อเพิ่มลักษณะพิเศษของ ripple แต่ทำในแต่ละมุมมองในRecyclerViewRippleViewCreator
นิโคลัสไทเลอร์

17

บางครั้งคุณมีพื้นหลังที่กำหนดเองในกรณีนั้นจะใช้วิธีแก้ปัญหาที่ดีกว่า android:foreground="?selectableItemBackground"


2
ใช่ แต่ใช้งานได้กับ API> = 23 หรือบนอุปกรณ์ที่มี 21 API แต่เฉพาะใน CardView หรือ FrameLayout
Skullper

17

ง่ายมาก ;-)

ก่อนอื่นคุณต้องสร้างไฟล์สองไฟล์ที่สามารถวาดได้หนึ่งไฟล์สำหรับเวอร์ชัน api เก่าและอีกไฟล์หนึ่งเป็นเวอร์ชั่นใหม่ล่าสุดแน่นอน! หากคุณสร้างไฟล์ drawable สำหรับ api รุ่นใหม่ล่าสุด android studio ขอแนะนำให้คุณสร้างไฟล์เก่าโดยอัตโนมัติ และสุดท้ายตั้งค่า drawable นี้เป็นมุมมองพื้นหลังของคุณ

ตัวอย่าง 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
มันง่ายมากจริงๆ!
Aditya S.

โซลูชันนี้ควร upvoted มากขึ้นแน่นอน! ขอบคุณ.
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.