如何使用支持库实现涟漪动画?

我试图添加一个按钮点击涟漪动画。我喜欢下面,但它需要 minSdKVersion 到21。

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

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

怎样才能做到这一点?

161501 次浏览

我以前投票结束这个问题作为偏离主题,但实际上我改变了我的想法,因为这是相当不错的视觉效果,不幸的是,还不是支持库的一部分。它很可能会出现在未来的更新,但没有宣布时间框架。

幸运的是,目前只有很少的自定义实现可用:

包括与旧版 Android 兼容的材质主题小工具集:

所以你可以尝试其中的一个或谷歌其他“物质小工具”或所以..。

基本的涟漪设置

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

  • 超出视野范围的涟漪:
    android:background="?selectableItemBackgroundBorderless"

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

支援图书馆

  • 使用 ?attr:(或 ?的简写)而不是 ?android:attr引用 支援图书馆,因此可以返回到 API 7。

图片/背景的涟漪

  • 要有一个图像或背景和覆盖纹波最简单的解决方案是包装的 ViewFrameLayout与纹波设置与 setForeground()setBackground()

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

我做了一个简单的类,使涟漪按钮,我从来没有需要它在结束时,所以它不是最好的,但在这里:

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

有时您有一个自定义的背景,在这种情况下,更好的解决方案是使用 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>

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 版本和另一个最新的版本,当然!如果你为最新的 api 版本 android 工作室创建可绘制文件,建议你自动创建旧文件。最后设置这个可绘制的背景视图。

新 API 版本的可绘制示例(res/draable-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/draable/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

有时将 b 可用于任何布局或组件的这一行。

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