• preface

Let’s cut to the chase and see how it works.

See so cool effect picture, have to admire our designer. From a programmer’s point of view, however, it’s more complicated than cool. However, what we see above is only the simplest form. The more complicated situation is the switching of primary and secondary face animations when there are multiple faces, the tracking of animations when the camera moves, and the timing control between multiple animations. In a word, UI display and various business logic make this animation extremely complicated. Today we’re going to talk about implementation on the UI without the business logic.

  • Why SurfaceView

Choose one option and give reasons why you don’t choose the other. That’s right, why not use a custom Vew here for drawing? Why introduce a SurfaceView when custom views can also be used for general animation? View can be thought of as a systemically optimized object that can be used to efficiently perform some low-frame animations, but for more flexible animations, View is not the best choice. At the same time, for ordinary views, they are drawn in the main thread of the application, and we know that on Android we cannot do time-consuming operations in the main thread, otherwise it will cause ANR. For game graphics, or for camera previews or video playback, their UIs are complex and require efficient drawing, so they are not suitable for drawing in the main thread of an application. It was necessary to generate a separate drawing surface for views that needed a complex and efficient UI, and to use a separate thread to draw the UI for those views, so the SurfaceView came into its own.

SurfaceView, which has a separate drawing surface, that is, it does not share the same drawing surface with its host window. With a separate drawing surface, the SurfaceView’S UI can be drawn in a separate thread. By not using mainthread resources, SurfaceView allows for a complex and efficient UI without causing user input to go unresponsive. The SurfaceView is typically used in conjunction with surfaceholder.callback, which requires overriding the following three methods:

@Overridepublic void surfaceCreated(SurfaceHolder holder) {
}    @Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}    @Overridepublic void surfaceDestroyed(SurfaceHolder holder) {
}Copy the code

The SurfaceHolder object can be retrieved from the SurfaceView’s getHolder() function, and the canvas can be retrieved from the child thread by mholder.lockCanvas (). Draw after call mHolder. UnlockCanvasAndPost (canvas) to release the canvas, and draw the contents of the display.

  • You know ObjectAnimator

Property animation: changes the property values of all objects that can be changed. The internal workings of ObjectAnimator are animated by finding the get and set methods for a particular property and changing the values through the methods. ObjectAnimator provides methods such as ofInt, ofFloat, and ofObject to interpolate properties. These methods set the element to be animated, the properties to be animated, the start and end of the animation, and any intermediate property values. During animation updates, setPropName is constantly called to update the element’s properties, so ObjectAnimator updates a property must have setter methods. ObjectAnimator is often used in conjunction with AnimatorSet:

if (null == mBCRotateAnimator1) {
    mBCRotateAnimator1 = ObjectAnimator.ofFloat(this."bCRotate".0f.360f);
    mBCRotateAnimator1.setInterpolator(new LinearInterpolator());
    mBCRotateAnimator1.setDuration(866);
}        if (null == mBCRotateAnimator2) {    mBCRotateAnimator2 = ObjectAnimator.ofFloat(this."bCRotate".360f.720f);
    mBCRotateAnimator2.setInterpolator(new LinearInterpolator());
    mBCRotateAnimator2.setDuration(334);
}        if (null == mBCRotateAnimator3) {
    mBCRotateAnimator3 = ObjectAnimator.ofFloat(this."bCRotate".720f.360f);
    mBCRotateAnimator3.setInterpolator(new LinearInterpolator());
    mBCRotateAnimator3.setDuration(12000);
    mBCRotateAnimator3.setRepeatCount(ValueAnimator.INFINITE);
}        if (null == mBCRotateAnimatorSet) {
    mBCRotateAnimatorSet = new AnimatorSet();
    ArrayList<Animator> animatorArrayList = new ArrayList<Animator>();
    animatorArrayList.add(mBCRotateAnimator1);
    animatorArrayList.add(mBCRotateAnimator2);
    animatorArrayList.add(mBCRotateAnimator3);
    mBCRotateAnimatorSet.playSequentially(animatorArrayList);
    mBCRotateAnimatorSet.setStartDelay(800);
}Copy the code
  • Useful and powerful trig functions

Did you notice there’s a couple of little triangles in the circle animation? If you look a little bit more carefully, you’ll see that this is an equilateral triangle, and one of the vertices is facing the center of the larger circle, and it rotates with the larger circle, right? As stated in the title, all elements here are self-drawn, so the two triangles are drawn on Canvas instead of icon resources given by the design students, so here comes the problem:

  • How do you determine the position of a triangle? (You can’t draw it out of the circle.)

  • How to determine the position of the three vertices of a triangle? (I don’t want to draw it crooked.)

I’m going to do a little bit of math here, and we’ll talk more about it later.

Given the center of a circle (a, b), radius r, and Angle m, find the coordinates of points on the circle. * the solution: If the coordinates of the center of the circle are (a, b), then the equation of the circle is (x-a)^2+(y-b)^2 = r^2. The coordinates of the points on the circle are (r*cos(m* math.pi /180)+a, r*sin(m* math.pi /180)+b). * Solution: You can draw the outer circle of the triangle, and then you can transform the problem into finding the coordinates of the three points on the circle. Since it is an equilateral triangle, the angles of each point are known. Same as above. * /Copy the code
  • Face recognition animation fully resolved

All animation elements can be broken down into the following types, and we’ll focus on the first one, the scan control, because this is the most difficult

First, take a rough look at the design draft of the scan control (this is not all, there are several pieces, if you can’t see clearly, you can enlarge it)

Turning animation is the core of the whole animation. Let’s take a look at it first. In fact, it is divided into several layers of circles, arcs, circles and dots, and then combined together to rotate according to their different directions and speeds. So, almost all the elements that can be changed in the animation that I usually touch are involved. (alpha, scale, rotate, color, shadow, speed, direction)

Let’s focus on the big blue circle (let’s call it the Big Blue circle) : It consists of two semi-arcs, shaded blue (and finally white), sandwiched between two symmetrical triangles, which rotate and scale at different rates over different periods of time. It is easy to draw an arc, just call canvas.drawarc (), the Angle of the arc is given. The difficulty here is to draw the gradient blue and shadow effect in different parts of the arc. To fill the gradient color, we can use the API provided by filling in the coordinates of the center of the circle, the color array, the color ratio. Finally, call the paint.setshader () method.

public SweepGradient(float cx, float cy, int colors[], float positions[]) {}Copy the code
int[] colors = new int[]{Color.argb(255.0x1E.0xFF.0xEC), Color.argb(255.0x00.0xBF.0xFF)}; // Blue circle gradient colorfloat[] positions = new float[] {0f.0.5 f}; // The gradient color occupies the positionMatrix matrix = new Matrix();
SweepGradient sweepGradient = new SweepGradient(scanningData.centerX, scanningData.centerY, colors, positions);
matrix.setRotate(scanningData.bCRightAngleStart+scanningData.bCRotate - 5, scanningData.centerX, scanningData.centerY);// Do you need to subtract 5°?sweepGradient.setLocalMatrix(matrix);
scanningData.mBCPaint.setShader(sweepGradient);Copy the code

The shadow effect can be implemented by calling the parameters provided by the API, including the shadow radius and Blur type. Finally, call the paint.setmaskFilter () method.

public BlurMaskFilter(float radius, Blur style) {}Copy the code

Now if we look at how the triangle on the big blue circle is drawn, we’ll just look at the top left corner, and the bottom right corner is in a symmetric position. If the positive direction of the clockwise X axis is 0°, then according to the initial state of the design draft, it can be calculated that the initial Angle of the triangle in the upper left corner is about 225°, and the initial Angle of the triangle in the lower right corner is about 45°. The ultimate goal here is to figure out the coordinates of the three vertices of a triangle, and then connect them together to form a triangle, but we can’t do that by directly calculating the coordinates of the vertices of a triangle. Now, let’s break it down: first, figure out the center coordinates of the triangle based on the trigonometric function, and then, since this is an equilateral triangle, figure out the coordinates of the vertices based on the trigonometric function.

Because of the triangle is on the circumference of a circle, assumes that the center of the circle (a, b), and the radius r, m and triangle in the point of view, in fact this a few variables are known, center coordinates (a, b) is the center of the face, can be returned after facial recognition by a rectangular coordinates, radius r is the design draft for the initial radius, 225 ° Angle m is just we calculated, X = r*cos(m* math.pi /180)+a, y = r*sin(m* math.pi /180)+b. The next step is to calculate the coordinates of the three vertices of the triangle. The idea is the same: draw the circumferential circle of the triangle, and the central coordinate of the triangle is the center of the circumferential circle. The problem can be transformed into finding the coordinates of the three points on the circumferential circle. Is it back to the above solution process? Yes. We know the radius of the circumferent circle (given in the design), the coordinates of the center of the circle, and now we need to know the angles of the three vertices of the triangle, and then we can figure out their coordinates. We know that this is an equilateral triangle, and one of the vertices points to the center of the big circle, so let’s call that vertex P. Draw the peripheral circle of the triangle, the position of vertex P relative to the peripheral circle is the position of the triangle on the lower right relative to the big blue circle, because these two triangles are symmetric, and the vertices are opposite. So the Angle of vertex P relative to the circle is 45 degrees. As shown in figure:

The first vertex has an Angle of 45°, the second vertex has an Angle of 45°+120° = 165°, and the third vertex has an Angle of 165°+120°=285°. Path.lineto (), path.close () and other methods describe the Path of the triangle, and finally call Canvas.drawPath() to draw the triangle, and the drawing process of the triangle is ended here.

Given the center of a circle (a, b), radius r, and Angle m, find the coordinates of points on the circle. * the solution: If the coordinates of the center of the circle are (a, b), then the equation of the circle is (x-a)^2+(y-b)^2 = r^2. The coordinates of the points on the circle are (r*cos(m* math.pi /180)+a, r*sin(m* math.pi /180)+b). * Solution: You can draw the outer circle of the triangle, and then you can transform the problem into finding the coordinates of the three points on the circle. Since it is an equilateral triangle, the angles of each point are known. Same as above. * /scanningData.bTLeftTopX = (float)((scanningData.bCRadius * scanningData.bCScale) * Math.cos((scanningData.bTLeftTopAngle + scanningData.bCRotate) * Math.PI / 180) + scanningData.centerX);scanningData.bTLeftTopY = (float)((scanningData.bCRadius * scanningData.bCScale) * Math.sin((scanningData.bTLeftTopAngle + scanningData.bCRotate) * Math.PI / 180) + scanningData.centerY);float tempAngle1 = scanningData.bTRightBottomAngle + scanningData.bCRotate; // Take a symmetric Angle, because this is the Angle inside the small triangular circle, not the Angle of the large circlefloat tempAngle2 = tempAngle1 + 120f; // Equilateral triangle, so add 120°float tempAngle3 = tempAngle2 + 120f;if (tempAngle1 >= 360) {
    tempAngle1 = tempAngle1 - 360;}  if (tempAngle2 >= 360) {
    tempAngle2 = tempAngle2 - 360;}  if (tempAngle3 >= 360) {
    tempAngle3 = tempAngle3 - 360;}  // Find the coordinates of the three vertices of a triangle.float vertex1_x = (float) (scanningData.bTRadius * scanningData.bCScale * Math.cos(tempAngle1 * Math.PI / 180) + scanningData.bTLeftTopX); float vertex1_y = (float) (scanningData.bTRadius * scanningData.bCScale * Math.sin(tempAngle1 * Math.PI / 180) + scanningData.bTLeftTopY);  float vertex2_x = (float) (scanningData.bTRadius * scanningData.bCScale * Math.cos(tempAngle2 * Math.PI / 180) + scanningData.bTLeftTopX);  float vertex2_y = (float) (scanningData.bTRadius * scanningData.bCScale * Math.sin(tempAngle2 * Math.PI / 180) + scanningData.bTLeftTopY);  float vertex3_x = (float) (scanningData.bTRadius * scanningData.bCScale * Math.cos(tempAngle3 * Math.PI / 180) + scanningData.bTLeftTopX);  float vertex3_y = (float) (scanningData.bTRadius * scanningData.bCScale * Math.sin(tempAngle3 * Math.PI / 180) + scanningData.bTLeftTopY);  Path path = new Path();path.moveTo(vertex1_x, vertex1_y);path.lineTo(vertex2_x, vertex2_y);path.lineTo(vertex3_x, vertex3_y);path.close();scanningData.mBTPaint.setColor(scanningData.bTLeftTopColor);  scanningData.mBTPaint.setAlpha((int) (scanningData.bTAlpha * 255));canvas.drawPath(path, scanningData.mBTPaint);Copy the code

Ok, the hardest part is done, and the rest of the outer, inner, transparent, and so on are fine. Well, next, we should get it moving. To rotate the animation, we can use a variable rotate on top of the ObjectAnimator and add the rotate change to it. Scale, alpha, and so on, you have to add those values when you draw.

if (null == mBCScaleAnimator1) {
    mBCScaleAnimator1 = ObjectAnimator.ofFloat(this."bCScale".0f.1.04 f);
    mBCScaleAnimator1.setInterpolator(new LinearInterpolator());
    mBCScaleAnimator1.setDuration(200);
}if (null == mBCScaleAnimator2) {
    mBCScaleAnimator2 = ObjectAnimator.ofFloat(this."bCScale".1.04 f.1f);
    mBCScaleAnimator2.setInterpolator(new LinearInterpolator());
    mBCScaleAnimator2.setDuration(66);
}if (null == mBCScaleAnimator3) {
    mBCScaleAnimator3 = ObjectAnimator.ofFloat(this."bCScale".1f.1.02 f);
    mBCScaleAnimator3.setInterpolator(new LinearInterpolator());
    mBCScaleAnimator3.setDuration(66);
}Copy the code
scanningData.bTRadius * scanningData.bCScale * Math.cos(tempAngle1 * Math.PI / 180)
scanningData.bTRadius * scanningData.bCScale * Math.sin(tempAngle1 * Math.PI / 180)
scanningData.bTRadius * scanningData.bCScale * Math.cos(tempAngle2 * Math.PI / 180)
scanningData.bTRadius * scanningData.bCScale * Math.sin(tempAngle2 * Math.PI / 180)
scanningData.bTRadius * scanningData.bCScale * Math.cos(tempAngle3 * Math.PI / 180)
scanningData.bTRadius * scanningData.bCScale * Math.sin(tempAngle3 * Math.PI / 180)Copy the code

DrawLine (), canvas.drawtext (), canvas.drawcircle (), etc. (in fact, the position and Angle of the line also need to use trigonometry to calculate). Here is a simple analysis of the ripple effect of the small picture on the right.

Actually, there’s another triangle that needs to be drawn. Do you feel the collapse? It doesn’t matter, or according to our above routine, change the initial Angle can be, algorithm in hand, TRIANGLE I have! In fact, the drawing of the ripple effect is relatively simple. Call Canvas.drawcircle () and use ObjectAnimator to change the alpha and scale values of the circle to achieve the ripple effect. See the following code

if (null == mFPOutCircleAlphaAnimator) {
    mFPOutCircleAlphaAnimator = ObjectAnimator.ofFloat(this."fPOutCircleAlpha".1f.0f.0f);
    mFPOutCircleAlphaAnimator.setInterpolator(new LinearInterpolator());
    mFPOutCircleAlphaAnimator.setDuration(1500);
    mFPOutCircleAlphaAnimator.setRepeatCount(ValueAnimator.INFINITE);
}if (null == mFPOutCircleAlphaAnimatorSet) {
    mFPOutCircleAlphaAnimatorSet = new AnimatorSet();
    mFPOutCircleAlphaAnimatorSet.play(mFPOutCircleAlphaAnimator);
    mFPOutCircleAlphaAnimatorSet.setStartDelay(2200);
}if (null == mFPOutCircleScaleAnimator) {
     mFPOutCircleScaleAnimator = ObjectAnimator.ofFloat(this."fPOutCircleScale".1f.1.3 f.1.3 f);
     mFPOutCircleScaleAnimator.setInterpolator(new LinearInterpolator());
     mFPOutCircleScaleAnimator.setRepeatCount(ValueAnimator.INFINITE);
     mFPOutCircleScaleAnimator.setDuration(1500);
}if (null == mFPOutCircleScaleAnimatorSet) {
      mFPOutCircleScaleAnimatorSet = new AnimatorSet();
      mFPOutCircleScaleAnimatorSet.play(mFPOutCircleScaleAnimator);
      mFPOutCircleScaleAnimatorSet.setStartDelay(2200);
}

scanningData.mFPOutCirclePaint.setAlpha((int)(scanningData.fPOutCircleAlpha * 255 * 0.5));
canvas.drawCircle(scanningData.fPCenterX, scanningData.fPCenterY, scanningData.fPOutCircleRadius * scanningData.fPOutCircleScale, scanningData.mFPOutCirclePaint);Copy the code

At this point, the entire animation is drawn.