動(dòng)畫效果
(最后有全部代碼)

第一步:新建Class自定義View
package com.example.percentloadinganimator
import android.content.Context
import android.util.AttributeSet
import android.view.View
class PercentLoading:View {
constructor(context:Context):super(context)
constructor(context: Context,attrs:AttributeSet):super(context){}
}
第二步:顯示自定義View
方法一:新建自定義View類的對(duì)象
方法二:在要顯示該自定義View的Activity(這里選擇MainActivity)的xml文件中配置該自定義View的屬性,使得Activity上能顯示該View
<com.example.percentloadinganimator.PercentLoading/>
第三步:重寫onSizeChanged方法和onDraw方法
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
}
第四步:準(zhǔn)備畫一個(gè)圓環(huán)所需要的元素
private var cx = 0f//初始化到x軸的距離
private var cy = 0f//初始化到y(tǒng)軸的距離
private var radius = 0f//初始化圓的半徑
private val mstrokeWidth = 50f//初始化圓圈的寬度
private var BackProgressCircle = Paint().apply {
color = Color.GRAY//設(shè)置背景圓圈的顏色
style = Paint.Style.STROKE//設(shè)置圓圈的風(fēng)格為STROKE
strokeWidth = mstrokeWidth//設(shè)置圓圈的寬度
}//初始化繪制圓圈的畫筆
第五步:確定要畫圓圈相對(duì)于自定義View的位置
在View的大小確定下來之后,我們需要對(duì)圓心的位置以及半徑進(jìn)行確定,這個(gè)時(shí)候在onSizeChanged方法中修改之前初始化了的cx、cy和radius的值
1.確定圓心
圓心的位置應(yīng)該處于控件的正中心,只需要取寬度和長度的一半即可
2.確定半徑
由于自定義的控件是矩形的,要想使得圓圈不越界且最大,則圓圈要與矩形控件的兩條邊相切。所以這里需要選取控件較短的邊作為確定半徑的數(shù)據(jù)。
此外,由于給圓圈增加了寬度,半徑的長度還要減去寬度。

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
cx = width*0.5f
cy = height*0.5f
//定位到控件的中心
radius = Math.min(width,height)/2f-mstrokeWidth//取View長或?qū)挼囊话耄贉p去圓圈的寬度作為半徑
//這里必須要減去圓圈的寬度,否則會(huì)View中就顯示不出來
}
現(xiàn)在我們已經(jīng)做出了第一個(gè)圓圈,作為背景圓圈

第六步:繪制進(jìn)度條圓?。ù隧?xiàng)目的難點(diǎn))
1.方法的參數(shù)詳解
在此過程中需要使用到drawArc方法,下面對(duì)該方法做簡(jiǎn)單介紹
public void drawArc(float left, float top, float right, float bottom, float startAngle,
float sweepAngle, boolean useCenter, @NonNull Paint paint) {
super.drawArc(left, top, right, bottom, startAngle, sweepAngle, useCenter, paint);
}
該方法的難點(diǎn)在于理解四個(gè)方向參數(shù)的意義。把所畫的圓弧假想為一個(gè)完整的圓圈,再假想一個(gè)與這個(gè)圓圈外切的正方形,其中l(wèi)eft、top、right、bottom為正方形相對(duì)于自定義控件的4個(gè)相對(duì)位置,注意,是相對(duì)于自定義的控件!通過這4個(gè)位置,可以確定所畫圓弧的圓心。

startAngle為開始繪制的角度,開發(fā)工具已經(jīng)規(guī)定好了圓圈的3點(diǎn)鐘方向?yàn)?°,順時(shí)針方向角度為正。逆時(shí)針方向角度為負(fù)。
sweepAngle為需要掃過的角度
useCenter表示,在繪制的過程中是否需要與圓心相連。如果是true,則繪制出來的是扇形。
在理清各個(gè)參數(shù)后,我們不難知道,left和top是圓圈的寬度mstrokeWidth,right是控件的寬度-圓圈的寬度,bottom是控件的高度-圓圈的寬度,起始角度為-90°
2.參數(shù)的確定
(1)確定四個(gè)方向
通過之前對(duì)參數(shù)的分析,我們不難計(jì)算出
left = mstrokeWidth
top = mstrokeWidth
right = width.toFloat()-mstrokeWidth
bottom = height.toFloat()-mstrokeWidth
(2)確定初始角度
我們從圓圈的頂部開始畫,所以startAngle = -90f
(3)確定掃過的角度
掃過的角度應(yīng)該是時(shí)刻變化的,而不是固定的一個(gè)角度。所以我們需要時(shí)刻獲取圓弧當(dāng)前的屬性值,再乘以360,就是當(dāng)前繪制的角度
var Progress = 0f//初始化Progress,由于外部要訪問該值,不能設(shè)為private
set(value){
field = value
invalidate()
}//通過field來更新Progress的值,并且通過invalidate來刷新(這一步需要通過之后的動(dòng)畫來實(shí)現(xiàn))
(4)初始化進(jìn)度條(Arc)的畫筆
private var ForeProgressCircle = Paint().apply {
color = Color.GREEN//設(shè)置進(jìn)度條的顏色
style = Paint.Style.STROKE
strokeWidth = mstrokeWidth
}//進(jìn)度條圓圈的畫筆
(5)更新onDraw方法
override fun onDraw(canvas: Canvas?) {
//繪制背景圓圈
canvas?.drawCircle(cx,cy,radius,progressPaint)
//繪制進(jìn)度條
canvas?.drawArc(
mstrokeWidth,mstrokeWidth,
width.toFloat()-mstrokeWidth,
height.toFloat()-mstrokeWidth,
-90f,360*Progress,
false,PercentprogressPaint
)
}
第七步:添加進(jìn)度條屬性動(dòng)畫
1.添加開始動(dòng)畫和停止動(dòng)畫按鈕
開始動(dòng)畫按鈕id為StartAnimatorbtn,停止動(dòng)畫按鈕id為StopAnimatorbtn

2.在主界面使用懶加載添加屬性動(dòng)畫
private val ProgressPercentAnimotor:ValueAnimator by lazy {
ValueAnimator.ofFloat(0f,1f).apply {
duration = 2000
addUpdateListener{
percentLoading.Progress =it.animatedValue as Float//獲取進(jìn)度條屬性的值
}
}
}
3.實(shí)現(xiàn)動(dòng)畫效果
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
StartAnimator.setOnClickListener {
if(ProgressPercentAnimotor.isPaused){
ProgressPercentAnimotor.resume()//如果當(dāng)前動(dòng)畫是停止?fàn)顟B(tài),則點(diǎn)擊開始按鈕重新運(yùn)行
}else{
ProgressPercentAnimotor.start()
}
}
StopAnimator.setOnClickListener {
ProgressPercentAnimotor.pause()
}
}
4.效果圖

第八步:用drawText方法繪制文本,記錄進(jìn)度
1.drawText方法一共有4種參數(shù)類型,這里我們選擇第二種

第一個(gè)參數(shù)為要寫的內(nèi)容,第二個(gè)參數(shù)為TextView的橫坐標(biāo),第三個(gè)參數(shù)為TextView的縱坐標(biāo),第四個(gè)參數(shù)為畫筆。
2.確定參數(shù)
private var text = "${(Progress*100).toInt()}%"http://文本內(nèi)容為一個(gè)百分?jǐn)?shù)
private var TextPaint = Paint().apply {
color = Color.BLACK
style = Paint.Style.FILL
textSize = 100f
textAlign = Paint.Align.CENTER//字體的位置
}//初始化繪制字體的畫筆
//x = cx,y = cy
3.繪制文本
canvas?.drawText(text,cx,cy,TextPaint)
4.寫進(jìn)onDraw方法里面
override fun onDraw(canvas: Canvas?) {
//繪制背景圓圈
canvas?.drawCircle(cx,cy,radius,progressPaint)
//繪制進(jìn)度條
canvas?.drawArc(
mstrokeWidth,mstrokeWidth,
width.toFloat()-mstrokeWidth,
height.toFloat()-mstrokeWidth,
-90f,360*Progress,
false,PercentprogressPaint
)
val text = "${(Progress*100).toInt()}%"
val metrics = TextPaint.fontMetrics
var space = (metrics.descent-metrics.ascent)/2f - metrics.descent
canvas?.drawText(text,cx,cy+space,TextPaint)
}
5.效果圖

這個(gè)時(shí)候整個(gè)效果就快要實(shí)現(xiàn)了,但是我們發(fā)現(xiàn)了一個(gè)問題,文本并沒有處于圓圈的正中心。
6.自定義Text講解

整個(gè)TextView由兩部分構(gòu)成,一個(gè)是這個(gè)文本與上個(gè)文本之間的間距,第二個(gè)是文本自身的size。
為了使Text處于圓圈的中心,我們需要使用Paint類里面的FontMetrics方法,通過這個(gè)方法,我們可以獲取跟文本有關(guān)的4個(gè)參數(shù),top,bottom,ascent,descent。
在自定義Text的時(shí)候,系統(tǒng)會(huì)給文本自動(dòng)設(shè)置一個(gè)基準(zhǔn)線,如圖中紅線所示。如果不進(jìn)行設(shè)置,系統(tǒng)將文本的位置自動(dòng)調(diào)為基準(zhǔn)線處。但是基準(zhǔn)線不是中線,所以視覺上看,文本有偏上的感覺。我們需要將文本設(shè)在中線的位置,如圖中綠線所示。
4個(gè)參數(shù)都是相對(duì)于基準(zhǔn)線來計(jì)算的,top表示基準(zhǔn)線到文本頂端的距離,bottom表示基準(zhǔn)線到文本底端的距離,ascent表示基準(zhǔn)線到文本自身頂端的距離,descent表示基準(zhǔn)線到文本自身底端的距離。
還有一點(diǎn)需要注意的是,基準(zhǔn)線上方的參數(shù)為負(fù)數(shù),基準(zhǔn)線下方的參數(shù)為正數(shù)。
由此可以計(jì)算出,需要下移的距離為space = (descent - ascent)/2 - descent
val metrics = TextPaint.fontMetrics
var space = (metrics.descent-metrics.ascent)/2f - metrics.descent
7.修改后的onDraw方法為
override fun onDraw(canvas: Canvas?) {
canvas?.drawCircle(cx,cy,radius,BackProgressCircle)
canvas?.drawArc(mstrokeWidth,mstrokeWidth,width-mstrokeWidth,height-mstrokeWidth,
-90f,360*Progress,false,ForeProgressCircle)
var text = "${(Progress*100).toInt()}%"http://文本內(nèi)容為一個(gè)百分?jǐn)?shù)
val metrics = TextPaint.fontMetrics
var space = (metrics.descent-metrics.ascent)/2f - metrics.descent
canvas?.drawText(text,cx,cy+space,TextPaint)
}
最終效果圖

全部代碼
1.MainActivity
package com.example.percentloadinganimator
import android.animation.ValueAnimator
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
private val ProgressPercentAnimotor:ValueAnimator by lazy {
ValueAnimator.ofFloat(0f,1f).apply {
duration = 2000
addUpdateListener{
percentLoading.Progress =it.animatedValue as Float//獲取進(jìn)度條屬性的值
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
StartAnimator.setOnClickListener {
if(ProgressPercentAnimotor.isPaused){
ProgressPercentAnimotor.resume()//如果當(dāng)前動(dòng)畫是停止?fàn)顟B(tài),則點(diǎn)擊開始按鈕重新運(yùn)行
}else{
ProgressPercentAnimotor.start()
}
}
StopAnimator.setOnClickListener {
ProgressPercentAnimotor.pause()
}
}
}
2.PercentLoading
package com.example.percentloadinganimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
class PercentLoading:View {
constructor(context:Context):super(context)
constructor(context: Context,attrs:AttributeSet):super(context,attrs){}
private var cx = 0f//初始化到x軸的距離
private var cy = 0f//初始化到y(tǒng)軸的距離
private var radius = 0f//初始化圓的半徑
private val mstrokeWidth = 50f//初始化圓圈的寬度
private var BackProgressCircle = Paint().apply {
color = Color.GRAY//設(shè)置背景圓圈的顏色
style = Paint.Style.STROKE//設(shè)置圓圈的風(fēng)格為STROKE
strokeWidth = mstrokeWidth//設(shè)置圓圈的寬度
}//初始化繪制圓圈的畫筆
var Progress = 0f//初始化Progress
set(value){
field = value
invalidate()
}//通過field來更新Progress的值,并且通過invalidate來刷新
private var ForeProgressCircle = Paint().apply {
color = Color.GREEN//設(shè)置進(jìn)度條的顏色
style = Paint.Style.STROKE
strokeWidth = mstrokeWidth
}//初始化繪制進(jìn)度條圓圈的畫筆
private var TextPaint = Paint().apply {
color = Color.BLACK
style = Paint.Style.FILL
textSize = 100f
textAlign = Paint.Align.CENTER//字體的位置
}//初始化繪制字體的畫筆
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
cx = width*0.5f
cy = height*0.5f
//定位到控件的中心
radius = Math.min(width,height)/2f-mstrokeWidth//取View長或?qū)挼囊话?,再減去圓圈的寬度作為半徑
//這里必須要減去圓圈的寬度,否則會(huì)View中就顯示不出來
}
override fun onDraw(canvas: Canvas?) {
canvas?.drawCircle(cx,cy,radius,BackProgressCircle)
canvas?.drawArc(mstrokeWidth,mstrokeWidth,width-mstrokeWidth,height-mstrokeWidth,
-90f,360*Progress,false,ForeProgressCircle)
var text = "${(Progress*100).toInt()}%"http://文本內(nèi)容為一個(gè)百分?jǐn)?shù)
val metrics = TextPaint.fontMetrics
var space = (metrics.descent-metrics.ascent)/2f - metrics.descent
canvas?.drawText(text,cx,cy+space,TextPaint)
}
}
3.MainActivity的xml文件
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.percentloadinganimator.PercentLoading
android:layout_width="300dp" android:layout_height="300dp"
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="8dp"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="8dp" android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent" android:layout_marginTop="8dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.238" android:id="@+id/percentLoading"/>
<Button
android:text="@string/開始動(dòng)畫"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/StartAnimator" app:layout_constraintStart_toStartOf="@+id/percentLoading"
android:layout_marginTop="120dp" app:layout_constraintTop_toBottomOf="@+id/percentLoading"/>
<Button
android:text="@string/停止動(dòng)畫"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/StopAnimator" app:layout_constraintEnd_toEndOf="@+id/percentLoading"
app:layout_constraintTop_toTopOf="@+id/StartAnimator"
app:layout_constraintBottom_toBottomOf="@+id/StartAnimator"/>
</androidx.constraintlayout.widget.ConstraintLayout>