Idle is idle time, open explore explore a row, quite many male compatriots will be so. This is not, I am also so, see the effect of the home probe or quite attractive. Before the imitation of the realization of one, the effect is almost, just today nothing to improve, write down, hope to see can have harvest.

Achieved effect

Let’s first look at the effect of the implementation, not to say more. Of course, there is still a gap with the original tan tan, there is no more optimization in the details. However, it is ok to take some time to adjust it. Now you can see the effect, I added the frame number below the display, which is very smooth on the real machine, but there is still a bit of lag on the simulator due to poor performance.

Analysis of implementation

It can be seen from the effect diagram that the overall realization can be divided into the following four steps:

  1. Ripple effect
  2. The gradient scan effect and the hollow out in the middle
  3. rotating
  4. Click on the animation of your avatar

Implement the above steps separately and you can do it. There is more than one specific implementation method, and the implementation I have chosen here is straightforward and easy to implement. The steps are broken down and the key details are explained in detail.

How to implement

Because there are avatars, and it involves loading web images. In theory we could inherit ImageView directly, but that would be too complicated to do. So the avatar is separate from what we’re trying to achieve. Then in the image combination, here you can make a custom ViewGroup to combine the two, I figure here to save trouble, here is not to do, but directly in the use of time, in the layout of the combination together.

  1. So the first step is not to worry about the avatar but to implement antanrippleView. Next, let’s look at the implementation of the water ripple: what we need is that the ripple is added dynamically by clicking on the picture, so we need to expose the interface. And the ripple is gradual, the more to the edge of the lower transparency, until disappear. Each ripple is a circle, and the opacity can be changed by changing the color of the Paint. The opacity is also consistent with the radius of the circle. So I’ve enclosed each ripple here.
    inner class RippleCircle {
        // 4s * 60 frms = 240
        private val slice = 150
        var startRadius = 0f
        var endRadius = 0f
        var cx = 0f
        var cy = 0f

        private var progress = 0

        fun draw(canvas: Canvas) {
            if (progress >= slice) {
                // remove
                post {
                    rippleCircles.remove(this)
                }
                return} progress++ ripplePaint. Alpha = (1 - progress.div(slice * 1.0f)).times(255).toint () val radis = startRadius + (endRadius - startRadius).div(slice).times(progress) canvas.drawCircle(cx, cy, radis, ripplePaint) } }Copy the code

You might be confused by the slice property, which defines the duration of the ripple. If 60 frames per second, then it lasts 4 seconds, making a total of 240 frames. The default is 150 frames, so the duration at 60 frames is 2.5s. Transparency and radius are related to slice:

Alpha = (1 - progress.div(slice * 1.0f)).times(255).toint () val radis = startRadius + (endRadius -) startRadius).div(slice).times(progress)Copy the code

Over time, the lower the transparency, the greater the radius.

How to use the encapsulated RippleCircle. Our requirement is that it can be added dynamically and removed when it disappears, so ArrayList is used as the container. However, this involves adding and deleting collections, and an exception will occur if both operations are performed. The solution is as follows, use CopyOnWriteArrayList, and remove by:

post {
                    rippleCircles.remove(this)
                }
Copy the code

And then in onDraw, it’s worth mentioning that in order not to get caught up in the scanned part, this code needs to be written later in the onDraw method.

  for (i in 0 until rippleCircles.size) {
            rippleCircles[i].draw(canvas)
        }
Copy the code

Add RippleCircle to the startRipple() method:

 rippleCircles.add(RippleCircle().apply {
                cx = width.div(2).toFloat()
                cy = height.div(2).toFloat()
                val maxRadius = Math.min(width, height).div(2).toFloat()
                startRadius = maxRadius.div(3)
                endRadius = maxRadius
            })
Copy the code

StartRipple is also exposed to call the add ripple method. Click on your picture and add. When it comes to customizing a View of course measurement is a key part of it. But for now I’m just going to use the default, and then I’m going to take the minimum width and height and divide it by 2 for the radius. Why startRadius is called 3 here, because that size is defined as the radius of the beginning of the ripple circle. This is the end of the first step.

  1. The effectiveness of scanning is a critical part, and efficiency directly affects availability. If you look closely, it’s actually a circle with a shader added. So the focus is on shader implementation. Android provides several shaders to use by default. SweepGradient is what we need, scan gradient. Then, after selecting it, it’s time to adjust the parameters. Look at the use of SweepGradient: constructor
SweepGradient(float cx, float cy,
            @NonNull @ColorInt int colors[], @Nullable float positions[])
Copy the code

The key is understanding the positions. Follow the documentation to explain and code. For example, a one-to-one correspondence to the values of colors must be monotonically increasing to prevent serious exceptions. Positions correspond to the position of each color, and of course, the position of the circle. Clockwise, 0 is 0°, 0.5 is 180°, and 1 is 360°. If you want to look like Tantan, you start with a very dark thread. It shows that the first color is dark and the second color is light and has a small proportion, as follows

val colors = intArrayOf(getColor(R.color.pink_fa758a),getColor(R.color.pink_f5b8c2),getColor(R.color.top_background_color),getColor(R .color.white)) SweepGradient(width.div(2).toFloat(), height.div(2).toFloat(), colors,floatArrayOf (0 f, f, 0.001 0.9 f, 1 f))Copy the code

So set the right parameters, the whole scan gradient effect is almost the same. And then I’m going to set the shader for the brush, in the drawCircle.

        backPaint.setShader(SweepGradient(width.div(2).toFloat(), height.div(2).toFloat(), colors, floatArrayOf(0f, 0.001f, 0.9f, 1f)) canvas. DrawCircle (width.div(2).tofloat (), height.div(2).tofloat (), radius, backPaint)Copy the code

After the above operation, the whole sweep scope is the whole circle, and the effect needed is the campus with hollow out in the middle. Here, the operation of Xfermode is involved. To perform xferMode, you must set the layer on the Canvas. If not, there will be a problem. The hollowed out campus is black. A detailed explanation is given in my article between the high imitation QQ send picture highlight HaloProgressView article. SetLayer needs to set the range, so our range is the rectangle that covers the entire circle

        val rectF = RectF(width.div(2f) - radius
                , height.div(2f) - radius
                , width.div(2f) + radius
                , height.div(2f) + radius)
        val sc = canvas.saveLayer(rectF, backPaint, Canvas.ALL_SAVE_FLAG)
Copy the code

Then drawCircle and set xfermode

        backPaint.setXfermode(PorterDuffXfermode(PorterDuff.Mode.DST_OUT))

Copy the code

DST_OUT is used here. You can see why Paint Xfermode is used in detail in previous articles. So here we have the gradient and the hollowing all done, except for the last step, the rotation. Rotate directly through the Canvas’s Rotate method is a good fit for the current scenario. Because the whole View is a circle. When it comes to the Canvas operation, you need to save, then restore

 canvas.save()
        canvas.rotate(sweepProgress.toFloat(), width.div(2f), height.div(2f))
        ...
        canvas.restore()
Copy the code

As you can see, sweepProgress is the key to rotation, and animation control is handy.

private val renderAnimator by lazy {
        ValueAnimator.ofInt(0, 60)
                .apply {
                    interpolator = LinearInterpolator()
                    duration = 1000
                    repeatMode = ValueAnimator.RESTART
                    repeatCount = ValueAnimator.INFINITE
                    addUpdateListener {
                        postInvalidateOnAnimation()
                        fps++
                        sweepProgress++

                    }
                    addListener(object : AnimatorListenerAdapter() {
                        override fun onAnimationRepeat(animation: Animator?) {
                            super.onAnimationRepeat(animation)
                            fps = 0
                        }

                    })
                }
    }
Copy the code

You can see that the parameter Settings are executed 60 times per second. That’s 60 frames. And then when we get to 360 degrees, we just put it at 0. At this point, the implementation of antanrippleView is complete. Then animate the avatar. Add the following to the avatar click event:

 ((TanTanRippleView)findViewById(R.id.ripple)).startRipple();
                AnimatorSet set = new AnimatorSet();
                set.setInterpolator(new BounceInterpolator());
                set.playTogether(
                        ObjectAnimator.ofFloat(v,"scaleX"0.8 and 1.2 f, f, 1 f), ObjectAnimator. OfFloat (v,"scaleY"0.8 and 1.2 f, f, 1 f)); set.setDuration(1100).start();Copy the code

Interested in viewing the source code I am the source code, view more details.