【问题标题】:How to achieve ripple animation using support library?如何使用支持库实现波纹动画?
【发布时间】:2014-12-23 14:38:11
【问题描述】:

我正在尝试在按钮单击时添加波纹动画。我确实喜欢下面,但它需要 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" />

我想让它向后兼容设计库。

如何做到这一点?

【问题讨论】:

    标签: android material-design


    【解决方案1】:

    基本波纹设置

    • 视图中包含的涟漪。
      android:background="?selectableItemBackground"

    • 超出视图边界的涟漪:
      android:background="?selectableItemBackgroundBorderless"

      查看 here 以解析 Java 代码中的 ?(attr) xml 引用。

    支持库

    • 使用?attr:(或? 简写)而不是?android:attr 引用support library,因此可返回到API 7。

    带有图像/背景的涟漪

    • 要拥有图像或背景并叠加波纹,最简单的解决方案是将View 包裹在FrameLayout 中,波纹设置为setForeground()setBackground()

    老实说,否则没有干净的方法可以做到这一点。

    【讨论】:

    • 这不会为 21 之前的版本添加波纹支持。
    • 它可能不会添加波纹支持,但此解决方案会很好地降级。这实际上解决了我遇到的特定问题。我想对 L 产生连锁反应,并在早期版本的 android 上进行简单的选择。
    • @AndroidDev, @Dave Jensen:实际上,使用 ?attr: 而不是 ?android:attr 引用了 v7 支持库,假设您使用它,它可以向后兼容 API 7。请参阅: developer.android.com/tools/support-library/features.html#v7
    • 如果我也想要背景色怎么办?
    • 波纹效果不适用于 API 可绘制),并将其放置在 res/drawable 用于非波纹点击效果(通常使用 可绘制)
    【解决方案2】:

    我以前投票关闭这个问题作为离题,但实际上我改变了主意,因为这是非常好的视觉效果,不幸的是,它还不是支持库的一部分。它很可能会出现在未来的更新中,但没有公布时间框架。

    幸运的是,已经有一些自定义实现可用:

    包括与旧版本 Android 兼容的 Material 主题小部件集:

    所以您可以尝试其中一种或谷歌搜索其他“材料小部件”……

    【讨论】:

    • 这现在是支持库的一部分,请参阅我的回答。
    • 谢谢!我用的是second lib,第一个在慢手机上太慢了。
    【解决方案3】:

    我做了一个简单的类来制作波纹按钮,我最终不需要它,所以它不是最好的,但它是:

    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); }
    • 实现简单...即插即用:)
    • 如果我在 RecyclerView 的每个视图上使用此类,我会收到 ClassCastException。
    • @Ali_Waris 现在支持库可以处理涟漪效应,但要解决这个问题,您所要做的就是不要使用addRippleToView 来添加涟漪效应。而是让RecyclerView 中的每个视图成为RippleViewCreator
    【解决方案4】:

    这很简单;-)

    首先您必须创建两个可绘制文件,一个用于旧 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>
    

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

    有关ripple drawable的更多信息,请访问:https://developer.android.com/reference/android/graphics/drawable/RippleDrawable.html

    【讨论】:

    • 真的很简单!
    • 这个解决方案绝对应该得到更多的支持!谢谢。
    【解决方案5】:

    有时您有自定义背景,在这种情况下,更好的解决方案是使用android:foreground="?selectableItemBackground"

    更新

    如果你想要带有圆角和自定义背景的波纹效果,你可以使用这个:

    background_ripple.xml

    <?xml version="1.0" encoding="utf-8"?>
    <ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="@color/ripple_color">
    <item android:id="@android:id/mask">
        <shape android:shape="rectangle">
            <solid android:color="@android:color/white" />
            <corners android:radius="5dp" />
        </shape>
    </item>
    
    <item android:id="@android:id/background">
        <shape android:shape="rectangle">
            <solid android:color="@android:color/white" />
            <corners android:radius="5dp" />
        </shape>
    </item>
    

    layout.xml

    <Button
        android:id="@+id/btn_play"
        android:background="@drawable/background_ripple"
        app:backgroundTint="@color/colorPrimary"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/play_now" />
    

    我在 API >= 21 上使用过这个

    【讨论】:

    • 是的,但它适用于 API >= 23 或具有 21 API 的设备,但仅适用于 CardView 或 FrameLayout
    • 如果你的自定义背景有圆角,波纹效果会很难看。
    • 如果有人需要波纹颜色 #1f000000`
    【解决方案6】:

    有时这条线可以用于任何布局或组件。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2023-03-30
      • 2019-08-31
      • 1970-01-01
      • 1970-01-01
      • 2019-07-22
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多