K
K
Konstantin Dovnar2015-08-23 14:59:58
Android
Konstantin Dovnar, 2015-08-23 14:59:58

Ripple effect with freeforms?

I'm looking for help from more advanced people.
You need to implement a ripple effect on the circle shape:

<shape  xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval" android:dither="true">
        <solid android:color="#cccccc"/>
</shape>

For SDK version 21+ there are no problems, we use a simple ripple tag, but the effect also needs to be implemented on older devices (SDK 15+).
In search of a solution, I came across two libraries: RippleEffect and RippleDrawable , and the second one immediately declares that it can implement the effect on arbitrary forms, however, having done everything as described in the project on github, the application simply crashes with an error on the SDK below version 21, as I understand it the point is still in the ripple tag, which is simply not supported, but is used for the library. Perhaps the problem is that I'm using a fragment, and in the example everything is done right away on the activity, which seems to be not a good practice.
The first library from custom forms has only a small examplethe drawable file itself, even without the implementation, however, this option did not work, the ripple effect still leaves the circle, creating a square.
There were a couple more libraries , but they also did not find anything about custom forms.
Perhaps someone has already implemented this for older devices and can help?

Answer the question

In order to leave comments, you need to log in

1 answer(s)
D
Dmitry Bolshakov, 2015-08-25
@SolidlSnake

I made controls with full rendering of all graphics on Canvas. And there is a limitation, you have to sacrifice hardware acceleration, but I didn’t notice the difference visually.
Full class code:

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.ValueAnimator;

public class RippleCircle extends View {
    // ===========================================================
    // Constants
    // ===========================================================
    private static final String TAG = RippleCircle.class.getSimpleName();
    private static final int RIPPLE_COLOR = 0x66000000;
    private static final int RIPPLE_FADE_COLOR = 0x22000000;
    private static final int FAB_COLOR = 0xffcccccc;
    private static final long ANIM_DURATION = 250;
    protected static int FLAGS = Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG;
    // ===========================================================
    // Fields
    // ===========================================================
    private Paint mButtonBg;
    private Paint mRippleButtonBg;
    private Path main;
    private ValueAnimator animatorRipple;
    private float selRadius;
    private float radius;
    private float centerY;
    private float centerX;
    private float rippleX;
    private float rippleY;
    private boolean isButtonTouchDown;
    private boolean isProgress;     
    // ===========================================================
    // Constructors
    // ===========================================================
    public RippleCircle(Context context) {
        super(context);
        init();
    }

    public RippleCircle(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    // ===========================================================
    // Getter & Setter
    // ===========================================================

    // ===========================================================
    // Methods for/from SuperClass/Interfaces
    // ===========================================================
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawCircle(centerX, centerY, radius, mButtonBg);
        canvas.clipPath(main);
        canvas.drawCircle(rippleX, rippleY, selRadius, mRippleButtonBg);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        float size = Math.min(w, h);
        centerY = size / 2;
        centerX = size / 2;
        radius = size / 2;
        main.addCircle(centerX, centerY, radius, Path.Direction.CCW);
        animatorRipple.setFloatValues(0, radius);
    }
    // ===========================================================
    // Inner Methods
    // ===========================================================
    private void init() {
        main = new Path();

        animatorRipple = ValueAnimator.ofFloat(0, 0);
        animatorRipple.setDuration(ANIM_DURATION);
        animatorRipple.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                selRadius = (Float) animation.getAnimatedValue();
                if (selRadius / radius <= 1 && selRadius / radius >= 0)
                    mRippleButtonBg.setColor(blendColors(RIPPLE_COLOR, RIPPLE_FADE_COLOR, 1 - selRadius / radius));
                invalidate();
            }
        });

        animatorRipple.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                if (!isButtonTouchDown) {
                    selRadius = 0;
                    invalidate();
                    isProgress = false;
                }
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) {
            setLayerType(LAYER_TYPE_SOFTWARE, null);
        }
        mButtonBg = new Paint(FLAGS);
        mButtonBg.setColor(FAB_COLOR);

        mRippleButtonBg = new Paint(FLAGS);
        mRippleButtonBg.setColor(RIPPLE_COLOR);

        setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {

                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        rippleX = event.getX();
                        rippleY = event.getY();
                        if (isPointInCircle(centerX, centerY, event.getX(), event.getY(), radius)) {
                            isButtonTouchDown = true;
                            if (!isProgress) {
                                rippleX = event.getX();
                                rippleY = event.getY();
                                isProgress = true;
                                float max = distance(centerX, centerY, rippleX, rippleY) + radius;
                                animatorRipple.setFloatValues(0, max);
                                animatorRipple.start();
                            }
                        }
                        break;
                    case MotionEvent.ACTION_UP:
                        isButtonTouchDown = false;
                        if (!animatorRipple.isRunning()) {
                            selRadius = 0;
                            invalidate();
                            isProgress = false;
                        }
                        break;
                }
                return true;
            }
        });
    }

    private boolean isPointInCircle(float centerX, float centerY, float x, float y, float radius) {
        return distance(centerX, centerY, x, y) <= radius;
    }

    private float distance(final float pX1, final float pY1, final float pX2, final float pY2) {
        final float dX = pX2 - pX1;
        final float dY = pY2 - pY1;
        return (float) Math.sqrt((dX * dX) + (dY * dY));
    }

    private int blendColors(int color1, int color2, float ratio) {
        final float inverseRation = 1f - ratio;
        float a = (Color.alpha(color1) * ratio) + (Color.alpha(color2) * inverseRation);
        float r = (Color.red(color1) * ratio) + (Color.red(color2) * inverseRation);
        float g = (Color.green(color1) * ratio) + (Color.green(color2) * inverseRation);
        float b = (Color.blue(color1) * ratio) + (Color.blue(color2) * inverseRation);
        return Color.argb((int) a, (int) r, (int) g, (int) b);
    }
    // ===========================================================
    // Inner and Anonymous Classes
    // ===========================================================
}

We use in the markup:
<yourPackage.RippleCircle
        android:layout_width="56dp"
        android:layout_height="56dp"
        android:layout_centerInParent="true"/>

By analogy, you can add any shape you like, I made a rectangle with rounded corners and a circle.

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question