Làm thế nào để đạt được hiệu ứng gợn bằng thư viện hỗ trợ?


171

Tôi đang cố gắng để thêm một hình ảnh động gợn lên trên nút bấm. Tôi đã làm như dưới đây nhưng nó yêu cầu minSdKVersion đến 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>

Cái nút

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

Tôi muốn làm cho nó tương thích ngược với thư viện thiết kế.

Làm thế nào điều này có thể được thực hiện?

Câu trả lời:


380

Thiết lập gợn cơ bản

  • Gợn sóng chứa trong chế độ xem.
    android:background="?selectableItemBackground"

  • Các gợn sóng vượt ra ngoài giới hạn của chế độ xem:
    android:background="?selectableItemBackgroundBorderless"

    Có một cái nhìn ở đây để giải quyết các ?(attr)tham chiếu xml trong mã Java.

Thư viện hỗ trợ

  • Sử dụng ?attr:(hoặc tốc ?ký) thay vì ?android:attrtham chiếu thư viện hỗ trợ , do đó, có sẵn trở lại API 7.

Gợn sóng với hình ảnh / hình nền

  • Để có một hình ảnh hoặc nền và lớp phủ gợn, giải pháp đơn giản nhất là bọc Viewtrong một FrameLayoutbộ gợn bằng setForeground()hoặc setBackground().

Thành thật mà nói, không có cách nào sạch sẽ để làm điều này nếu không.


38
Điều này không thêm hỗ trợ Ripple cho các phiên bản trước 21.
AndroidDev

21
Nó có thể không thêm hỗ trợ gợn nhưng giải pháp này xuống cấp độc đáo. Điều này thực sự giải quyết vấn đề cụ thể mà tôi đang có. Tôi muốn có một hiệu ứng gợn trên L và một lựa chọn đơn giản trên phiên bản Android trước.
Dave Jensen

4
@AndroidDev, @Dave Jensen: Trên thực tế, bằng cách sử dụng ?attr:thay vì ?android:attrtham chiếu thư viện hỗ trợ v7, giả sử bạn sử dụng nó, mang lại cho bạn khả năng tương thích ngược với API 7. Xem: developer.android.com/tools/support-l
Ben De La Haye

14
Nếu tôi cũng muốn có màu nền thì sao?
stanley santoso

9
Hiệu ứng Ripple KHÔNG có nghĩa là dành cho API <21. Ripple là hiệu ứng nhấp chuột của thiết kế vật liệu. Phối cảnh Google Design Team không hiển thị trên các thiết bị tiền kẹo. pre-lolipop có hiệu ứng nhấp chuột riêng (mặc định là bìa màu xanh nhạt). Câu trả lời được cung cấp gợi ý sử dụng hiệu ứng nhấp mặc định của hệ thống. Nếu bạn muốn tùy chỉnh màu sắc của hiệu ứng nhấp chuột, bạn cần tạo một drawable và đặt nó ở độ phân giải / drawable-v21 cho hiệu ứng nhấp chuột gợn (với <ripple> drawable) và tại res / drawable cho không hiệu ứng nhấp chuột gợn (với <selector> drawable thường)
nbtk

55

Trước đây tôi đã bỏ phiếu để đóng câu hỏi này ngoài chủ đề nhưng thực sự tôi đã thay đổi suy nghĩ của mình vì đây là hiệu ứng hình ảnh khá đẹp mà thật không may, nó chưa phải là một phần của thư viện hỗ trợ. Nó rất có thể sẽ xuất hiện trong bản cập nhật trong tương lai, nhưng không có khung thời gian nào được công bố.

May mắn thay, có một vài triển khai tùy chỉnh đã có sẵn:

bao gồm các bộ tiện ích theo chủ đề Materlial tương thích với các phiên bản Android cũ hơn:

vì vậy bạn có thể thử một trong những thứ này hoặc google cho các "vật liệu" khác hoặc ...


12
Đây là một phần của thư viện hỗ trợ, xem câu trả lời của tôi.
Ben De La Haye

Cảm ơn! Tôi đã sử dụng lib thứ hai , đầu tiên là quá chậm trong điện thoại chậm.
Ferran Maylinch

27

Tôi đã tạo một lớp đơn giản để tạo các nút gợn, cuối cùng tôi không bao giờ cần nó vì vậy nó không phải là tốt nhất, nhưng đây là:

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

BIÊN TẬP

Vì nhiều người đang tìm kiếm thứ gì đó như thế này, tôi đã tạo ra một lớp có thể khiến các khung nhìn khác có hiệu ứng gợn:

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 if (clickListener! = null) {clickListener.onClick (thisRípView); }
Volodymyr Kulyk

Đơn giản để thực hiện ... cắm và chơi :)
Ranjith Kumar

Tôi đang nhận ClassCastException nếu tôi sử dụng lớp này qua mỗi chế độ xem của RecyclerView.
Ali_Waris

1
@Ali_Waris Thư viện hỗ trợ có thể xử lý các gợn sóng ngày nay nhưng để khắc phục điều này, tất cả những gì bạn phải làm là, thay vì sử dụng addRippleToViewđể thêm hiệu ứng gợn. Thay vì thực hiện mỗi chế độ xem trong RecyclerViewaRippleViewCreator
Nicolas Tyler

17

Đôi khi bạn có một nền tùy chỉnh, trong trường hợp đó, một giải pháp tốt hơn là sử dụng android:foreground="?selectableItemBackground"


2
Có, nhưng nó hoạt động trên API> = 23 hoặc trên các thiết bị có 21 API, nhưng chỉ trong CardView hoặc FrameLayout
Skullper

17

Nó rất đơn giản ;-)

Trước tiên, bạn phải tạo hai tệp có thể vẽ một cho phiên bản api cũ và một tệp khác cho phiên bản mới nhất, Tất nhiên! Nếu bạn tạo tập tin có thể vẽ cho phiên bản api mới nhất, android sẽ đề nghị bạn tự động tạo tập tin cũ. và cuối cùng thiết lập drawable này để xem nền của bạn.

Mẫu có thể vẽ cho phiên bản api mới (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>

Mẫu có thể vẽ cho phiên bản api cũ (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>

Để biết thêm thông tin về Ripple drawable, chỉ cần truy cập vào đây: https://developer.android.com/reference/android/graphics/drawable/RípDrawable.html


1
Nó thực sự rất đơn giản!
Aditya S.

Giải pháp này chắc chắn sẽ được nâng cao hơn nhiều! Cảm ơn bạn.
JerabekJakub

0

đôi khi sẽ sử dụng dòng này trên bất kỳ bố cục hoặc thành phần nào.

 android:background="?attr/selectableItemBackground"

Giống như là.

 <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">
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.