当前位置:网站首页>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;
}
边栏推荐
- 无需显示屏的VNC Viewer远程连接树莓派
- 离散数学及其应用 2018-2019学年春夏学期期末考试 习题详解
- Working principle analysis of kubernetes architecture core components
- 不重要的token可以提前停止计算!英伟达提出自适应token的高效视觉Transformer网络A-ViT,提高模型的吞吐量!...
- iNFTnews | 国内NFT发展仅限于数字藏品吗?
- Tiktok wallpaper applet v1.0.2 function, new arrival function
- C程序设计专题 15-16年期末考试习题解答(上)
- On the difficulty of developing large im instant messaging system
- 传输层 以字节为单位的滑动窗口技术
- Source code analysis the problem that fragments cannot be displayed in the custom ViewGroup
猜你喜欢
A small program written this week
【Redis实现秒杀业务③】超卖问题之乐观锁具体实现
Difficult and miscellaneous problems: A Study on the phenomenon of text fuzziness caused by transform
Outer screen and widescreen wasted? Harmonyos folding screen design specification teaches you to use it
iNFTnews | 国内NFT发展仅限于数字藏品吗?
Ott marketing is booming. How should businesses invest?
Use of JMeter easynmon
VNC viewer remote connection raspberry pie without display
How to reduce the font size of custom controls (optimize the round dot progress bar)
Registration method of native method in JNI
随机推荐
无人驾驶: 对多传感器融合的一些思考
Is it so difficult to calculate the REM size of the web page according to the design draft?
Dynamic effect of canvas lines
Difficult and miscellaneous problems: A Study on the phenomenon of text fuzziness caused by transform
On the difficulty of developing large im instant messaging system
Human body transformation vs digital Avatar
ros(24):error: invalid initialization of reference of type ‘xx’ from expression of type ‘xx’
Using tcp/udp tools to debug the yeelight ribbon
Go crawler framework -colly actual combat (IV) -- Zhihu answer crawl (I)
ros(24):error: invalid initialization of reference of type ‘xx’ from expression of type ‘xx’
Signal integrity (SI) power integrity (PI) learning notes (XXV) differential pair and differential impedance (V)
Applet opening traffic master
2021-02-15
After 5 years of software testing in didi and ByteDance, it's too real
实现mnist手写数字识别
【Redis实现秒杀业务③】超卖问题之乐观锁具体实现
Solution to network access packet loss of Tencent cloud international ECS
The third generation of power electronics semiconductors: SiC MOSFET learning notes (V) research on driving power supply
Svg+js keyboard control path
The drawableleft of the custom textview in kotlin is displayed in the center together with the text