安卓開發(fā)-用Kotlin語言自定義View實(shí)現(xiàn)百分比加載動(dòng)畫

動(dòng)畫效果

(最后有全部代碼)

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類的對(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ù)。
此外,由于給圓圈增加了寬度,半徑的長度還要減去寬度。

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長或?qū)挼囊话耄贉p去圓圈的寬度作為半徑
        //這里必須要減去圓圈的寬度,否則會(huì)View中就顯示不出來
    }

現(xiàn)在我們已經(jīng)做出了第一個(gè)圓圈,作為背景圓圈

image.png

第六步:繪制進(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è)位置,可以確定所畫圓弧的圓心。

image.png

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

image.png

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

1602838109688[00_00_04--00_00_24].gif

第八步:用drawText方法繪制文本,記錄進(jìn)度

1.drawText方法一共有4種參數(shù)類型,這里我們選擇第二種

image.png

第一個(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.效果圖

1602839786984[00_00_04--00_00_24].gif

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

6.自定義Text講解

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

整個(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)
    }

最終效果圖

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//獲取進(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>
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容