当前位置:网站首页>Custom control - round dot progress bar (imitating one key acceleration in security guard)
Custom control - round dot progress bar (imitating one key acceleration in security guard)
2022-06-25 00:41:00 【a10615】
This article has authorized the official account of WeChat : Hongyang (hongyangAndroid) First episode .
One 、 Source code
Source code and demo download ( This progress bar will continue to be added in the future 、 to update )
Two 、 Origin of action
In the development communication group , One child shoe said to realize this progress bar , But there is no open source project on the Internet . 
See this picture , Very familiar with it. ? Youmu has the impulse to think about it ? I think it's interesting , You can study it , Besides, I haven't written custom controls for a while , Just review ( Tell the truth , I haven't written for a while , There are ideas , But I just don't know where to start ). It took a day to get it done yesterday , Look at the renderings first :
3 Display mode : 
Simulate progress animation effect : 
Talk about its characteristics :
- There are three display modes available . As shown above ;
- Full display in the control range . The width and height of the control are different , Will take the small as the boundary ;
- All colors are customizable ;
- percentage 、 Company 、 The font size of the buttons can be customized ;
- The text of units and buttons can be customized ;
- The alignment of units and percentages is optional . See below “ Drawing units ”;
- The vertical distance between the button and the percentage is adjustable ;
- Button to set the click event ( Seems like crap , Otherwise, why is it called a button );
- The progress can be updated directly in the sub thread .
3、 ... and 、 Design Analysis
See this picture , First of all, we can confirm , It has four parts :
- The outermost dot
- percentage
- Company
- Button
Of course, you need to know the steps of customizing the control first , Refer to this great man's post .
First, summarize the steps of customizing the control :
- Analyze each part of the control , Set its properties . stay res/values/attrs.xml Well defined ;
- Implement the three basic construction methods , And get the attribute value or default value in the constructor ;
- rewrite onDraw(Canvas canvas) Method , Draw the interface in this method ;
- 【 Optional 】 Handle touch events ;
- Use... In layout files . Don't forget to add namespaces in the base layout ( Such as :xmlns:zjun=”http://schemas.android.com/apk/res-auto”), This allows you to use custom attributes .
Then analyze them one by one ...
1、 Draw the outermost dot
The outermost dot , It looks like a lot , After a lap, I feel 100 individual , Its current percentage is 65, Count it , Is, indeed, 65 A white dot , There is no need to count the grey dots , It must be the rest 35 individual . such , The total number of points in a circle is determined , Namely 100 individual .
The blank distance between points , It looks like a point space . Come down in a circle , Namely 200 A continuous dot . The dot should be inscribed inside the control boundary . 
Their relationship is : R0 = R1 + r; ······ ①
among R0 Control size (min(getWidth(), getHeight())) Half of ;R1 Is the radius of the circle track composed of the center of the dot ;r Is the radius of the dot ;
in addition 200 Consecutive dots are represented by R1 On a circle of radius , Both dot and dot are circumscribed . So the angle of each dot is 360°/200 = 1.8° 
Their relationship :
sin(0.9°)=rR1 ······ ②
from ① and ② You know ,r And R0 The relationship between :
r=sin(0.9°)sin(0.9°)+1R0
The effect of this , The distance between actual dots is too large , Finally, put sin(0.9°) Up to sin(1°). This makes it fuller .
mSin_1 = (float) Math.sin(Math.toRadians(1));
// Calculate the radius of the dot
float outerRadius = (getWidth() < getHeight() ? getWidth() : getHeight()) / 2f;
float dotRadius = mSin_1 * outerRadius / (1 + mSin_1);Find the radius of the dot , The position of the center of the dot is also known . Draw dots , A circle is 100 individual , It's ready to use canvas To draw , Every time you draw a dot , Just rotate 3.6°(360°/100).
As for progress , You can draw the completed progress first , Draw the unfinished progress . You can also draw the unfinished progress first , Use it as a background , Then draw the completed progress .
I chose the first method , reason : In order not to draw repeatedly , The other is to prevent the color of completed progress from being transparent , So the overlay is not the desired color .
float centerX = getWidth() / 2f;
float centerY = getHeight() / 2f;
// 1 Draw progress
mPaint.setColor(dotColor);
mPaint.setStyle(Paint.Style.FILL);
int count = 0;
// 1.1 Current progress
while (count++ < percent) {
canvas.drawCircle(centerX, centerY - outerRadius + dotRadius, dotRadius, mPaint);
canvas.rotate(3.6f, centerX, centerY);
}
// 1.2 Incomplete progress
mPaint.setColor(dotBgColor);
count--;
while (count++ < 100) {
canvas.drawCircle(centerX, centerY - outerRadius + dotRadius, dotRadius, mPaint);
canvas.rotate(3.6f, centerX, centerY);
}2、 Draw percentage
Percentages are words , Direct use canvas.drawText() Just draw .
mPaint.setTextSize(percentTextSize);
mPaint.setColor(percentTextColor);
canvas.drawText(percent + "", centerX - textWidth / 2, baseline, mPaint);When drawing , To find where to start drawing , Note the alignment of the two directions :
2.1 Horizontal alignment
This simple , First measure the width of the text , The starting position on the left is centerX - textWidth/2. On it textWidth It's because you have to add the width of the unit font .
mPaint.setTextSize(percentTextSize);
float percentTextWidth = mPaint.measureText(percent + "");2.3 The vertical alignment
baseline yes y The starting position of the direction , The initial setup is centerY + percentTextSize/2. In the default display mode , The result is dark purple : 
wipe , This is not vertical alignment . Is there universal gravitation ?
Then I found out why ( Reference resources ). Because of its y The coordinate position is baseline, It's not what we think bottom. Please look at the chart below. (ascent It is a little different from the reference blog , But there's no doubt about my accuracy , Because this diagram is drawn directly through custom controls ) 
explain :
- these top、bottom、ascent、descent Can be directly from FontMetrics In order to get , These values are expressed in baseline Benchmarking .
- baseline Namely canvas.drawText(text, x, y, paint) Medium y.
- center yes ascent and descent The middle line of , namely (ascent + descent)/2. You can see here ,center Is the middle line of the text .
and FontMetrics After setting the font size, you can get :
mPaint.setTextSize(textHeight);
Paint.FontMetrics fm = mPaint.getFontMetrics();So center it ,baseline The more accurate value is :
baseline = centerY + (fm.descent - fm.ascent)/2 - fm.descent;It took me a long time to understand . It's understandable : When the text is in the middle ,center Linear y The coordinate value is centerY, add (fm.descent - fm.ascent)/2 That's it descent Line ( Distinguish fm.descent Value ), subtract fm.descent That's it baseline Line .
The corrected results : 
Another more accurate value :
baseline = centerY + (fm.bottom- fm.top)/2 - fm.bottom;3、 Drawing units
The drawing method is the same as percentage , The beginning of his drawing x The coordinates are immediately followed by the percent .
but ,,, If the unit is not %, It is “ branch ”, So it can't be aligned with the number of percentage at the bottom . Look at the demand , Some places allow this , But driven by OCD , He made a pot for himself .
Look at the above baseline Analysis of , Especially that picture . Did you find the letters 、 Numbers 、 The bottom alignment of Chinese characters is different ( The words inside are useful , It's no joke ).
So we added such a unique attribute :unitTextAlignMode, It means the alignment of units and percentages . There are three ways :
- DEFAULT: As long as with baseline You can use this to align the bottom of the line ,eg: %, a, b, …
- CN: chinese , Just use it
- EN: Mainly for English letters and characters with tails ,eg: g, j, p, q, y, [, ], {, }, |, …
The principle is based on the size of the text , Fine tuned :
switch (unitTextAlignMode) {
case UNIT_TEXT_ALIGN_MODE_CN:
baseline -= fm_unit.descent / 4;
break;
case UNIT_TEXT_ALIGN_MODE_EN:
baseline -= fm_unit.descent * 2/3;
} What's the effect ? Don't worry. , The guest officer , We have exhibits here , It's not too late to watch the re-election :
The percentage is the same as the unit font size : 
The percentage font is larger than the unit font : 
4、 Draw button

Mainly the background , If you look carefully, , You can draw it this way : First, divide the background into three parts , A rectangle in the middle , A semicircle on each side . The length of the rectangle is equal to the width of the font , Height is the height of the font 2 times .
When drawing , It can be done by Path To finish drawing at one time :
mPath.reset();
mPath.moveTo(mButtonRect_start.x, mButtonRect_start.y);
mPath.rLineTo(buttonTextWidth, 0);
float left = centerX + buttonTextWidth/2 - mButtonRadius;
float top = centerY + buttonTopOffset;
float right = left + 2 * mButtonRadius;
float bottom = top + 2 * mButtonRadius;
mRectF.set(left, top, right, bottom);
mPath.arcTo(mRectF, 270, 180); // Parameters 1: Inscribe this square , Parameters 2: Starting angle , Parameters 3: The angle range of the painting
mPath.rLineTo(-buttonTextWidth, 0);
mRectF.offset(-buttonTextWidth, 0); // Translational position
mPath.arcTo(mRectF, 90, 180);
mPath.close();
canvas.drawPath(mPath, mPaint);5、 Handle button click events
Click event to process button , First, determine whether the touch point is in the button , Just like drawing , Judge whether it is square 、 Left semicircle 、 In the right semicircle .
Square is a good judge , Judgment of left semicircle and right semicircle , We use analytic geometry : Judge a point (x, y) Whether it is in the circle (x0,y0, r) Inside , First subtract the coordinates of the center of the circle from the coordinates of the point , This is equivalent to moving the origin of the coordinate system to the center of the circle , If in a circle , Must satisfy :
(x−x0)2+(y−y0)2<=r2
The code implementation is as follows :
/** * Determine whether the coordinates are in the button * @param x Coordinate x * @param y Coordinate y * @return true- In the button ,false- Not in the button */
private boolean isTouchInButton(final float x, final float y) {
// Determine whether it is in the button rectangle
if (x >= mButtonRect_start.x && x <= mButtonRect_end.x
&& y >= mButtonRect_start.y && y <= mButtonRect_end.y) {
return true;
}
// Judge whether it is in the left semicircle : The other half of the circle is in a rectangle , It also belongs to the button range , So you can judge the whole circle directly
// Move the coordinate system to the center of the circle and judge
float centerX = mButtonRect_start.x;
float centerY = (mButtonRect_start.y + mButtonRect_end.y) / 2;
float newX = x - centerX;
float newY = y - centerY;
if (newX * newX + newY * newY <= mButtonRadius * mButtonRadius) {
return true;
}
// Judge whether it is in the right semicircle
centerX = mButtonRect_end.x;
newX = x - centerX;
return newX * newX + newY * newY <= mButtonRadius * mButtonRadius;
}
Button processing event , stay onTouchEvent() Intercept in , This will distinguish the button being clicked , And controls ( Except button area ) Clicked
@Override
public boolean onTouchEvent(MotionEvent event) {
if (showMode == SHOW_MODE_ALL) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (isTouchInButton(event.getX(), event.getY())) {
mIsButtonTouched = true;
postInvalidate();
}
break;
case MotionEvent.ACTION_MOVE:
if (mIsButtonTouched) {
if (!isTouchInButton(event.getX(), event.getY())) {
mIsButtonTouched = false;
postInvalidate();
}
}
break;
case MotionEvent.ACTION_UP:
if (mIsButtonTouched && mButtonClickListener != null) {
mButtonClickListener.onClick(this);
}
mIsButtonTouched = false;
postInvalidate();
}
if (mIsButtonTouched) {
return true;
}
}
return super.onTouchEvent(event);
}Four 、 Core code
In addition to the above button event handling methods , also 2 The main method :
- Get the attribute value or default value in the constructor
- onDraw() in
public CircleDotProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CircleDotProgressBar);
// Get custom attribute value or default value
progressMax = ta.getInteger(R.styleable.CircleDotProgressBar_progressMax, 100);
dotColor = ta.getColor(R.styleable.CircleDotProgressBar_dotColor, Color.WHITE);
dotBgColor = ta.getColor(R.styleable.CircleDotProgressBar_dotBgColor, Color.GRAY);
showMode = ta.getInt(R.styleable.CircleDotProgressBar_showMode, SHOW_MODE_PERCENT);
if (showMode != SHOW_MODE_NULL) {
percentTextSize = ta.getDimension(R.styleable.CircleDotProgressBar_percentTextSize, dp2px(30));
percentTextColor = ta.getInt(R.styleable.CircleDotProgressBar_percentTextColor, Color.WHITE);
unitText = ta.getString(R.styleable.CircleDotProgressBar_unitText);
unitTextSize = ta.getDimension(R.styleable.CircleDotProgressBar_unitTextSize, percentTextSize);
unitTextColor = ta.getInt(R.styleable.CircleDotProgressBar_unitTextColor, Color.WHITE);
unitTextAlignMode = ta.getInt(R.styleable.CircleDotProgressBar_unitTextAlignMode, UNIT_TEXT_ALIGN_MODE_DEFAULT);
if (unitText == null) {
unitText = "%";
}
}
if (showMode == SHOW_MODE_ALL) {
buttonText = ta.getString(R.styleable.CircleDotProgressBar_buttonText);
buttonTextSize = ta.getDimension(R.styleable.CircleDotProgressBar_buttonTextSize, dp2px(15));
buttonTextColor = ta.getInt(R.styleable.CircleDotProgressBar_buttonTextColor, Color.GRAY);
buttonBgColor = ta.getInt(R.styleable.CircleDotProgressBar_buttonBgColor, Color.WHITE);
buttonClickColor = ta.getInt(R.styleable.CircleDotProgressBar_buttonClickColor, buttonBgColor);
buttonClickBgColor = ta.getInt(R.styleable.CircleDotProgressBar_buttonClickBgColor, buttonTextColor);
buttonTopOffset = ta.getDimension(R.styleable.CircleDotProgressBar_buttonTopOffset, dp2px(15));
if (buttonText == null) {
buttonText = context.getString(R.string.CircleDotProgressBar_speed_up_one_key);
}
}
ta.recycle();
// Other preparations
mSin_1 = (float) Math.sin(Math.toRadians(1)); // seek sin(1°). Angles need to be converted into radians
mPaint = new Paint();
mPaint.setAntiAlias(true); // Anti aliasing
mPath = new Path();
mRectF = new RectF();
mButtonRect_start = new PointF();
mButtonRect_end = new PointF();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Calculate the radius of the dot
float outerRadius = (getWidth() < getHeight() ? getWidth() : getHeight()) / 2f;
float centerX = getWidth() / 2f;
float centerY = getHeight() / 2f;
// outerRadius = innerRadius + dotRadius
// sin((360°/200)/2) = sin(0.9°) = dotRadius / innerRadius;
// To make the dots fuller , Angle 0.9° increase 0.1° To 1°
float dotRadius = mSin_1 * outerRadius / (1 + mSin_1);
// 1 Draw progress
mPaint.setColor(dotColor);
mPaint.setStyle(Paint.Style.FILL);
int count = 0;
// 1.1 Current progress
while (count++ < percent) {
canvas.drawCircle(centerX, centerY - outerRadius + dotRadius, dotRadius, mPaint);
canvas.rotate(3.6f, centerX, centerY);
}
// 1.2 Incomplete progress
mPaint.setColor(dotBgColor);
count--;
while (count++ < 100) {
canvas.drawCircle(centerX, centerY - outerRadius + dotRadius, dotRadius, mPaint);
canvas.rotate(3.6f, centerX, centerY);
}
if (showMode == SHOW_MODE_NULL) {
return;
}
if (showMode == SHOW_MODE_PERCENT) {
// 2 Draw percentages and units : Center horizontally and vertically
// Measure the width
mPaint.setTextSize(percentTextSize);
// mPaint.setTypeface(Typeface.DEFAULT_BOLD); // bold
float percentTextWidth = mPaint.measureText(percent + "");
mPaint.setTextSize(unitTextSize);
float unitTextWidth = mPaint.measureText(unitText);
Paint.FontMetrics fm_unit = mPaint.getFontMetrics();
float textWidth = percentTextWidth + unitTextWidth;
float textHeight = percentTextSize > unitTextSize ? percentTextSize : unitTextSize;
// Calculation Text When vertically centered baseline
mPaint.setTextSize(textHeight);
Paint.FontMetrics fm = mPaint.getFontMetrics();
// When the font is vertically centered , In the middle of the font is centerY, Plus half the actual height of the font is descent Line , subtract descent Namely baseline The position of the line (fm China and Israel baseline Benchmarking )
float baseline = centerY + (fm.descent - fm.ascent)/2 - fm.descent;
// 2.1 Draw percentage
mPaint.setTextSize(percentTextSize);
mPaint.setColor(percentTextColor);
canvas.drawText(percent + "", centerX - textWidth / 2, baseline, mPaint);
// 2.2 Drawing unit
mPaint.setTextSize(unitTextSize);
mPaint.setColor(unitTextColor);
// Unit alignment
switch (unitTextAlignMode) {
case UNIT_TEXT_ALIGN_MODE_CN:
baseline -= fm_unit.descent / 4;
break;
case UNIT_TEXT_ALIGN_MODE_EN:
baseline -= fm_unit.descent * 2/3;
}
canvas.drawText(unitText, centerX - textWidth / 2 + percentTextWidth, baseline, mPaint);
}else if (showMode == SHOW_MODE_ALL) {
// 2 Draw percentages and units : Horizontal center , In a vertical direction baseline stay centerY
// Measure the width
mPaint.setTextSize(percentTextSize);
// mPaint.setTypeface(Typeface.DEFAULT_BOLD); // bold
float percentTextWidth = mPaint.measureText(percent + "");
mPaint.setTextSize(unitTextSize);
float unitTextWidth = mPaint.measureText(unitText);
Paint.FontMetrics fm_unit = mPaint.getFontMetrics();
float textWidth = percentTextWidth + unitTextWidth;
// 2.1 Draw percentage
mPaint.setTextSize(percentTextSize);
mPaint.setColor(percentTextColor);
float baseline_per = centerY;
canvas.drawText(percent + "", centerX - textWidth / 2, baseline_per, mPaint);
// 2.2 Drawing unit
mPaint.setTextSize(unitTextSize);
mPaint.setColor(unitTextColor);
// Unit alignment
switch (unitTextAlignMode) {
case UNIT_TEXT_ALIGN_MODE_CN:
baseline_per -= fm_unit.descent / 4;
break;
case UNIT_TEXT_ALIGN_MODE_EN:
baseline_per -= fm_unit.descent * 2/3;
}
canvas.drawText(unitText, centerX - textWidth / 2 + percentTextWidth, baseline_per, mPaint);
// 3 Draw button
mPaint.setTextSize(buttonTextSize);
float buttonTextWidth = mPaint.measureText(buttonText);
Paint.FontMetrics fm = mPaint.getFontMetrics();
// 3.1 Draw the button background
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mIsButtonTouched ? buttonClickBgColor : buttonBgColor);
float buttonHeight = 2 * buttonTextSize;
mButtonRadius = buttonHeight / 2;
mButtonRect_start.set(centerX - buttonTextWidth / 2, centerY + buttonTopOffset);
mButtonRect_end.set(centerX + buttonTextWidth/2, centerY + buttonTopOffset + buttonHeight);
mPath.reset();
mPath.moveTo(mButtonRect_start.x, mButtonRect_start.y);
mPath.rLineTo(buttonTextWidth, 0);
float left = centerX + buttonTextWidth/2 - mButtonRadius;
float top = centerY + buttonTopOffset;
float right = left + 2 * mButtonRadius;
float bottom = top + 2 * mButtonRadius;
mRectF.set(left, top, right, bottom);
mPath.arcTo(mRectF, 270, 180); // Parameters 1: Inscribe this square , Parameters 2: Starting angle , Parameters 3: The angle range of the painting
mPath.rLineTo(-buttonTextWidth, 0);
mRectF.offset(-buttonTextWidth, 0); // Translational position
mPath.arcTo(mRectF, 90, 180);
mPath.close();
canvas.drawPath(mPath, mPaint);
// 3.2 Draw button text
mPaint.setColor(mIsButtonTouched ? buttonClickColor : buttonTextColor);
float baseline = centerY + buttonTopOffset + buttonTextSize + (fm.descent - fm.ascent)/2 - fm.descent;
canvas.drawText(buttonText, centerX - buttonTextWidth / 2, baseline, mPaint);
}
}
/** * Set the schedule * Sync , Allow multi thread access * @param progress speed of progress */
public synchronized void setProgress(int progress) {
if (progress < 0 || progress > progressMax) {
throw new IllegalArgumentException(String.format("progress must between 0 and max(%d)", progressMax));
}
this.progress = progress;
percent = progress * 100 / progressMax;
postInvalidate(); // Can be called directly in a child thread , and invalidate() Must be on the main thread (UI Threads ) Call in
}
public synchronized void setProgressMax(int progressMax) {
if (progressMax < 0) {
throw new IllegalArgumentException("progressMax mustn't smaller than 0");
}
this.progressMax = progressMax;
}边栏推荐
- Paint rounded rectangle
- Zed acquisition
- Related operations of ansible and Playbook
- 断言(assert)的用法
- Scrollview height cannot fill full screen
- Why are life science enterprises on the cloud in succession?
- JS dynamically generates variable names and assigns values
- Tiktok wallpaper applet source code
- Color gradient gradient color collection
- After 5 years of software testing in didi and ByteDance, it's too real
猜你喜欢

Related operations of ansible and Playbook

无人驾驶: 对多传感器融合的一些思考
Hyperledger Fabric 2. X dynamic update smart contract

Meta&伯克利基于池化自注意力机制提出通用多尺度视觉Transformer,在ImageNet分类准确率达88.8%!开源...

Xcode预览(Preview)显示List视图内容的一个Bug及解决

Decoupling pages and components using lifecycle

Registration method of native method in JNI

【微服务|Sentinel】Sentinel快速入门|构建镜像|启动控制台

【Redis实现秒杀业务②】超卖问题的解决方案

Adding, deleting, modifying and checking in low build code
随机推荐
Common redis commands in Linux system
Several ways for wechat applet to jump to the page are worth collecting
C program design topic 15-16 final exam exercise solutions (Part 1)
[interview question] the difference between instancof and getclass()
MySQL semi sync replication
What is test development? Can you find a job at this stage?
[distributed system design profile (2)] kV raft
Jar package merging using Apache ant
Domain Driven Design and coding
How to quickly open traffic master for wechat applet
Practical operation notes - notebook plus memory and ash cleaning
Svg line animation background JS effect
Unimportant tokens can be stopped in advance! NVIDIA proposes an efficient visual transformer network a-vit with adaptive token to improve the throughput of the model
【微服务|Sentinel】实时监控|RT|吞吐量|并发数|QPS
Working principle analysis of kubernetes architecture core components
Meta & Berkeley proposed a universal multi-scale visual transformer based on pooled self attention mechanism. The classification accuracy in Imagenet reached 88.8%! Open source
The picture of wechat official account can not be displayed normally
Kibana installation via kubernetes visual interface (rancher)
Go crawler framework -colly actual combat (II) -- Douban top250 crawling
Usage of ViewModel and livedata in jetpack