android中的恒定涟漪效应
Posted
技术标签:
【中文标题】android中的恒定涟漪效应【英文标题】:Constant ripple effect in android 【发布时间】:2020-05-30 20:08:54 【问题描述】:我想要做的是在 LinearLayout 的背景上产生恒定的涟漪效应。为什么?基本上,此 LinearLayout 表示正在观看此项目的实时用户。因此,我希望背景具有恒定的波纹动画,类似于某些具有实时指示器的应用程序,该指示器在该指示器的背景上具有波纹效果。我希望我的问题很清楚。
示例: 我希望这种效果不断发生
【问题讨论】:
检查我的答案我已经根据您的需要创建了自定义波纹视图。 【参考方案1】:您好,我尝试编写类似这样的代码,下面是我接近的内容。您可以随时调整数字以减慢动画和其他内容的速度。
1) 在您的 res/drawable 中创建一个名为 temp_ripple.xml
的波纹可绘制背景<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/colorPrimary"
>
<item android:id="@android:id/mask"
android:drawable="@android:color/holo_green_dark"
>
</item>
<item
android:drawable="@android:color/holo_orange_dark">
</item>
</ripple>
2) 将背景分配给可能的候选视图,如下所示,此处将 AppCompatButton 分配给 android:background="@drawable/temp_ripple"
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_
android:layout_
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnLive"
android:layout_
android:layout_
android:layout_centerInParent="true"
android:layout_gravity="center"
android:background="@drawable/temp_ripple"
android:foreground="?selectableItemBackground"
android:text="12.5k Live"
android:textColor="@android:color/white" />
</RelativeLayout>
3) 从视图中获取ripple drawable并创建一个runnable,在2秒后运行,通过在按钮的点击监听器中设置ripple drawable的状态来重复动画
package com.example.android.treasureHunt
import android.content.res.ColorStateList
import android.graphics.drawable.RippleDrawable
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.util.Log
import android.view.MotionEvent
import android.view.View
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.temp_activity.*
class TempActivity : AppCompatActivity(R.layout.temp_activity)
val handler = Handler()
lateinit var runnable: Runnable
var count = 0
lateinit var rippleDrawable: RippleDrawable
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onCreate(savedInstanceState: Bundle?)
super.onCreate(savedInstanceState)
rippleDrawable = btnLive.background as RippleDrawable
setLiveCountListener()
btnLive.setOnClickListener
Log.d("TAG++", "button clicked")
rippleDrawable.state = intArrayOf(
android.R.attr.state_pressed,
android.R.attr.state_enabled
)
private fun setLiveCountListener()
runnable = Runnable
rippleDrawable.state = intArrayOf()
btnLive.performClick()
//to perform another runnable after some time creating a race condition
handler.postDelayed(runnable, 2000)
//condition to breakout from loop
if (count == 10)
handler.removeCallbacks(runnable)
Log.d("TAG++", "Loop running")
//trigger the start of the ui thread
handler.postDelayed(runnable, 2000)
【讨论】:
【参考方案2】:花了很多小时后,我根据您的需要为 无限波纹视图 创建了 自定义类,并使用 this lib 进行自定义。
InfiniteRippleLayout
public class InfiniteRippleLayout extends FrameLayout
/**
* Author:Hardik Talaviya
* Date: 2020.02.15 1:30 PM
* Describe:
*/
private static final int DEFAULT_DURATION = 350;
private static final int DEFAULT_FADE_DURATION = 75;
private static final float DEFAULT_ALPHA = 0.2f;
private static final int DEFAULT_COLOR = Color.BLACK;
private static final int DEFAULT_BACKGROUND = Color.TRANSPARENT;
private static final boolean DEFAULT_DELAY_CLICK = true;
private static final boolean DEFAULT_PERSISTENT = false;
private static final boolean DEFAULT_SEARCH_ADAPTER = false;
private static final boolean DEFAULT_RIPPLE_OVERLAY = false;
private static final int DEFAULT_ROUNDED_CORNERS = 0;
private static final int FADE_EXTRA_DELAY = 50;
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Rect bounds = new Rect();
private int rippleColor;
private boolean rippleOverlay;
private int rippleDuration;
private int rippleAlpha;
private boolean rippleDelayClick;
private int rippleFadeDuration;
private boolean ripplePersistent;
private Drawable rippleBackground;
private boolean rippleInAdapter;
private float rippleRoundedCorners;
private float radius;
private AdapterView parentAdapter;
private View childView;
private AnimatorSet rippleAnimator;
private Point currentCoords = new Point();
private int layerType;
private int positionInAdapter;
/*
* Animations
*/
private Property<InfiniteRippleLayout, Float> radiusProperty
= new Property<InfiniteRippleLayout, Float>(Float.class, "radius")
@Override
public Float get(InfiniteRippleLayout object)
return object.getRadius();
@Override
public void set(InfiniteRippleLayout object, Float value)
object.setRadius(value);
;
private Property<InfiniteRippleLayout, Integer> circleAlphaProperty
= new Property<InfiniteRippleLayout, Integer>(Integer.class, "rippleAlpha")
@Override
public Integer get(InfiniteRippleLayout object)
return object.getRippleAlpha();
@Override
public void set(InfiniteRippleLayout object, Integer value)
object.setRippleAlpha(value);
;
public InfiniteRippleLayout(Context context)
this(context, null, 0);
public InfiniteRippleLayout(Context context, AttributeSet attrs)
this(context, attrs, 0);
public InfiniteRippleLayout(Context context, AttributeSet attrs, int defStyle)
super(context, attrs, defStyle);
setWillNotDraw(false);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.InfiniteRippleLayout);
rippleColor = a.getColor(R.styleable.InfiniteRippleLayout_mrl_rippleColor, DEFAULT_COLOR);
rippleOverlay = a.getBoolean(R.styleable.InfiniteRippleLayout_mrl_rippleOverlay, DEFAULT_RIPPLE_OVERLAY);
rippleDuration = a.getInt(R.styleable.InfiniteRippleLayout_mrl_rippleDuration, DEFAULT_DURATION);
rippleAlpha = (int) (255 * a.getFloat(R.styleable.InfiniteRippleLayout_mrl_rippleAlpha, DEFAULT_ALPHA));
rippleDelayClick = a.getBoolean(R.styleable.InfiniteRippleLayout_mrl_rippleDelayClick, DEFAULT_DELAY_CLICK);
rippleFadeDuration = a.getInteger(R.styleable.InfiniteRippleLayout_mrl_rippleFadeDuration, DEFAULT_FADE_DURATION);
rippleBackground = new ColorDrawable(a.getColor(R.styleable.InfiniteRippleLayout_mrl_rippleBackground, DEFAULT_BACKGROUND));
ripplePersistent = a.getBoolean(R.styleable.InfiniteRippleLayout_mrl_ripplePersistent, DEFAULT_PERSISTENT);
rippleInAdapter = a.getBoolean(R.styleable.InfiniteRippleLayout_mrl_rippleInAdapter, DEFAULT_SEARCH_ADAPTER);
rippleRoundedCorners = a.getDimensionPixelSize(R.styleable.InfiniteRippleLayout_mrl_rippleRoundedCorners, DEFAULT_ROUNDED_CORNERS);
a.recycle();
paint.setColor(rippleColor);
paint.setAlpha(rippleAlpha);
enableClipPathSupportIfNecessary();
startRipple();
@Override
public final void addView(View child, int index, ViewGroup.LayoutParams params)
if (getChildCount() > 0)
throw new IllegalStateException("MaterialRippleLayout can host only one child");
//noinspection unchecked
childView = child;
super.addView(child, index, params);
@Override
public void setOnClickListener(OnClickListener onClickListener)
if (childView == null)
throw new IllegalStateException("MaterialRippleLayout must have a child view to handle clicks");
childView.setOnClickListener(onClickListener);
@Override
public void setOnLongClickListener(OnLongClickListener onClickListener)
if (childView == null)
throw new IllegalStateException("MaterialRippleLayout must have a child view to handle clicks");
childView.setOnLongClickListener(onClickListener);
@Override
public boolean onInterceptTouchEvent(MotionEvent event)
return !findClickableViewInChild(childView, (int) event.getX(), (int) event.getY());
private void startRipple()
float endRadius = getEndRadius();
cancelAnimations();
rippleAnimator = new AnimatorSet();
rippleAnimator.addListener(new AnimatorListenerAdapter()
@Override
public void onAnimationEnd(Animator animation)
if (!ripplePersistent)
setRadius(0);
setRippleAlpha(rippleAlpha);
if (rippleDelayClick)
startRipple();
childView.setPressed(false);
);
ObjectAnimator ripple = ObjectAnimator.ofFloat(this, radiusProperty, radius, endRadius);
ripple.setDuration(rippleDuration);
ripple.setInterpolator(new DecelerateInterpolator());
ObjectAnimator fade = ObjectAnimator.ofInt(this, circleAlphaProperty, rippleAlpha, 0);
fade.setDuration(rippleFadeDuration);
fade.setInterpolator(new AccelerateInterpolator());
fade.setStartDelay(rippleDuration - rippleFadeDuration - FADE_EXTRA_DELAY);
if (ripplePersistent)
rippleAnimator.play(ripple);
else if (getRadius() > endRadius)
fade.setStartDelay(0);
rippleAnimator.play(fade);
else
rippleAnimator.playTogether(ripple, fade);
rippleAnimator.start();
private void cancelAnimations()
if (rippleAnimator != null)
rippleAnimator.cancel();
rippleAnimator.removeAllListeners();
private float getEndRadius()
final int width = getWidth();
final int height = getHeight();
final int halfWidth = width / 2;
final int halfHeight = height / 2;
final float radiusX = halfWidth > currentCoords.x ? width - currentCoords.x : currentCoords.x;
final float radiusY = halfHeight > currentCoords.y ? height - currentCoords.y : currentCoords.y;
return (float) Math.sqrt(Math.pow(radiusX, 2) + Math.pow(radiusY, 2)) * 1.2f;
private AdapterView findParentAdapterView()
if (parentAdapter != null)
return parentAdapter;
ViewParent current = getParent();
while (true)
if (current instanceof AdapterView)
parentAdapter = (AdapterView) current;
return parentAdapter;
else
try
current = current.getParent();
catch (NullPointerException npe)
throw new RuntimeException("Could not find a parent AdapterView");
private boolean adapterPositionChanged()
if (rippleInAdapter)
int newPosition = findParentAdapterView().getPositionForView(InfiniteRippleLayout.this);
final boolean changed = newPosition != positionInAdapter;
positionInAdapter = newPosition;
if (changed)
cancelAnimations();
childView.setPressed(false);
setRadius(0);
return changed;
return false;
private boolean findClickableViewInChild(View view, int x, int y)
if (view instanceof ViewGroup)
ViewGroup viewGroup = (ViewGroup) view;
for (int i = 0; i < viewGroup.getChildCount(); i++)
View child = viewGroup.getChildAt(i);
final Rect rect = new Rect();
child.getHitRect(rect);
final boolean contains = rect.contains(x, y);
if (contains)
return findClickableViewInChild(child, x - rect.left, y - rect.top);
else if (view != childView)
return (view.isEnabled() && (view.isClickable() || view.isLongClickable() || view.isFocusableInTouchMode()));
return view.isFocusableInTouchMode();
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh)
super.onSizeChanged(w, h, oldw, oldh);
bounds.set(0, 0, w, h);
rippleBackground.setBounds(bounds);
@Override
public boolean isInEditMode()
return true;
/*
* Drawing
*/
@Override
public void draw(Canvas canvas)
final boolean positionChanged = adapterPositionChanged();
currentCoords = new Point(getWidth() / 2, getHeight() / 2);
if (rippleOverlay)
if (!positionChanged)
rippleBackground.draw(canvas);
super.draw(canvas);
if (!positionChanged)
if (rippleRoundedCorners != 0)
Path clipPath = new Path();
RectF rect = new RectF(0, 0, canvas.getWidth(), canvas.getHeight());
clipPath.addRoundRect(rect, rippleRoundedCorners, rippleRoundedCorners, Path.Direction.CW);
canvas.clipPath(clipPath);
canvas.drawCircle(currentCoords.x, currentCoords.y, radius, paint);
else
if (!positionChanged)
rippleBackground.draw(canvas);
canvas.drawCircle(currentCoords.x, currentCoords.y, radius, paint);
super.draw(canvas);
private float getRadius()
return radius;
public void setRadius(float radius)
this.radius = radius;
invalidate();
public int getRippleAlpha()
return paint.getAlpha();
public void setRippleAlpha(Integer rippleAlpha)
paint.setAlpha(rippleAlpha);
invalidate();
/**
* @link Canvas#clipPath(Path) is not supported in hardware accelerated layers
* before API 18. Use software layer instead
* <p/>
* https://developer.android.com/guide/topics/graphics/hardware-accel.html#unsupported
*/
private void enableClipPathSupportIfNecessary()
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR1)
if (rippleRoundedCorners != 0)
layerType = getLayerType();
setLayerType(LAYER_TYPE_SOFTWARE, null);
else
setLayerType(layerType, null);
attributes.xml
将此属性添加到您的res->values->attributes.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="InfiniteRippleLayout">
<attr name="mrl_rippleColor" format="color" localization="suggested" />
<attr name="mrl_rippleOverlay" format="boolean" localization="suggested" />
<attr name="mrl_rippleAlpha" format="float" localization="suggested" />
<attr name="mrl_rippleDuration" format="integer" localization="suggested" />
<attr name="mrl_rippleFadeDuration" format="integer" localization="suggested" />
<attr name="mrl_rippleBackground" format="color" localization="suggested" />
<attr name="mrl_rippleDelayClick" format="boolean" localization="suggested" />
<attr name="mrl_ripplePersistent" format="boolean" localization="suggested" />
<attr name="mrl_rippleInAdapter" format="boolean" localization="suggested" />
<attr name="mrl_rippleRoundedCorners" format="dimension" localization="suggested" />
</declare-styleable>
</resources>
使用下面的xml代码添加效果
<com.broooapps.curvegraphview.InfiniteRippleLayout
android:layout_
android:layout_
app:mrl_rippleAlpha="0.2"
app:mrl_rippleColor="#585858"
app:mrl_rippleDelayClick="true"
app:mrl_rippleDuration="1100"
app:mrl_rippleOverlay="true">
<TextView
android:layout_
android:layout_
android:layout_gravity="center"
android:text="HARDIK TALAVIYA"
android:textColor="#000"
android:textSize="20sp" />
</com.broooapps.curvegraphview.InfiniteRippleLayout>
结果
希望对你有帮助!
【讨论】:
以上是关于android中的恒定涟漪效应的主要内容,如果未能解决你的问题,请参考以下文章
Android L 的涟漪效应 - 按钮的触摸反馈 - 使用 XML
为啥在棒棒糖之前的 android 设备上很难创建涟漪效应?
如何在视图内的特定位置触发 Android Lollipop 的涟漪效应,而不触发触摸事件?