动画效果
(最后有全部代码)
第一步:新建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类的对象
方法二:在要显示该自定义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)
}
第四步:准备画一个圆环所需要的元素
private var cx = 0f//初始化到x轴的距离
private var cy = 0f//初始化到y轴的距离
private var radius = 0f//初始化圆的半径
private val mstrokeWidth = 50f//初始化圆圈的宽度
private var BackProgressCircle = Paint().apply {
color = Color.GRAY//设置背景圆圈的颜色
style = Paint.Style.STROKE//设置圆圈的风格为STROKE
strokeWidth = mstrokeWidth//设置圆圈的宽度
}//初始化绘制圆圈的画笔
第五步:确定要画圆圈相对于自定义View的位置
在View的大小确定下来之后,我们需要对圆心的位置以及半径进行确定,这个时候在onSizeChanged方法中修改之前初始化了的cx、cy和radius的值
1.确定圆心
圆心的位置应该处于控件的正中心,只需要取宽度和长度的一半即可
2.确定半径
由于自定义的控件是矩形的,要想使得圆圈不越界且最大,则圆圈要与矩形控件的两条边相切。所以这里需要选取控件较短的边作为确定半径的数据。
此外,由于给圆圈增加了宽度,半径的长度还要减去宽度。
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长或宽的一半,再减去圆圈的宽度作为半径
//这里必须要减去圆圈的宽度,否则会View中就显示不出来
}
现在我们已经做出了第一个圆圈,作为背景圆圈
第六步:绘制进度条圆弧(此项目的难点)
1.方法的参数详解
在此过程中需要使用到drawArc方法,下面对该方法做简单介绍
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);
}
该方法的难点在于理解四个方向参数的意义。把所画的圆弧假想为一个完整的圆圈,再假想一个与这个圆圈外切的正方形,其中left、top、right、bottom为正方形相对于自定义控件的4个相对位置,注意,是相对于自定义的控件!通过这4个位置,可以确定所画圆弧的圆心。
startAngle为开始绘制的角度,开发工具已经规定好了圆圈的3点钟方向为0°,顺时针方向角度为正。逆时针方向角度为负。
sweepAngle为需要扫过的角度
useCenter表示,在绘制的过程中是否需要与圆心相连。如果是true,则绘制出来的是扇形。
在理清各个参数后,我们不难知道,left和top是圆圈的宽度mstrokeWidth,right是控件的宽度-圆圈的宽度,bottom是控件的高度-圆圈的宽度,起始角度为-90°
2.参数的确定
(1)确定四个方向
通过之前对参数的分析,我们不难计算出
left = mstrokeWidth
top = mstrokeWidth
right = width.toFloat()-mstrokeWidth
bottom = height.toFloat()-mstrokeWidth
(2)确定初始角度
我们从圆圈的顶部开始画,所以startAngle = -90f
(3)确定扫过的角度
扫过的角度应该是时刻变化的,而不是固定的一个角度。所以我们需要时刻获取圆弧当前的属性值,再乘以360,就是当前绘制的角度
var Progress = 0f//初始化Progress,由于外部要访问该值,不能设为private
set(value){
field = value
invalidate()
}//通过field来更新Progress的值,并且通过invalidate来刷新(这一步需要通过之后的动画来实现)
(4)初始化进度条(Arc)的画笔
private var ForeProgressCircle = Paint().apply {
color = Color.GREEN//设置进度条的颜色
style = Paint.Style.STROKE
strokeWidth = mstrokeWidth
}//进度条圆圈的画笔
(5)更新onDraw方法
override fun onDraw(canvas: Canvas?) {
//绘制背景圆圈
canvas?.drawCircle(cx,cy,radius,progressPaint)
//绘制进度条
canvas?.drawArc(
mstrokeWidth,mstrokeWidth,
width.toFloat()-mstrokeWidth,
height.toFloat()-mstrokeWidth,
-90f,360*Progress,
false,PercentprogressPaint
)
}
第七步:添加进度条属性动画
1.添加开始动画和停止动画按钮
开始动画按钮id为StartAnimatorbtn,停止动画按钮id为StopAnimatorbtn
2.在主界面使用懒加载添加属性动画
private val ProgressPercentAnimotor:ValueAnimator by lazy {
ValueAnimator.ofFloat(0f,1f).apply {
duration = 2000
addUpdateListener{
percentLoading.Progress =it.animatedValue as Float//获取进度条属性的值
}
}
}
3.实现动画效果
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
StartAnimator.setOnClickListener {
if(ProgressPercentAnimotor.isPaused){
ProgressPercentAnimotor.resume()//如果当前动画是停止状态,则点击开始按钮重新运行
}else{
ProgressPercentAnimotor.start()
}
}
StopAnimator.setOnClickListener {
ProgressPercentAnimotor.pause()
}
}
4.效果图
第八步:用drawText方法绘制文本,记录进度
1.drawText方法一共有4种参数类型,这里我们选择第二种
第一个参数为要写的内容,第二个参数为TextView的横坐标,第三个参数为TextView的纵坐标,第四个参数为画笔。
2.确定参数
private var text = "${(Progress*100).toInt()}%"//文本内容为一个百分数
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.写进onDraw方法里面
override fun onDraw(canvas: Canvas?) {
//绘制背景圆圈
canvas?.drawCircle(cx,cy,radius,progressPaint)
//绘制进度条
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.效果图
这个时候整个效果就快要实现了,但是我们发现了一个问题,文本并没有处于圆圈的正中心。
6.自定义Text讲解
整个TextView由两部分构成,一个是这个文本与上个文本之间的间距,第二个是文本自身的size。
为了使Text处于圆圈的中心,我们需要使用Paint类里面的FontMetrics方法,通过这个方法,我们可以获取跟文本有关的4个参数,top,bottom,ascent,descent。
在自定义Text的时候,系统会给文本自动设置一个基准线,如图中红线所示。如果不进行设置,系统将文本的位置自动调为基准线处。但是基准线不是中线,所以视觉上看,文本有偏上的感觉。我们需要将文本设在中线的位置,如图中绿线所示。
4个参数都是相对于基准线来计算的,top表示基准线到文本顶端的距离,bottom表示基准线到文本底端的距离,ascent表示基准线到文本自身顶端的距离,descent表示基准线到文本自身底端的距离。
还有一点需要注意的是,基准线上方的参数为负数,基准线下方的参数为正数。
由此可以计算出,需要下移的距离为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()}%"//文本内容为一个百分数
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//获取进度条属性的值
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
StartAnimator.setOnClickListener {
if(ProgressPercentAnimotor.isPaused){
ProgressPercentAnimotor.resume()//如果当前动画是停止状态,则点击开始按钮重新运行
}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轴的距离
private var radius = 0f//初始化圆的半径
private val mstrokeWidth = 50f//初始化圆圈的宽度
private var BackProgressCircle = Paint().apply {
color = Color.GRAY//设置背景圆圈的颜色
style = Paint.Style.STROKE//设置圆圈的风格为STROKE
strokeWidth = mstrokeWidth//设置圆圈的宽度
}//初始化绘制圆圈的画笔
var Progress = 0f//初始化Progress
set(value){
field = value
invalidate()
}//通过field来更新Progress的值,并且通过invalidate来刷新
private var ForeProgressCircle = Paint().apply {
color = Color.GREEN//设置进度条的颜色
style = Paint.Style.STROKE
strokeWidth = mstrokeWidth
}//初始化绘制进度条圆圈的画笔
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长或宽的一半,再减去圆圈的宽度作为半径
//这里必须要减去圆圈的宽度,否则会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()}%"//文本内容为一个百分数
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/开始动画"
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/停止动画"
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>