preface
Hi everyone, I’m the Crimson Knight. I love to joke, I’m bad at technology and I love to study. This article is the last one of the year. Today I’m going to learn bezier curves, which I’ve always wanted to learn but didn’t have the time to do. What is a Bezier curve? I didn’t understand it at the beginning. After checking a lot of materials, I still didn’t know enough about it, and the derivation formula still couldn’t be thoroughly understood. In 1962, Pierre Bessel used Bessel curves to design the body of a car, Bessel curve was first developed by Paul de Casteljau in 1959 using de Casteljau algorithm, which is a stable reporting method to find Bessel curve. In fact, Bessel curve is in our daily life, such as some mature bitmap software: PhotoSHop, Flash5 and so on. Bezier curves are also ubiquitous in front end development: front end 2D or 3D graphics icon libraries use Bezier curves; It can be used to draw curves. In SVG and Canvas, curve drawing provided natively is implemented using Bezier curves. In the transition-timing-function property of the CSS, Bessel curves can be used to describe the slow calculation of transitions.
Bezier curve principle
Bezier curves control the state of the curve with a series of points, which I divide into three points: starting point, end point and control point. By changing these points, the Bezier curve changes.
- Starting point: Determines the starting point of the curve
- Endpoint: Determine the endpoint of a curve
- Control point: Determine the control point of the curve
First order curve principle
A first order curve is a straight line, with only two points, the beginning and the end, and the final effect is a line segment. The above picture is more intuitive:
Second order curve principle
A second-order curve consists of two data points (starting point and end point) and one control point to describe the curve state, as shown in the figure below. Point A is the starting point, C is the end point, and B is the control point.
The starting point
At the end of
Third order curve principle
The third-order curve is actually described by two data points (starting point and end point) and two control points, as shown in the figure below. Below, A is the starting point, D is the end point, and B and C are the control points.
Bezier curves in Android
In Android, there are four methods in the Path class that are related to the Bezier curve, that is, functions that have been wrapped around the Bezier curve, and developers can call them directly:
// Second order Bezier
public void quadTo(float x1, float y1, float x2, float y2);
public void rQuadTo(float dx1, float dy1, float dx2, float dy2);
// Third order Bessel
public void cubicTo(float x1, float y1, float x2, float y2,float x3, float y3);
public void rCubicTo(float x1, float y1, float x2, float y2,float x3, float y3);
Copy the code
Among the above four functions, quadTo and rQuadTo are second-order Bessel curves, and cubicTo and rCubicTo are third-order Bessel curves. Because third-order Bezier curves are used in a similar way to second-order Bezier curves, they are also less useful, so I won’t go into details. The following is a detailed description of quadTo and rQuadTo Bessel curves of second order.
QuadTo principle
First look at the quadTo function definition:
/**
* Add a quadratic bezier from the last point, approaching control point
* (x1,y1), and ending at (x2,y2). If no moveTo() call has been made for
* this contour, the first point is automatically set to (0,0).
*
* @param x1 The x-coordinate of the control point on a quadratic curve
* @param y1 The y-coordinate of the control point on a quadratic curve
* @param x2 The x-coordinate of the end point on a quadratic curve
* @param y2 The y-coordinate of the end point on a quadratic curve
*/
public void quadTo(float x1, float y1, float x2, float y2) {
isSimplePath = false;
nQuadTo(mNativePath, x1, y1, x2, y2);
}
Copy the code
(x1,y1) is the control point,(x2,y2) is the terminal point, why there is no starting point coordinates? As Android developers know, the starting point of a line segment is specified by path.move (x,y). If the quadTo function is called consecutively, the end point of the previous quadTo function is the start point of the next quadTo function. If path.moveto (x,y) is not called to specify the start point, the control view will start at the upper left corner (0,0). The following implementation draws the following renderings:
Pay attention to
The sample code
public class PathView extends View {
/ / brush
private Paint paint;
/ / path
private Path path;
public PathView(Context context) {
super(context);
}
public PathView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
// Override the onDraw method
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setStyle(Paint.Style.STROKE);
// Line width
paint.setStrokeWidth(10);
paint.setColor(Color.RED);
// Set the starting position to (200,400)
path.moveTo(200.400);
// line P0-P2 control point (300,300)
path.quadTo(300.300.400.400);
// Line P2-P4 control point (500,500) end position (600,400)
path.quadTo(500.500.600.400);
canvas.drawPath(path, paint);
}
private void init(a) {
paint = new Paint();
path = newPath(); }}Copy the code
The layout file is as follows:
<? The XML version = "1.0" encoding = "utf-8"? > <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#000000"> <Button android:id="@+id/btn_reset" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" Android: layout_marginTop = "10 dp" android: text = "empty path" / > < com. Example. Okhttpdemo. PathView android: id = "@ + id/path_view" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/btn_reset" android:background="#000000"/> </android.support.constraint.ConstraintLayout>Copy the code
The effect picture is as follows:
Path. The moveTo (200400);
- When a quadTo function is called consecutively, the end point of the previous quadTo function is the start point of the next quadTo function.
- The starting point of the bezier curve is specified by path.moveto (x,y). If path.move (x,y) is not called initially, the upper left corner (0,0) of the control is taken as the starting point.
Path.lineTo and path. quadTo
Path.lineTo is a line connecting the previous point to the current point. In order to draw the Path of your finger on the screen, add onTouchEvent to the above method. The code is as follows:
public class PathView extends View {
/ / brush
private Paint paint;
/ / path
private Path path;
public PathView(Context context) {
super(context);
}
public PathView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
// Override the onDraw method
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setStyle(Paint.Style.STROKE);
// Line width
// paint.setStrokeWidth(10);
paint.setColor(Color.RED);
canvas.drawPath(path, paint);
}
private void init(a) {
paint = new Paint();
path = new Path();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
Log.d("ssd"."Trigger press");
path.moveTo(event.getX(), event.getY());
return true;
}
case MotionEvent.ACTION_MOVE:
Log.d("ssd"."Trigger move");
path.lineTo(event.getX(), event.getY());
invalidate();
break;
default:
break;
}
return super.onTouchEvent(event);
}
public void reset(a) { path.reset(); invalidate(); }}Copy the code
Direct renderings:
MotionEvent.DOWN
path.move(event.getX(),event.getY())
path.lineTo(event.getX,event.getY())
invalidate
MotionEvent.ACTION_DOWN
return true
return true
ACTION_UP
ACTION_MOVE
case MotionEvent.ACTION_DOWN
return false
MOTION_MOVE
MMOTION_UP
ACTION_DOWN
ACTION_DOWN
ACTION_MOVE
ACTION_UP
The midpoints of the two lines are the starting and ending points respectively
The control points
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
Log.d("ssd"."Trigger press");
path.moveTo(event.getX(), event.getY());
// Save the coordinates of this point
mBeforeX = event.getX();
mBeforeY = event.getY();
return true;
}
case MotionEvent.ACTION_MOVE:
Log.d("ssd"."Trigger move");
// Draw a second-order curve as you move
// The endpoint is the midpoint of the segment
endX = (mBeforeX + event.getX()) / 2;
endY = (mBeforeY + event.getY()) / 2;
// Draw a second-order curve
path.quadTo(mBeforeX,mBeforeY,endX,endY);
// Then update the coordinates of the previous point
mBeforeX = event.getX();
mBeforeY = event.getY();
invalidate();
break;
default:
break;
}
return super.onTouchEvent(event);
}
Copy the code
ACTION_DOWN: path.moveto (event.getx (), event.gety ()); Set the initial position of the curve to be where the finger touches the screen. It is explained that if moveTo(event.getx (), event.gety ()) is not called, the drawing point will start at (0,0) of the control. Use mBeforeX and mBeforeY to record the first horizontal and vertical coordinates of the finger movement, which is the control point, and return true in order for ACTION_MOVE and ACTION_UP to be passed to the control. EndX = (mBeforeX + event.getx ()) / 2; endX = (mBeforeX + event.getx ()) / 2; And endY = (mBeforeY + event.gety ()) / 2; Find the horizontal and vertical coordinates of the middle position, and the control point is the position of the last finger touch screen, followed by the update of the previous finger coordinates. Note that when you call “quardTo” in a row, the first start point is set to path.moveto (x,y). The rest of the quadTo is the start point of the next quard, which is the middle point of the previous segment. The logic above is expressed in one sentence: The middle point of each line segment is used as the starting point and ending point, and the position of the previous finger is used as the control point. The final effect is as follows:
quadT
Path. RQuadTo principle
Look directly at the description of this function:
/**
* Add a quadratic bezier from the last point, approaching control point
* (x1,y1), and ending at (x2,y2). If no moveTo() call has been made for
* this contour, the first point is automatically set to (0,0).
*
* @param x1 The x-coordinate of the control point on a quadratic curve
* @param y1 The y-coordinate of the control point on a quadratic curve
* @param x2 The x-coordinate of the end point on a quadratic curve
* @param y2 The y-coordinate of the end point on a quadratic curve
*/
public void quadTo(float x1, float y1, float x2, float y2) {
isSimplePath = false;
nQuadTo(mNativePath, x1, y1, x2, y2);
}
Copy the code
- X1: The X coordinate of the control point, representing the displacement value relative to the X coordinate of the last endpoint, can be negative, positive value means addition, negative value means subtraction
- X2: the Y coordinate of the control point, representing the displacement value relative to the Y coordinate of the last terminal point, which can be negative, positive value means addition, negative value means subtraction
- X2: X coordinate of the end point, representing the displacement value relative to the X coordinate of the last end point, which can be negative, positive value means addition, negative value means subtraction
- Y2: the y-coordinate of the end point, which represents the displacement value relative to the y-coordinate of the previous end point, can be negative, positive value means addition, negative value means subtraction. If the last endpoint coordinate is (100,200), if rQuardTo(100,-100,200,200) is called at this time, the control point coordinate obtained is (100 + 100,200-100), which is (200,100). The resulting endpoint coordinate is (100 + 200,200 + 200) which is (300,400). The following two segments are equal:
path.moveTo(100.200);
path.quadTo(200.100.300.400);
Copy the code
path.moveTo(100.200);
path.rQuadTo(100, -100.200.200);
Copy the code
In the above, quadTo implements a wavy line, as shown below:
quadTo
// Override the onDraw method
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setStyle(Paint.Style.STROKE);
// Line width
paint.setStrokeWidth(10);
paint.setColor(Color.RED);
// Set the starting position to (200,400)
path.moveTo(200.400);
// line P0-P2 control point (300,300)
path.quadTo(300.300.400.400);
// Line P2-P4 control point (500,500) end position (600,400)
path.quadTo(500.500.600.400);
canvas.drawPath(path, paint);
}
Copy the code
Below, rQuadTo is used to realize this wavy line. First, the analysis figure is shown:
// Override the onDraw method
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setStyle(Paint.Style.STROKE);
// Line width
paint.setStrokeWidth(10);
paint.setColor(Color.RED);
// Set the starting position to (200,400)
path.moveTo(200.400);
//线条p0-p2控制点(300,300) 终点坐标位置(400,400)
path.rQuadTo(100, -100.200.0);
//线条p2-p4控制点(500,500) 终点坐标位置(600,400)
path.rQuadTo(100.100.200.0);
canvas.drawPath(path, paint);
}
Copy the code
First line: path.rquadto (100,-100,200,0); This line of code calculates the control points and endpoint coordinates of curves P0-P2 based on the point (200,400).
- X-coordinate of control point = X-coordinate of last endpoint + x-displacement value of control point = 200 + 100 = 300;
- Control point Y coordinate = last terminal point Y coordinate + control point Y displacement value = 400-100 = 300;
- End point X coordinate = the X coordinate of the previous end point + the displacement value of end point X = 200 + 200 = 400;
- End point Y coordinate = last end point Y coordinate + end point Y displacement value = 400 + 0 = 400; Path. quadTo(300,300,400,400)
Path. rQuadTo(100,100,200,0) calculates the control points and end points of the second curve based on this end point (400,400).
- X-coordinate of control point = X-coordinate of last endpoint + x-displacement value of control point = 400 + 100 = 500;
- Control point Y coordinate = last terminal point Y coordinate + control point Y displacement value = 400 + 100 = 500
- End point X coordinate = the X coordinate of the previous end point + the displacement value of end point X = 400 + 200 = 600;
- End point Y coordinate = last end point Y coordinate + end point Y displacement value = 400 + 0 = 400;
Path. rQuadTo(100,100,200,0); Is and path. The quadTo (500500600400); Similarly, the effect map of actual operation is the same as that drawn by quadTo method. Through this example, it can be known that the parameters of quadTo method are the coordinates of actual results, while the parameters of rQuadTo method are the displacement based on the above terminal position.
Realize closed wave
The following effects should be achieved:
Achieve static closed wave
The corresponding code is as follows:
// Override the onDraw method
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
path.reset();
// Set the fill drawing
paint.setStyle(Paint.Style.FILL_AND_STROKE);
// Line width
//paint.setStrokeWidth(10);
paint.setColor(Color.RED);
int control = waveLength / 2;
// Start with the initial state (-400,1200)
path.moveTo(-waveLength,origY);
// Because the width of the whole wave is the width of the View plus the left and right wavelengths
for(inti = -waveLength; i <= getWidth() + waveLength; i += waveLength){ path.rQuadTo(control /2, -70,control,0);
path.rQuadTo(control / 2.70,control,0);
}
path.lineTo(getWidth(),getHeight());
path.lineTo(0,getHeight());
path.close();
canvas.drawPath(path, paint);
}
Copy the code
Here’s a line by line analysis:
// Start with the initial state (-400,1200)
path.moveTo(-waveLength,origY);
Copy the code
First move the starting position of the Path to the left by one wavelength, in order to animate the subsequent displacement, and then use the loop to draw all the waves in the screen:
for(inti = -waveLength; i <= getWidth() + waveLength; i += waveLength){ path.rQuadTo(control /2, -70,control,0);
path.rQuadTo(control / 2.70,control,0);
}
Copy the code
Path. rQuadTo(Control / 2,-70, Control,0); The first line in the loop shows the first half of a waveLength. This is easy to understand by plugging in the values below, because the waveLength is 400, so control = waveLength / 2 is 200. Path. rQuadTo(control / 2,-70,control,0) is path.rQuadTo(100,-70,200,0), Path. rQuadTo(control / 2,70,control,0) is path.rQuadTo(100,70,200,0). Coordinates of control points, and other waves are just drawn through cycles without analysis:
rQuadTo
paint.setStyle(Paint.Style.FILL_AND_STROKE);
path
path.lineTo(getWidth(),getHeight());
path.lineTo(0,getHeight());
path.close();
Realize the wave of displacement animation
The following implementation or displacement of the animation, it will feel a little bit of progress bar, my approach is very simple, because more than a start on the left side of the View drew a wave, that is to say, will move to the right starting point, and the length of a wave to move can make corrugated overlapping, then cycle, simply is, The distance the animation moves is the length of a wave, and when it moves to the maximum distance the setting is repeated to redraw the initial state of the wave.
/** * Animation shift method */
public void startAnim(a){
// Create an animation instance
ValueAnimator moveAnimator = ValueAnimator.ofInt(0,waveLength);
// The animation time
moveAnimator.setDuration(2500);
// Set the number of animations to INFINITE to indicate an INFINITE loop
moveAnimator.setRepeatCount(ValueAnimator.INFINITE);
// Set the animation interpolation
moveAnimator.setInterpolator(new LinearInterpolator());
// Add a listener
moveAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
moveDistance = (int)animation.getAnimatedValue(); invalidate(); }});// Start animation
moveAnimator.start();
}
Copy the code
The distance moved by the animation is the length of a wave and is saved to moveDistance. Then, to start the animation, add this distance to moveTo. The complete code is as follows:
// Override the onDraw method
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Clear the path to redraw must be added or rectangle
path.reset();
// Set the fill drawing
paint.setStyle(Paint.Style.FILL_AND_STROKE);
// Line width
//paint.setStrokeWidth(10);
paint.setColor(Color.RED);
int control = waveLength / 2;
// Start with the initial state (-400,1200)
path.moveTo(-waveLength + moveDistance,origY);
for(inti = -waveLength; i <= getWidth() + waveLength; i += waveLength){ path.rQuadTo(control /2, -70,control,0);
path.rQuadTo(control / 2.70,control,0);
}
path.lineTo(getWidth(),getHeight());
path.lineTo(0,getHeight());
path.close();
canvas.drawPath(path, paint);
}
Copy the code
The effect is as follows:
path.moveTo(-waveLength + moveDistance,origY - moveDistance);
pathView = findViewById(R.id.path_view);
pathView.startAnim();
Copy the code
The effect is shown above. After the above, their initial understanding of Bessel curve, the following to achieve the wavy progress bar.
Implement wave progress bar
Once you have learned the basics above, here is a small example of how to implement the circular wavy progress bar. The final effect is at the bottom of the article.
Draw a wave
First draw a section of full screen wavy line, drawing principle will not be detailed, directly on the code:
/** * Describe: Created by Knight on 2019/2/1 * See the world by bit **/
public class CircleWaveProgressView extends View {
// Draw a wave brush
private Paint wavePaint;
// Draw wave Path
private Path wavePath;
// Wave width
private float waveLength;
// Wave height
private float waveHeight;
public CircleWaveProgressView(Context context) {
this(context,null);
}
public CircleWaveProgressView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public CircleWaveProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
/** * Initializes some brush path configuration *@param context
*/
private void init(Context context){
// Set the wave width
waveLength = Density.dip2px(context,25);
// Set the wave height
waveHeight = Density.dip2px(context,15);
wavePath = new Path();
wavePaint = new Paint();
wavePaint.setColor(Color.parseColor("#ff7c9e"));
// Set anti-aliasing
wavePaint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
// Draw wavy lines
canvas.drawPath(paintWavePath(),wavePaint);
}
/** * draw wavy lines **@return* /
private Path paintWavePath(a){
Clear the route first
wavePath.reset();
// Start at (0,waveHeight)
wavePath.moveTo(0,waveHeight);
for(int i = 0; i < getWidth() ; i += waveLength){ wavePath.rQuadTo(waveLength /2,waveHeight,waveLength,0);
wavePath.rQuadTo(waveLength / 2,-waveHeight,waveLength,0);
}
returnwavePath; }}Copy the code
XML layout file:
<? The XML version = "1.0" encoding = "utf-8"? > <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" > <com.example.progressbar.CircleWaveProgressView android:id="@+id/circle_progress" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintRight_toRightOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" /> </android.support.constraint.ConstraintLayout>Copy the code
The actual effect is as follows:
Draw closed static waves
As the wave in the circular progress box rises with the increase of progress, the wave is a filler. Draw the wave first, and then use path.lineTo and path.close to connect and close to form a filling graph.
public class CircleWaveProgressView extends View {
// Draw a wave brush
private Paint wavePaint;
// Draw wave Path
private Path wavePath;
// Wave width
private float waveLength;
// Wave height
private float waveHeight;
// The number of wave groups a wave is one low and one high
private int waveNumber;
// Customize the wave width and height of the View
private int waveDefaultSize;
// The maximum width and height of a custom View is higher than the wave
private int waveMaxHeight;
public CircleWaveProgressView(Context context) {
this(context,null);
}
public CircleWaveProgressView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public CircleWaveProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
/** * Initializes some brush path configuration *@param context
*/
private void init(Context context){
// Set the wave width
waveLength = Density.dip2px(context,25);
// Set the wave height
waveHeight = Density.dip2px(context,15);
// Set the width and height of the custom View
waveDefaultSize = Density.dip2px(context,250);
// Set the maximum width and height of the custom View
waveMaxHeight = Density.dip2px(context,300);
Math.ceil(a) returns the smallest integer not less than a
// For example:
/ / Math. Ceil (125.9) = 126.0
/ / Math. Ceil (0.4873) = 1.0
/ / Math. Ceil (0.65) = 0.0
Use ceil to make sure that the View is fully filled with waves to prepare for looping. The smaller the denominator is, the more accurate it is
waveNumber = (int) Math.ceil(Double.parseDouble(String.valueOf(waveDefaultSize / waveLength / 2)));
wavePath = new Path();
wavePaint = new Paint();
// Set the color
wavePaint.setColor(Color.parseColor("#ff7c9e"));
// Set anti-aliasing
wavePaint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
// Draw wavy lines
canvas.drawPath(paintWavePath(),wavePaint);
Log.d("ssd",getWidth()+"");
}
/** * draw wavy lines **@return* /
private Path paintWavePath(a){
Clear the route first
wavePath.reset();
// Start at (0,waveHeight)
wavePath.moveTo(0,waveMaxHeight - waveDefaultSize);
// The maximum number of waves can be drawn
I < getWidth(); I +=waveLength is not perfect
// Draw p0-p1 draw wavy lines
for(int i = 0; i < waveNumber ; i ++){ wavePath.rQuadTo(waveLength /2,waveHeight,waveLength,0);
wavePath.rQuadTo(waveLength / 2,-waveHeight,waveLength,0);
}
// connect p1-p2
wavePath.lineTo(waveDefaultSize,waveDefaultSize);
// Connect p2-P3
wavePath.lineTo(0,waveDefaultSize);
// Connect p3-P0
wavePath.lineTo(0,waveMaxHeight - waveDefaultSize);
// Close and fill
wavePath.close();
return wavePath;
}
Copy the code
Measure the width and height of the adaptive View
The width and height of a View are defined in an XML file or in a class file, so we need to rewrite the onMeasure method of a View:
@Override
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
int height = measureSize(waveDefaultSize, heightMeasureSpec);
int width = measureSize(waveDefaultSize, widthMeasureSpec);
// Get the shortest edge of the View
int minSize = Math.min(height,width);
// Change View to a square
setMeasuredDimension(minSize,minSize);
//waveActualSize is the actual width and height
waveActualSize = minSize;
Math.ceil(a) returns the smallest integer not less than a
// For example:
/ / Math. Ceil (125.9) = 126.0
/ / Math. Ceil (0.4873) = 1.0
/ / Math. Ceil (0.65) = 0.0
Use ceil to make sure that the View is fully filled with waves to prepare for looping. The smaller the denominator is, the more accurate it is
waveNumber = (int) Math.ceil(Double.parseDouble(String.valueOf(waveActualSize / waveLength / 2)));
}
/** * returns the specified value *@paramDefaultSize The default value *@paramMeasureSpec mode *@return* /
private int measureSize(int defaultSize,int measureSpec) {
int result = defaultSize;
int specMode = View.MeasureSpec.getMode(measureSpec);
int specSize = View.MeasureSpec.getSize(measureSpec);
/ / the MeasureSpec. EXACTLY: if it is match_parent or setting fixed value
/ / the MeasureSpec. AT_MOST: wrap_content
if (specMode == View.MeasureSpec.EXACTLY) {
result = specSize;
} else if (specMode == View.MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
return result;
}
Copy the code
Add a variable waveActualSize to the View’s actual width and height to make the code more scalable and accurate.
Plot wave rise
Implementation under the wave height changes along with the progress, when the progress increases, the wave height increases, when the schedule to reduce wave height decreases, actually very simple, namely p0 – p3, the height of the p1 and p2 changes according to the schedule, and increase the animation, increase code is as follows:
// Ratio of current progress value to total progress value
private float currentPercent;
// Current progress value
private float currentProgress;
// Maximum progress
private float maxProgress;
// Animate objects
private WaveProgressAnimat waveProgressAnimat;
/** * Initializes some brush path configuration *@param context
*/
private void init(Context context){
/ /...
// The ratio is initially set to 0
currentPercent = 0;
// Progress bar The progress is set to 0
currentProgress = 0;
// Set the maximum value of the progress bar to 100
maxProgress = 100;
// Animate instantiate
waveProgressAnimat = new WaveProgressAnimat();
}
/** * draw wavy lines **@return* /
private Path paintWavePath(a){
Clear the route first
wavePath.reset();
// Start point moved to (0,waveHeight) p0-P1 height changes with progress
wavePath.moveTo(0, (1 - currentPercent) * waveActualSize);
// The maximum number of waves can be drawn
I < getWidth(); I +=waveLength is not perfect
// Draw p0-p1 draw wavy lines
for(int i = 0; i < waveNumber ; i ++){ wavePath.rQuadTo(waveLength /2,waveHeight,waveLength,0);
wavePath.rQuadTo(waveLength / 2,-waveHeight,waveLength,0);
}
// connect p1-p2
wavePath.lineTo(waveActualSize,waveActualSize);
// Connect p2-P3
wavePath.lineTo(0,waveActualSize);
P3-p0 p3-P0d height changes as progress changes
wavePath.lineTo(0, (1 - currentPercent) * waveActualSize);
// Close and fill
wavePath.close();
return wavePath;
}
// Create a new animation class
public class WaveProgressAnimat extends Animation{
// applyTransformation is called repeatedly during animation drawing,
// The parameter interpolatedTime changes with each invocation. The parameter interpolates from 0 to 1, indicating that the animation is over
@Override
protected void applyTransformation(float interpolatedTime, Transformation t){
super.applyTransformation(interpolatedTime, t);
// Update the ratio
currentPercent = interpolatedTime * currentProgress / maxProgress;
// Redrawinvalidate(); }}/** * Sets the progress bar value *@paramCurrentProgress currentProgress *@paramTime Animation duration */
public void setProgress(float currentProgress,int time){
this.currentProgress = currentProgress;
// Change from 0
currentPercent = 0;
// Set the animation time
waveProgressAnimat.setDuration(time);
// Animation is enabled for the current view
this.startAnimation(waveProgressAnimat);
}
Copy the code
Finally, call some code in the Activity:
// Progress of 50 is 2500 milliseconds
circleWaveProgressView.setProgress(50.2500);
Copy the code
The final effect is shown below:
Draw wave translation left and right
Implements the above animation of the spike in the waves, the following implementation wave translational animation, add the effect of the shift to the left, here in front of the thought of also achieved the effect of translation, but this is implemented with the above is a little discrepancy, simply is the mobile p0 coordinates, but if p0 moving waves there will be not covered the whole View of the situation, A very common loop is used here. In the background scroll picture of aircraft war, two background pictures are splice together. When the plane starts from the bottom of the first background picture and moves up to the height of the first background picture, the role is put back to the bottom of the first background picture, so as to achieve the effect of background picture cycle. So you start drawing p0-P1, and then as you progress, P0 will move to the left, and the wave that wasn’t in the View at the beginning will move from the right to the left, and when you reach the maximum distance, you’ll redraw the original state, and you’ll have a loop. Let’s start with the diagram:
// Wave translation distance
private float moveDistance = 0;
/** * draw wavy lines **@return* /
private Path paintWavePath(a){
Clear the route first
wavePath.reset();
// Start point moved to (0,waveHeight) p0-P1 height changes with progress
wavePath.moveTo(-moveDistance,(1 - currentPercent) * waveActualSize);
// The maximum number of waves can be drawn
I < getWidth(); I +=waveLength is not perfect
There is a segment that is out of View, to the right of the distance to the right of View, so * 2, for horizontal displacement
for(int i = 0; i < waveNumber * 2 ; i ++){
wavePath.rQuadTo(waveLength / 2,waveHeight,waveLength,0);
wavePath.rQuadTo(waveLength / 2,-waveHeight,waveLength,0);
}
// connect p1-p2
wavePath.lineTo(waveActualSize,waveActualSize);
// Connect p2-P3
wavePath.lineTo(0,waveActualSize);
P3-p0 p3-P0d height changes as progress changes
wavePath.lineTo(0, (1 - currentPercent) * waveActualSize);
// Close and fill
wavePath.close();
return wavePath;
}
/** * Sets the progress bar value *@paramCurrentProgress currentProgress *@paramTime Animation duration */
public void setProgress(final float currentProgress, int time){
this.currentProgress = currentProgress;
// Change from 0
currentPercent = 0;
// Set the animation time
waveProgressAnimat.setDuration(time);
// Set the loop to play
waveProgressAnimat.setRepeatCount(Animation.INFINITE);
// Let the animation play at a uniform speed to avoid the phenomenon of waves moving and stopping
waveProgressAnimat.setInterpolator(new LinearInterpolator());
waveProgressAnimat.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {}@Override
public void onAnimationEnd(Animation animation) {}@Override
public void onAnimationRepeat(Animation animation) {
// When the wave reaches its peak, the speed of movement changes. Set the monitor for the animation. When the animation ends, it runs at 7000 milliseconds, slowing down
if(currentPercent == currentProgress /maxProgress){
waveProgressAnimat.setDuration(7000); }}});// Animation is enabled for the current view
this.startAnimation(waveProgressAnimat);
}
// Create a new animation class
public class WaveProgressAnimat extends Animation{
// applyTransformation is called repeatedly during animation drawing,
// The parameter interpolatedTime changes with each invocation. The parameter interpolates from 0 to 1, indicating that the animation is over
@Override
protected void applyTransformation(float interpolatedTime, Transformation t){
super.applyTransformation(interpolatedTime, t);
// When the wave height reaches the maximum, there is no circulation, only translation
if(currentPercent < currentProgress / maxProgress){
currentPercent = interpolatedTime * currentProgress / maxProgress;
}
// The distance to the left changes according to the animation progress
moveDistance = interpolatedTime * waveNumber * waveLength * 2;
// Redrawinvalidate(); }}Copy the code
The final effect is shown below:
Draws a circular frame background
Here to use the knowledge of PorterDuffXfermode, in fact, is not difficult, first on the porterduff. Mode various modes of effect:
Developers make a big mistake
Android Port DuffxferMode real effect test set (compared to the official demo)
PorterDuff.Mode.SRC_IN
PorterDuff.Mode.SRC_IN
// Round background brush
private Paint circlePaint;
//bitmap
private Bitmap circleBitmap;
/ / bitmap canvas
private Canvas bitmapCanvas;
/** * Initializes some brush path configuration *@param context
*/
private void init(Context context){
/ /...
// Start by drawing a circular background
wavePaint = new Paint();
// Set the brush to take intersection mode
wavePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
// The circular background is initialized
circlePaint = new Paint();
/ / color
circlePaint.setColor(Color.GRAY);
// Set anti-aliasing
circlePaint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
Log.d("ssd",getWidth()+"");
// The cache is used to create a new bitmap based on the parameters
circleBitmap = Bitmap.createBitmap(waveActualSize, waveActualSize, Bitmap.Config.ARGB_8888);
// Create a canvas at the base of the bitmap
bitmapCanvas = new Canvas(circleBitmap);
// Drawing the center diameter of the circle is easy
bitmapCanvas.drawCircle(waveActualSize/2, waveActualSize/2, waveActualSize/2, circlePaint);
// Draw wave shapes
bitmapCanvas.drawPath(paintWavePath(),wavePaint);
// Crop the image
canvas.drawBitmap(circleBitmap, 0.0.null);
// Draw wavy lines
// canvas.drawPath(paintWavePath(),wavePaint);
}
Copy the code
The actual effect is shown below:
res\vaules
attrs.xml
CircleWaveProgressView
<! <declare-styleable name="CircleWaveProgressView"> <! - the color of the wave - > < attr name = "wave_color format =" "color" > < / attr > <! <attr name="circlebg_color" format="color"></attr> <! <attr name="wave_length" format="dimension"></attr> <! <attr name="wave_height" format="dimension"></attr> <! <attr name="currentProgress" format="float"></attr> <! <attr name="maxProgress" format="float"></attr> </declare-styleable>Copy the code
Assign a property value to a custom View:
// Wave color
private int wave_color;
// Round background progress box color
private int circle_bgcolor;
public CircleWaveProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// Get attrs configuration attributes
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleWaveProgressView);
Dip2px (context,25); // If the XML does not specify wave_length, the default value will be used.
waveLength = typedArray.getDimension(R.styleable.CircleWaveProgressView_wave_length,Density.dip2px(context,25));
// Get the wave height
waveHeight = typedArray.getDimension(R.styleable.CircleWaveProgressView_wave_height,Density.dip2px(context,15));
// Get the wave color
wave_color = typedArray.getColor(R.styleable.CircleWaveProgressView_wave_color,Color.parseColor("#ff7c9e"));
// Round background color
circle_bgcolor = typedArray.getColor(R.styleable.CircleWaveProgressView_circlebg_color,Color.GRAY);
// Current progress
currentProgress = typedArray.getFloat(R.styleable.CircleWaveProgressView_currentProgress,50);
// Maximum progress
maxProgress = typedArray.getFloat(R.styleable.CircleWaveProgressView_maxProgress,100);
// Remember to recycle TypedArray
// A program maintains a Pool of TypedArray at runtime. When a program is called, it requests an instance from the pool and calls the Recycle () method to release the instance so that it can be reused by other modules.
// Why use this mode? One of the scenarios for using TypedArray is the above custom View, which is created each time an Activity is created.
// Therefore, the system needs to create arrays frequently, which is not a small cost to memory and performance. If you do not use pooling mode and let GC collect each time, it is likely to cause OutOfMemory.
// This is the reason for using the pool + singleton mode, which is why the official documentation repeatedly emphasizes: after use must recycle,recycle,recycle
typedArray.recycle();
init(context);
}
/** * Initializes some brush path configuration *@param context
*/
private void init(Context context){
// Set the width and height of the custom View
waveDefaultSize = Density.dip2px(context,250);
// Set the maximum width and height of the custom View
waveMaxHeight = Density.dip2px(context,300);
wavePath = new Path();
wavePaint = new Paint();
// Set the brush to take intersection mode
wavePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
// The circular background is initialized
circlePaint = new Paint();
// Set the circular background color
circlePaint.setColor(circle_bgcolor);
// Set anti-aliasing
circlePaint.setAntiAlias(true);
// Set the wave color
wavePaint.setColor(wave_color);
// Set anti-aliasing
wavePaint.setAntiAlias(true);
// The ratio is initially set to 0
currentPercent = 0;
// Progress bar The progress is set to 0
currentProgress = 0;
// Set the maximum value of the progress bar to 100
maxProgress = 100;
// Animate instantiate
waveProgressAnimat = new WaveProgressAnimat();
Copy the code
Here you can customize the wave color, height, width, and circular background color in the layout file:
<? The XML version = "1.0" encoding = "utf-8"? > <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <com.android.quard.CircleWaveProgressView android:id="@+id/circle_progress" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintRight_toRightOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:wave_color="@color/colorPrimaryDark" app:circlebg_color="@android:color/black" /> </android.support.constraint.ConstraintLayout>Copy the code
The renderings will not be posted.
Draw text progress effect
The simplest is to draw text directly in the View. This is very simple. I used to implement custom View by putting logic in it, which makes the View look bloated and expansibility is not high, because you think, If I want to change font position and style now, I need to change it in this View. If this View can open the interface for processing text, that is, modify text styles only through this interface, so that the text and the progress bar View can be decoupled.
// Progress displays TextView
private TextView tv_progress;
// Progress bar display value monitor interface
private UpdateTextListener updateTextListener;
// Create a new animation class
public class WaveProgressAnimat extends Animation{
// applyTransformation is called repeatedly during animation drawing,
// The parameter interpolatedTime changes with each invocation. The parameter interpolates from 0 to 1, indicating that the animation is over
@Override
protected void applyTransformation(float interpolatedTime, Transformation t){
super.applyTransformation(interpolatedTime, t);
// When the wave height reaches the maximum, there is no circulation, only translation
if(currentPercent < currentProgress / maxProgress){
currentPercent = interpolatedTime * currentProgress / maxProgress;
// This is displayed directly according to the progress value
tv_progress.setText(updateTextListener.updateText(interpolatedTime,currentProgress,maxProgress));
}
// The left distance
moveDistance = interpolatedTime * waveNumber * waveLength * 2;
// Redrawinvalidate(); }}// Define a value listener
public interface UpdateTextListener{
/** * provides interface for external modification of numeric styles, etc. *@paramInterpolatedTime The value is animated from 0 to 1 *@paramCurrentProgress Indicates the value of the progress bar *@paramMaxProgress Indicates the maximum value of the progress bar *@return* /
String updateText(float interpolatedTime,float currentProgress,float maxProgress);
}
// Set the listener
public void setUpdateTextListener(UpdateTextListener updateTextListener){
this.updateTextListener = updateTextListener;
}
/** ** Set the display *@paramTv_progress content values can be anything * */
public void setTextViewVaule(TextView tv_progress){
this.tv_progress = tv_progress;
}
Copy the code
And then implement the Activity file CircleWaveProgressView UpdateTextListener interface, logic processing:
public class MainActivity extends AppCompatActivity {
private CircleWaveProgressView circleWaveProgressView;
private TextView tv_value;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/ / TextView widgets
tv_value = findViewById(R.id.tv_value);
// Progress bar control
circleWaveProgressView = findViewById(R.id.circle_progress);
// Set TextView to the progress bar
circleWaveProgressView.setTextViewVaule(tv_value);
// Set the font value display listener
circleWaveProgressView.setUpdateTextListener(new CircleWaveProgressView.UpdateTextListener() {
@Override
public String updateText(float interpolatedTime, float currentProgress, float maxProgress) {
// Take one integer and keep two decimal places
DecimalFormat decimalFormat=new DecimalFormat("0.00");
String text_value = decimalFormat.format(interpolatedTime * currentProgress / maxProgress * 100) +"%";
// Finally bring the formatted content (value into the progress bar)
returntext_value ; }});// Set the progress and animation time
circleWaveProgressView.setProgress(50.2500); }}Copy the code
Layout file adds a TextView:
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.android.quard.CircleWaveProgressView
android:id="@+id/circle_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
/>
<TextView
android:id="@+id/tv_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:textColor="#ffffff"
android:textSize="24dp"
/>
</android.support.constraint.ConstraintLayout>
Copy the code
The final effect is shown below:
Draw double wave effect
To realize that the translation direction of the second layer wave is opposite to that of the first layer wave, the drawing sequence should be changed. Below:
// Whether to draw double wavy lines
private boolean isCanvasSecond_Wave;
// The color of the second wave
private int second_WaveColor;
// The second layer of wave brushes
private Paint secondWavePaint;
Copy the code
Attrs file adds a second layer of wave color:
<! <declare-styleable name="CircleWaveProgressView"> <! - the color of the wave - > < attr name = "wave_color format =" "color" > < / attr > <! <attr name="circlebg_color" format="color"></attr> <! <attr name="wave_length" format="dimension"></attr> <! <attr name="wave_height" format="dimension"></attr> <! <attr name="currentProgress" format="float"></attr> <! - the biggest progress - > < attr name = "maxProgress format =" float ">" < / attr > <! <attr name="second_color" format="color"></attr> </declare-styleable>Copy the code
Class files:
// The color of the second wave
second_WaveColor = typedArray.getColor(R.styleable.CircleWaveProgressView_second_color,Color.RED);
Copy the code
Add to init method:
// Initialize the second layer of wave brushes
secondWavePaint = new Paint();
secondWavePaint.setColor(second_WaveColor);
secondWavePaint.setAntiAlias(true);
// To override the first layer wave, choose SRC_ATOP mode. The second layer wave is fully displayed, and the non-intersection part of the first layer is displayed. This pattern can be understood by looking at the image composition article above
secondWavePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
// The initial state does not draw the second wave
isCanvasSecond_Wave = false;
Copy the code
In the onDraw method add:
@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
Log.d("ssd",getWidth()+"");
// The cache is used to create a new bitmap based on the parameters
circleBitmap = Bitmap.createBitmap(waveActualSize, waveActualSize, Bitmap.Config.ARGB_8888);
// Create a canvas at the base of the bitmap
bitmapCanvas = new Canvas(circleBitmap);
// Draw the circle with a smaller radius so that the waves fill the entire circle
bitmapCanvas.drawCircle(waveActualSize/2, waveActualSize/2, waveActualSize/2 - Density.dip2px(getContext(),8), circlePaint);
// Draw wave shapes
bitmapCanvas.drawPath(paintWavePath(),wavePaint);
// Whether to draw a second wave
if(isCanvasSecond_Wave){
bitmapCanvas.drawPath(cavasSecondPath(),secondWavePaint);
}
// Crop the image
canvas.drawBitmap(circleBitmap, 0.0.null);
// Draw wavy lines
// canvas.drawPath(paintWavePath(),wavePaint);
}
// Whether to draw a second wave
public void isSetCanvasSecondWave(boolean isCanvasSecond_Wave){
this.isCanvasSecond_Wave = isCanvasSecond_Wave;
}
/** * Draw the second wave method *@return* /
private Path cavasSecondPath(a){
float secondWaveHeight = waveHeight;
wavePath.reset();
// Move to the upper right, at p1
wavePath.moveTo(waveActualSize + moveDistance, (1 - currentPercent) * waveActualSize);
//p1 - p0
for(int i = 0; i < waveNumber * 2 ; i ++){
wavePath.rQuadTo(-waveLength / 2,secondWaveHeight,-waveLength,0);
wavePath.rQuadTo(-waveLength / 2,-secondWaveHeight,-waveLength,0);
}
// P0-p3-p0d height changes as the progress changes
wavePath.lineTo(0, waveActualSize);
// Connect p3-P2
wavePath.lineTo(waveActualSize,waveActualSize);
// connect p2-P1
wavePath.lineTo(waveActualSize,(1 - currentPercent) * waveActualSize);
// Close and fill
wavePath.close();
return wavePath;
}
Copy the code
Finally set in Activty file:
// Whether to draw a second wave
circleWaveProgressView.isSetCanvasSecondWave(true);
Copy the code
The final effect is shown below:
conclusion
After the derivation of Bessel formula and the realization of small examples, there is a more profound impression. There are a lot of things does not look so touching, like when you get a development demand, technology assessment found themselves without before use technology is used, this time is how to realize the ideas and methods, with reference to others or the cheek to ask technical elite, learned is their own, more than just effort easier.
Example source code