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-&gt;values-&gt;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 的涟漪效应,而不触发触摸事件?

如何在 Android 上的 textview 或 imageview 上设置涟漪效果?

recyclerview 中的触摸没有涟漪效应