preface

We know that when a stone is thrown into the water, there is a circle spread effect, which is very fine and beautiful. From 5.0 onwards, there is also a ripple word in the development. Many apps have started a new journey, adapting this effect to various mobile phones.

The default Button will have this effect. If you are not satisfied, you can also customize the color. The effect is as follows:

However, this kind of effect is suitable for triggering when clicking, and in some cases, it needs to be continuously displayed in a circle of ripples. For example, when searching for a girl in some social apps, such animation can be used. Although the two effects are not related, they are also evolved, and the effect is as follows:

implementation

If you look at the animation, there are only two elements, a circular avatar and a circle of diffused circles. In this case, the circular avatar uses the CircleImageView library. Of course, you can draw it yourself, but who knows which version of the phone will cause problems, so it’s good to use a proven library. Pictures are loaded using Glide.

implementation 'com. Making. Bumptech. Glide: glide: 4.12.0'
annotationProcessor 'com. Making. Bumptech. Glide: the compiler: 4.12.0'
implementation 'DE. Hdodenhof: circleimageview: 3.1.0'
Copy the code

So the overall layout looks like this:

In onDraw in FrameLayout, the circle is always drawn with the drawCircle method, and the opacity is constantly set to fade away.

Draw the circle

The method of drawing a circle is very simple. The parameters are center X, center y, circle size, and brush

drawCircle(float cx, float cy, float radius, @NonNull Paint paint)
Copy the code

So to go from a circle of size 50, to a circle of size 100, is to constantly set radius, and also set the transparency of the brush.

class WaterView @JvmOverloads constructor(
    context: Context.attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    var TAG = "TAG";

    var circleSize: Float = 10f;
    var circleAlpha: Int = 255;
    init {

    }


    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (circleAlpha<=0){circleAlpha=0}
        var paint = Paint().apply {
            color = Color.RED
            alpha=circleAlpha
        }

        canvas.drawCircle(
            (measuredWidth / 2).toFloat(),
            (measuredHeight / 2).toFloat(),
            circleSize,
            paint
        )
        circleSize += 20
        circleAlpha-=10

        postInvalidateDelayed(80)}}Copy the code

The next step is to add a second circle when the first circle has reached a certain size, remove it when the first circle has reached a certain size, and then repeat the process.

Calculate the transparency step

The most important thing in this animation is to calculate the decrement step of the transparency, which is 255-0. How many decrement steps should we set? If it is too large, the edge of the View will disappear before it spreads out, and if it is too small, the circles will feel like they are all stacked, so we can’t write this value to death.

For example, the smallest side is 825 and the DIFFUSE_DISTANCE value is 10. Then temp is 82. This 82 is the number of increments from 0 to the edge of the View. And then I get the final value by (255/82) times 2,

 var min = min(w, h)
 var temp = min / DIFFUSE_DISTANCE;
 circleAlphaReduceStep = ((255 / temp) * 2);
Copy the code

The complete code

package com.example.kotlindemo.widget

import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.os.Handler
import android.os.Looper
import android.util.AttributeSet
import android.util.Log
import android.view.GestureDetector
import android.view.Gravity
import android.view.View
import android.view.animation.OvershootInterpolator
import android.widget.FrameLayout
import androidx.core.os.postDelayed
import com.bumptech.glide.Glide
import de.hdodenhof.circleimageview.CircleImageView
import meow.bottomnavigation.dp
import kotlin.math.max
import kotlin.math.min

class WaterView @JvmOverloads constructor(
    context: Context.attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr), Runnable {

    /** ** x and y */ at the center of the circle
    var centerXPoint: Float = 0f;
    var centerYPoint: Float = 0f;

    /** * circles default color */
    val CIRCLE_DEFAULT_COLOR: Int = Color.parseColor("#FFB6C1")


    /** * when the circle expands to this size, add the next circle */
    val CENTER_CIRCLE_SIZE = 75;

    /** * The circle width and transparency are stored in the circle set */
    var circleList = mutableListOf<Int>()
    var circleAlphaList = mutableListOf<Int>()

    /** * circle opacity decrement steps */
    var circleAlphaReduceStep: Int = 0;

    /** * circle spread steps */
    val DIFFUSE_DISTANCE: Int = 10;

    /** * circle brush */
    lateinit var circlePaint: Paint

    /** * ImageView */
    var circleImageView: CircleImageView = CircleImageView(context);

    var mHandler = Handler(Looper.myLooper()!!)

    /** * Center circle ImageView animation */
    override fun run(a) {
        var x = ObjectAnimator.ofFloat(circleImageView, "scaleX".1f.1.3 f.1f)
        x.interpolator = OvershootInterpolator();

        var y = ObjectAnimator.ofFloat(circleImageView, "scaleY".1f.1.3 f.1f)
        y.interpolator = OvershootInterpolator();
        var animatorSet = AnimatorSet()

        animatorSet.setDuration(700)
        animatorSet.playTogether(x, y);
        animatorSet.start()

        mHandler.postDelayed(this.800)
    }

    init {
        Glide.with(context)
            .load("Https://img2.baidu.com/it/u=1194131577, 2954769920 & FM = 26 & FMT = auto&gp = 0. JPG")
            .into(circleImageView);
        var layoutParams =
            FrameLayout.LayoutParams(CENTER_CIRCLE_SIZE.dp(context), CENTER_CIRCLE_SIZE.dp(context))
        layoutParams.gravity = Gravity.CENTER
        addView(circleImageView, layoutParams)
        addNewCircle(a);
        setWillNotDraw(false)
        mHandler.postDelayed(this.0)
        
        circlePaint = Paint().apply {
            color = CIRCLE_DEFAULT_COLOR
        }
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        centerXPoint = (w / 2).toFloat();
        centerYPoint = (h / 2).toFloat();

        /** * calculates the number of steps to decrement transparency from the center point */
        var min = min(w, h)
        var temp = min / DIFFUSE_DISTANCE;
        circleAlphaReduceStep = ((255 / temp) * 2);
    }

    fun addNewCircle(a) {
        circleList.add(0)
        circleAlphaList.add(255)}override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        /** * traverses all circles, the first time the number is 1 */
        for (i in circleList.indices) {
            circlePaint.alpha = circleAlphaList[i];
            var circleWidth = circleList[i];
            canvas.drawCircle(centerXPoint, centerYPoint, circleWidth.toFloat(), circlePaint)
            circleList[i] = DIFFUSE_DISTANCE + circleList[i];

            /** * sets the current transparency */
            circleAlphaList[i] = if (circleAlphaList[i] - circleAlphaReduceStep <= 0) 0 else {
                circleAlphaList[i] - circleAlphaReduceStep
            }
        }

        /** * If the width of the innermost circle is greater than CENTER_CIRCLE_SIZE, add the next circle */
        if (circleList[circleList.lastIndex] > CENTER_CIRCLE_SIZE) {
            addNewCircle();
        }

        /** * Benefits if the outermost circle width is greater than measuredWidth */
        if (circleList[0] >= measuredWidth - 100) {
            circleList.removeAt(0);
            circleAlphaList.removeAt(0);

        }

        /** * Start the next */ after 75 ms delay
        postInvalidateDelayed(75)}}Copy the code