[[191018]] Rendering You can see that this custom control combines color gradient, dynamic drawing scale, and dynamic water ball effect. Next, let's take a look at how this effect is achieved step by step. Start customizing controls Like many custom control methods, you need to base it on some kind of View or some kind of ViewGroup I choose View here, as shown below: - public class HuaWeiView extends View {
-
- /**
- * Used to initialize brushes, etc.
- * @param context
- * @param attrs
- */
- public HuaWeiView(Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- }
-
- /**
- * Used to measure and limit the view to a square
- * @param widthMeasureSpec
- * @param heightMeasureSpec
- */
- @Override
- protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
-
- /**
- * Implement various drawing functions
- * @param canvas
- */
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- }
- }
The construction method is used in layout. The onMeasure() method is used to measure and limit the view size The onDraw() method is used to perform specific drawing functions 1. Use the onMeasure() method to limit the View to a square Only when a rectangle is determined can an ellipse be drawn. If the rectangle is a square, the ellipse will become a circle. - @Override
- protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- int width=MeasureSpec.getSize(widthMeasureSpec);
- int height = MeasureSpec.getSize(heightMeasureSpec);
- //The minimum value is the length of the square
- len= Math.min (width,height);
- //Set the measured height and width (must be called, otherwise it will have no effect)
- setMeasuredDimension(len,len);
- }
We use MeasureSpec to get the width and height set by the user, and then take the minimum value and set it to our view, so that we can make a rectangle. Now use it in the layout: - <?xml version= "1.0" encoding= "utf-8" ?>
- <LinearLayout 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"
- android:orientation= "vertical"
- android:background= "@color/colorPrimary"
- android:padding= "20dp"
- tools:context= "com.example.huaweiview.MainActivity" >
-
- <com.example.huaweiview.HuaWeiView
- android:layout_gravity= "center"
- android:background= "@color/colorAccent"
- android:layout_width= "200dp"
- android:layout_height= "300dp"
- />
-
- </LinearLayout>
The background of the parent layout is blue, the background of the control is pink, and the width and height are set differently, but the display effect of the control is still a square, and the smaller value is used as the standard. Our onMeasure() is effective The next step is how to determine a circular area. 2. onDraw() draws a circular area Before drawing, we need to have an understanding of the coordinate system in Android We all know that the upper left corner of the mobile phone screen is the coordinate origin, the right is the positive X axis, and the bottom is the positive Y axis. In fact, the mobile page is the display interface of the activity, and it is also a View. Can we say that all Views have their own coordinate system when drawing graphics? (My personal opinion...) That is, each View has its own coordinate system, such as the current custom View: Now we need to draw an arc in our custom view, then the radius of this arc is half the length of our custom view, that is: radius=len/2; Then the coordinates of the center of the circle are exactly (radius, radius) Next, start drawing - @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- //How to draw an arc
- canvas.drawArc(oval, startAngle, sweepAngle, useCenter,paint);
- }
Here is how to draw an arc: - Parameter oval is a RectF object for a rectangle
- Parameter 2 startAngle is the starting angle of the arc
- Parameter three: sweepAngle is the angle of the arc (sweep angle)
- Parameter 4 useCenter is a boolean value for the arc. When it is true, an arc is drawn. When it is false, a cutting arc is drawn.
- Parameter five paint is a brush object
That is to say, as long as a rectangle is determined, an arc can be drawn by determining its starting and passing angles (you can use the drawing board to test this) Next is to initialize these parameters Initialize the rectangle - @Override
- protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- int width = MeasureSpec.getSize(widthMeasureSpec);
- int height = MeasureSpec.getSize(heightMeasureSpec);
- //The minimum value is the length of the square
- len = Math. min (width, height);
- //Instantiate the rectangle
- oval=new RectF(0,0,len,len);
- //Set the measured height and width (must be called, otherwise it will have no effect)
- setMeasuredDimension(len, len);
- }
To draw a rectangle, you need to determine the coordinates of the upper left corner and the lower right corner (which can be tested through the drawing board). According to the above analysis, the origin of the coordinates is the upper left corner of our view, and the coordinates of the lower right corner are of course len. Next is to initialize the starting and passing angles - private float startAngle=120;
-
- private float sweepAngle=300;
You need to understand that the positive axis of the Y axis is downward, which is exactly the opposite of what you learned in school, that is, 90 degrees is at the bottom and -90 degrees is at the top. Initialize the brush - public HuaWeiView(Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- paint = new Paint();
- //Set the brush color
- paint.setColor(Color.WHITE);
- //Set the brush anti-aliasing
- paint.setAntiAlias( true );
- // Make the drawn shape hollow (not filled)
- paint.setStyle(Paint.Style.STROKE);
- }useCenter = false
It is not easy to get here, but I find that it is useless to just draw an arc. What I want is a scale line, and there is no method to draw scale lines in canvas. At this time, we need to write a method to draw scale lines ourselves. By observing the picture, we can see that all lines are drawn from a point on the arc to a certain direction. So how to determine these two points? We need to do two things: Moving coordinate system - @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- //How to draw an arc
- canvas.drawArc(oval, startAngle, sweepAngle, useCenter,paint);
- //How to draw tick marks
- drawViewLine(canvas);
- }
-
- private void drawViewLine(Canvas canvas) {
- //Save the previous canvas content first
- canvas.save();
- //Move canvas (X axis moving distance, Y axis moving distance)
- canvas.translate(radius,radius);
- //Restore the state after the operation is completed
- canvas.restore();
- }
We wrote a method to draw tick marks and called it in the onDraw() method. Before moving the coordinate system, we need to save the previous canvas state, and then move the X and Y axes by the radius of the arc, as shown below: canvas.translate(radius,radius); method moves the coordinate system (obtained through actual effects and data search) canvas.save() and canvas.restore() should appear in pairs, just like the stream needs to be closed after use. After the first task is completed, start the second task, rotating the coordinate system It is still difficult to determine the coordinates of the arc point and the coordinates of another point only by moving the coordinate system. It would be great if both points were on the coordinate axis. The following is implemented: - private void drawViewLine(Canvas canvas) {
- //Save the previous canvas content first
- canvas.save();
- //Move canvas (X axis moving distance, Y axis moving distance)
- canvas.translate(radius,radius);
- //Rotate coordinate system
- canvas.rotate(30);
- //Restore the state after the operation is completed
- canvas.restore();
The method of drawing tick marks adds a code for rotating 30 degrees. What should the coordinate system look like after the rotation? Because the starting point is 30 degrees away from 90 degrees, after the rotation, the starting point just falls on the Y axis, so the coordinates of this point are easy to determine, that's right, (0, radius); if we find a point on the Y axis, can't we draw a tick mark, then what are its coordinates? Yes, it should be (0, radius-y), because we want to internalize the tick mark, so we subtract a value, try it quickly, the code is as follows: - private void drawViewLine(Canvas canvas) {
- //Save the previous canvas content first
- canvas.save();
- //Move canvas (X axis moving distance, Y axis moving distance)
- canvas.translate(radius,radius);
- //Rotate coordinate system
- canvas.rotate(30);
- Paint linePatin=new Paint();
- //Set the brush color
- linePatin.setColor(Color.WHITE);
- //Line width
- linePatin.setStrokeWidth(2);
- //Set the brush anti-aliasing
- linePatin.setAntiAlias( true );
- //Draw a tick mark
- canvas.drawLine(0,radius,0,radius-40,linePatin);
- //Restore the state after the operation is completed
- canvas.restore();
- }
According to the coordinates of the two points obtained, draw a white line, as shown in the figure: Of course, these points are obtained by rotating the coordinate system by 30 degrees after the movement. A line is drawn here. If you want to draw more lines, you can still use the previous idea to rotate it by a small angle each time and then draw a straight line. So how many degrees should it be rotated? For example, here: the total swept angle sweepAngle=300; 100 scales are needed, so the angle to be rotated each time rotateAngle=sweepAngle/100, the specific code is as follows: - private void drawViewLine(Canvas canvas) {
- //Save the previous canvas content first
- canvas.save();
- //Move canvas (X axis moving distance, Y axis moving distance)
- canvas.translate(radius,radius);
- //Rotate coordinate system
- canvas.rotate(30);
- Paint linePatin=new Paint();
- //Set the brush color
- linePatin.setColor(Color.WHITE);
- //Line width
- linePatin.setStrokeWidth(2);
- //Set the brush anti-aliasing
- linePatin.setAntiAlias( true );
- //Determine the angle of each rotation
- float rotateAngle=sweepAngle/99;
- for ( int i=0;i<100;i++){
- //Draw a tick mark
- canvas.drawLine(0,radius,0,radius-40,linePatin);
- canvas.rotate(rotateAngle);
- }
-
- //Restore state after the operation is completed
- canvas.restore();
- }
100 scales require 101 cycles of drawing lines (please check your watch), and the line will rotate after drawing. Cycle in sequence, as shown in the figure After so much time, the dial is finally completed. The next step is to determine what color to display at different angles. First, we need to determine the range to be drawn, targetAngle: Draw the colored part - private void drawViewLine(Canvas canvas) {
- //Save the previous canvas content first
- canvas.save();
- //Move canvas (X axis moving distance, Y axis moving distance)
- canvas.translate(radius,radius);
- //Rotate coordinate system
- canvas.rotate(30);
- Paint linePatin=new Paint();
- //Set the brush color
- linePatin.setColor(Color.WHITE);
- //Line width
- linePatin.setStrokeWidth(2);
- //Set the brush anti-aliasing
- linePatin.setAntiAlias( true );
- //Determine the angle of each rotation
- float rotateAngle=sweepAngle/100;
- //Draw the brush for the colored part
- Paint targetLinePatin=new Paint();
- targetLinePatin.setColor(Color.GREEN);
- targetLinePatin.setStrokeWidth(2);
- targetLinePatin.setAntiAlias( true );
- //Record the range of the colored part that has been drawn
- float hasDraw=0;
- for ( int i=0;i<=100;i++){
- if(hasDraw<=targetAngle&&targetAngle!=0){//When you need to draw the colored part
- //Draw a tick mark
- canvas.drawLine(0,radius,0,radius-40,targetLinePatin);
- } else {//No need to draw the colored part
- //Draw a tick mark
- canvas.drawLine(0,radius,0,radius-40,linePatin);
- }
- //The accumulated drawn parts
- hasDraw+=rotateAngle;
- //Rotation
- canvas.rotate(rotateAngle);
- }
-
- //Restore state after the operation is completed
- canvas.restore();
- }
We need to constantly record the valid parts that have been drawn, and draw the rest in white. Color gradient according to the angle ratio It is necessary to calculate the ratio of the angle that has been drawn to the total angle (300) - for ( int i=0;i<=100;i++){
- if(hasDraw<=targetAngle&&targetAngle!=0){//When you need to draw the colored part
- //Calculate the proportion of the drawing
- float percent=hasDraw/sweepAngle;
- int red= 255-( int ) (255*percent);
- int green= ( int ) (255*percent);
- targetLinePatin.setARGB(255,red,green,0);
- //Draw a tick mark
- canvas.drawLine(0,radius,0,radius-40,targetLinePatin);
- } else {//No need to draw the colored part
- //Draw a tick mark
- canvas.drawLine(0,radius,0,radius-40,linePatin);
- }
- hasDraw+=rotateAngle;
- canvas.rotate(rotateAngle);
- }
Only when drawing the colored part, the three elements are used to achieve the gradient. The lower the proportion, the greater the red value, anyway, the greater the green value. Implementing dynamic display First think about its movement, which is divided into forward state and backward state. If it is moving (a complete backward and forward movement is not completed), it cannot start the next movement. Two parameters are required, state and isRunning - // Determine if it is moving
- private boolean isRunning;
- //Judge whether it is a backward or forward state
- private int state = 1;
- public void changeAngle(final float trueAngle) {
- if (isRunning){//If it is running, return directly
- return ;
- }
- final Timer timer = new Timer();
- timer.schedule(new TimerTask() {
- @Override
- public void run() {
- switch (state) {
- Case 1://Back state
- isRunning= true ;
- targetAngle -= 3;
- if (targetAngle <= 0) {//If it falls back to 0
- targetAngle = 0;
- //Change to forward state
- state = 2;
- }
- break;
- Case 2: //Forward state
- targetAngle += 3;
- if (targetAngle >= trueAngle) {//If it increases to the specified angle
- targetAngle = trueAngle;
- //Change to back state
- state = 1;
- isRunning= false ;
- //End this exercise
- timer.cancel();
- }
- break;
- default :
- break;
- }
- //Redraw (method used in child thread)
- postInvalidate();
- }
- }, 500, 30);
- }
Use the time task to execute the run method every 30 milliseconds, redraw the picture each time, and then call this method in the activity - HuaWeiView hwv;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- hwv= (HuaWeiView) findViewById(R.id.hwv);
- hwv.setOnClickListener(new View .OnClickListener() {
- @Override
- public void onClick( View v) {
- //In the click event, call the dynamic method
- hwv.changeAngle(200);
- }
- });
-
- }
After reading this, I believe you have a good understanding of the coordinate system, the dynamic changes of angles, and the drawing of the dial. More verification will help with your understanding. Next, we need to implement dynamic background gradient Think about where the gradient is used in our view? Yes, when drawing the colored part, it would be great if we could continuously pass the color gradient value to the activity. Next, we will use the interface to pass the value to achieve this function: - ***Declare an internal interface in the custom View:
- private OnAngleColorListener onAngleColorListener;
-
- public void setOnAngleColorListener(OnAngleColorListener onAngleColorListener) {
- this.onAngleColorListener = onAngleColorListener;
- }
-
- public interface OnAngleColorListener{
- void colorListener( int red, int green);
- }
We declare an internal interface in the custom View, declare a global interface object, and provide a set method There is a method in the interface to get the color value The next step is to call this method in the right place, so where is it? It is called when we draw the color scale: - for ( int i = 0; i <= 100; i++) {
- if (hasDraw <= targetAngle && targetAngle != 0) {//When you need to draw the colored part
- //Calculate the proportion of the drawing
- float percent = hasDraw / sweepAngle;
- int red = 255 - ( int ) (255 * percent);
- int green = ( int ) (255 * percent);
- //Implement interface callback and pass color value
- if (onAngleColorListener!= null ){
- onAngleColorListener.colorListener(red,green);
- }
- targetLinePatin.setARGB(255, red, green, 0);
- //Draw a tick mark
- canvas.drawLine(0, radius, 0, radius - 40, targetLinePatin);
- } else {//No need to draw the colored part
- //Draw a tick mark
- canvas.drawLine(0, radius, 0, radius - 40, linePatin);
- }
We implemented the interface callback when drawing, and then implemented the interface in the activity - public class MainActivity extends AppCompatActivity {
-
- HuaWeiView hwv;
- LinearLayout ll_parent;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- hwv= (HuaWeiView) findViewById(R.id.hwv);
- //Instance parent layout
- ll_parent= (LinearLayout) findViewById(R.id.ll_parent);
- hwv.setOnClickListener(new View .OnClickListener() {
- @Override
- public void onClick( View v) {
- //In the click event, call the dynamic method
- hwv.changeAngle(200);
- }
- });
- //Set the angle color change monitor
- hwv.setOnAngleColorListener(new HuaWeiView.OnAngleColorListener() {
- @Override
- public void colorListener( int red, int green) {
- Color color=new Color();
- //Convert RGB value to int type through Color object
- int backColor=color.argb(100,red,green,0);
- //Set the background of the parent layout
- ll_parent.setBackgroundColor(backColor);
- }
- });
-
- }
- }
Give the parent layout an id and then instantiate it. Set an angle color change listener for our custom control to get the value passed in the callback, then use the Color object to convert the RGB value to an int value, and then set it to the background of the parent layout. Here the background is slightly transparent. Effect diagram: Now it feels a lot cooler, in fact, the functions have been almost realized, the next step is to draw the content inside Draw Text Of course, you don't need to draw text, you can directly add textview to the layout. Without further ado, let's analyze the drawing process. There is a small circle inside the dial, and the text is inside the small circle. To draw the small circle, you only need to make its radius smaller. - /**
- * How to draw a small circle and text. The color of the small circle also changes gradually.
- * @param canvas
- */
- private void drawScoreText(Canvas canvas) {
- // Draw a small circle first
- Paint smallPaint = new Paint();
- smallPaint.setARGB(100,red,green,0);
- // Draw a small circle and specify the center coordinates, radius, and brush
- int smallRadius=radius-60;
- canvas.drawCircle(radius, radius, radius - 60, smallPaint);
- //Draw the text
- Paint textPaint=new Paint();
- //Set the text to center
- textPaint.setTextAlign(Paint.Align.CENTER);
- textPaint.setColor(Color.WHITE);
- textPaint.setTextSize(smallRadius/2);
- //score needs to be calculated
- canvas.drawText( "" +score,radius,radius,textPaint);
- //Draw the points, in the upper right corner of the score
- textPaint.setTextSize(smallRadius/6);
- canvas.drawText( "分" ,radius+smallRadius/2,radius-smallRadius/4,textPaint);
- //Draw click optimization below the score
- textPaint.setTextSize(smallRadius/6);
- canvas.drawText( "Click to optimize" , radius, radius+smallRadius/2, textPaint);
-
- }
Here, the previously gradient red and green are taken as global variables. First, draw a small circle with a gradient brush color. Then draw the text score, which needs to be calculated. - //Calculated score
- score=( int )(targetAngle/sweepAngle*100);
- //Redraw (method used in child thread)
- postInvalidate();
In the time task, calculate the score before each drawing, then draw a fixed value score in the upper right corner, and then click Optimize on the fixed content below (the coordinates at this time have returned to their original appearance) So far, the functions have been almost written. There is also a water wave acceleration ball effect, which will be written in the next blog. |