安卓开发-用Kotlin语言自定义View实现百分比加载动画

动画效果

(最后有全部代码)

1602840951140[00_00_01--00_00_05].gif

第一步:新建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.确定半径

由于自定义的控件是矩形的,要想使得圆圈不越界且最大,则圆圈要与矩形控件的两条边相切。所以这里需要选取控件较短的边作为确定半径的数据。
此外,由于给圆圈增加了宽度,半径的长度还要减去宽度。

image.png
 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中就显示不出来
    }

现在我们已经做出了第一个圆圈,作为背景圆圈

image.png

第六步:绘制进度条圆弧(此项目的难点)

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个位置,可以确定所画圆弧的圆心。

image.png

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

image.png

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.效果图

1602838109688[00_00_04--00_00_24].gif

第八步:用drawText方法绘制文本,记录进度

1.drawText方法一共有4种参数类型,这里我们选择第二种

image.png

第一个参数为要写的内容,第二个参数为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.效果图

1602839786984[00_00_04--00_00_24].gif

这个时候整个效果就快要实现了,但是我们发现了一个问题,文本并没有处于圆圈的正中心。

6.自定义Text讲解

8}(Q8%RCG`AWQQ1O(JYVRYK.jpg

整个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)
    }

最终效果图

1602840951140[00_00_01--00_00_05].gif

全部代码

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>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269