带你了解Android Progress Bar 3

2,619 阅读7分钟

1、前言

书接上一回 5分钟带你了解Android Progress Bar 2

上一回说到,要自定义ProgressBar,自定义一个ProgressBar,但功能不如原生的完整版!!主要还是用来了解如何写一个进度条,从而引出后面的,功能肯定没有原生的那么齐全啦。

如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏

系列更新:

5分钟带你了解Android Progress Bar1

5分钟带你了解Android Progress Bar2

5分钟带你了解Android Progress Bar3

看看效果先

image-20230212184032248

image-20230212183949898

2、条形ProgressBar简单仿写

(1)准备好Attribute

attr.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ZProgressBar2">
        <attr name="zTextColor" format="color" />
        <attr name="zBarHeight" format="dimension"/>
        <attr name="zMax" format="float" />
        <attr name="zProgress" format="float" />
        <attr name="zProgressColor" format="string" />
        <attr name="zRemainColor" format="string" />
        <attr name="zTextVisibility" format="boolean"/>
    </declare-styleable>
</resources>

随便想点属性啦

(2)新建一个ZProgressBar2.kt

class ZProgressBar2 @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {}

名字随意,新建文件 -> 继承自 View -> 由编译器补全代码。

image-20230209111517663

选@JvmOverloads的这条!这样Java也能用了

(3)准备好一些变量声明

声明啥,你自己看着办就行。我这里基本就是准备了,Attribute中属性

/**
 * 默认进度条颜色
 */
private val DEFAULT_PROGRESS_COLOR: Int = Color.parseColor("#FFA5E05B")
private val DEFAULT_REMAIN_COLOR: Int = Color.parseColor("#FFFFFFFF")
private val DEFAULT_TEXT_COLOR: Int = Color.parseColor("#FFFFFFFF")
​
/**
 * 默认进度
 */
private val DEFAULT_MAX: Float = 100f
private val DEFAULT_PROGRESS: Float = 0f
​
/**
 * 进度条最大值
 */
var mMax: Float = DEFAULT_MAX
var mMin: Float = DEFAULT_PROGRESS
/**
 * 进度条当前进度值
 */
var mProgress: Float = DEFAULT_PROGRESS
/**
 * 进度条颜色
 * 完成
 */
var mProgressColor = DEFAULT_PROGRESS_COLOR
/**
 * 进度条颜色
 * 剩余
 */
var mRemainColor = DEFAULT_REMAIN_COLOR
/**
 * 进度条高度
 */
var mBarHeight = 30f.dp
/**
 * 剩余进度区域
 */
private val mRemainRectF = RectF(0f, 0f, 0f, 0f)
​
/**
 * 已完成进度区域
 */
private val mProgressRectF = RectF(0f, 0f, 0f, 0f)
​
/**
 * ANTI_ALIAS_FLAG 抗锯齿
 * isAntiAlias 防抖动
 * Paint.Cap.ROUND 笔画凸出成半圆形
 */
private val mPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    strokeCap = Paint.Cap.ROUND
    isAntiAlias = true
}
​
/**
 * 进度文字颜色
 */
private var mTextColor = Color.WHITE
private var mTextVisibility = false
private var progressFormat = DecimalFormat("#")
​

(4)准备好读取Attribute的方法

记得在init中调用哈

/**
 * 读取自定义的布局属性
 */
private fun initArr(attrs: AttributeSet?) {
    val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ZProgressBar2)
    typedArray.run {
        mMax = getFloat(R.styleable.ZProgressBar2_zMax, mMax)
        mProgress = getFloat(R.styleable.ZProgressBar2_zProgress, mProgress)
        mProgressColor =
            getColor(R.styleable.ZProgressBar2_zProgressColor, DEFAULT_PROGRESS_COLOR)
        mRemainColor =
            getColor(R.styleable.ZProgressBar2_zRemainColor, DEFAULT_REMAIN_COLOR)
        mBarHeight = getDimension(R.styleable.ZProgressBar2_zBarHeight, 30f.dp)
        mTextColor = getColor(
            R.styleable.ZProgressBar2_zTextColor,
            DEFAULT_REMAIN_COLOR
        )
        mTextVisibility =
            getBoolean(R.styleable.ZProgressBar2_zTextVisibility, true)
    }
    typedArray.recycle()
}
init{
    initArr(attrs)
}

(5)在onDraw中绘制

主要代码就在这了,没多少,看注释!

override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)
    calculateProgressRect()
    canvas?.run {
        //先画剩余的,要让进度压在上面
        mPaint.color = mRemainColor
        //进度和剩余进度,就是两个圆角方形叠在一起
        drawRoundRect(mRemainRectF, mBarHeight / 2, mBarHeight / 2, mPaint)
        //再画进度
        mPaint.color = mProgressColor
        drawRoundRect(mProgressRectF, mBarHeight / 2, mBarHeight / 2, mPaint)
        //然后文字在最上层
        mPaint.textSize = mBarHeight * 0.5f
        //格式化Progress
        var mCurrentDrawText: String = progressFormat.format(mProgress * 100 / mMax)
        //画文字的基本操作,先测一下宽度
        val mDrawTextWidth = mPaint.measureText(mCurrentDrawText)
        //要判断下进度的宽度,够不够画文字出来
        if (mTextVisibility && mProgress > 0 && mProgressRectF.right > mDrawTextWidth) {
            mPaint.color = mTextColor
            drawText(
                mCurrentDrawText,
                mProgressRectF.right - mDrawTextWidth - mBarHeight * 0.4f,
                //descent/ascent 关于文字的高度可以去看看相关文章
                (height / 2.0f - (mPaint.descent() + mPaint.ascent()) / 2.0f).toInt().toFloat(),
                mPaint
            )
        }
    }
}
/**
 * 很常规的计算一个View的四个坐标的方法
 */
private fun calculateProgressRect() {
    val ttop = (height - mBarHeight) / 2.0f
    val bbottom = (height + mBarHeight) / 2.0f
    mProgressRectF.run {
        left = paddingLeft.toFloat()
        top = ttop
        /**
         * 计算已完成进度的长度
         */
        right = (width - paddingLeft - paddingRight) / (mMax * 1.0f) * mProgress + paddingLeft
        bottom = bbottom
    }
    mRemainRectF.run {
        left = paddingLeft.toFloat()
        top = ttop
        right = (width - paddingRight).toFloat()
        bottom = bbottom
    }
}

(6)暴露更新方法

重写下set方法就好了,都去调用invalidate()方法。

/**
 * 进度条最大值
 */
var mMax: Float = DEFAULT_MAX
    set(value) {
        field = value
        invalidate()
    }
​
var mMin: Float = DEFAULT_PROGRESS
    set(value) {
        field = value
        invalidate()
    }
​
/**
 * 进度条当前进度值
 */
@set:Synchronized
var mProgress: Float = DEFAULT_PROGRESS
    set(value) {
        if (value == field) {
            // No change from current.
            return
        }
            //不能超过最大值,最小值啊
        field = value.coerceIn(mMin, mMax)
        invalidate()
    }
​
/**
 * 进度条颜色
 * 完成
 */
var mProgressColor = DEFAULT_PROGRESS_COLOR
    set(value) {
        field = value
        invalidate()
    }
​
/**
 * 进度条颜色
 * 剩余
 */
var mRemainColor = DEFAULT_REMAIN_COLOR
    set(value) {
        field = value
        invalidate()
    }
​
/**
 * 进度条高度
 */
var mBarHeight = 30f.dp
    set(value) {
        field = value
        invalidate()
    }

没啦,就这么多,一个看起来差不多的就行了,简单版~

(7)完整代码

下面是全部代码,以及调用代码

import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import com.nf.framework.utillib.ext.dp
import com.nf.module_test.R
import java.text.DecimalFormat
/**
 * 仿造ProgressBar
 */
class ZProgressBar2 @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    /**
     * 默认进度条颜色
     */
    private val DEFAULT_PROGRESS_COLOR: Int = Color.parseColor("#FFA5E05B")
    private val DEFAULT_REMAIN_COLOR: Int = Color.parseColor("#FFFFFFFF")
    private val DEFAULT_TEXT_COLOR: Int = Color.parseColor("#FFFFFFFF")
​
    /**
     * 默认进度
     */
    private val DEFAULT_MAX: Float = 100f
    private val DEFAULT_PROGRESS: Float = 0f
​
    /**
     * 进度条最大值
     */
    var mMax: Float = DEFAULT_MAX
        set(value) {
            field = value
            invalidate()
        }
​
    var mMin: Float = DEFAULT_PROGRESS
        set(value) {
            field = value
            invalidate()
        }
​
    /**
     * 进度条当前进度值
     */
    @set:Synchronized
    var mProgress: Float = DEFAULT_PROGRESS
        set(value) {
            if (value == field) {
                // No change from current.
                return
            }
            field = value.coerceIn(mMin, mMax)
            invalidate()
        }
​
    /**
     * 进度条颜色
     * 完成
     */
    var mProgressColor = DEFAULT_PROGRESS_COLOR
        set(value) {
            field = value
            invalidate()
        }
​
    /**
     * 进度条颜色
     * 剩余
     */
    var mRemainColor = DEFAULT_REMAIN_COLOR
        set(value) {
            field = value
            invalidate()
        }
​
    /**
     * 进度条高度
     */
    var mBarHeight = 30f.dp
        set(value) {
            field = value
            invalidate()
        }
​
    /**
     * 剩余进度区域
     */
    private val mRemainRectF = RectF(0f, 0f, 0f, 0f)
​
    /**
     * 已完成进度区域
     */
    private val mProgressRectF = RectF(0f, 0f, 0f, 0f)
​
    /**
     * ANTI_ALIAS_FLAG 抗锯齿
     */
    private val mPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        /**
         * 笔画凸出成半圆形
         */
        strokeCap = Paint.Cap.ROUND
        /**
         * 防抖动
         */
        isAntiAlias = true
    }
​
    /**
     * 进度文字颜色
     */
    private var mTextColor = Color.WHITE
    private var mTextVisibility = false
    private var progressFormat = DecimalFormat("#")
​
    init {
        initArr(attrs)
    }
    /**
     * 读取自定义的布局属性
     */
    private fun initArr(attrs: AttributeSet?) {
        val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ZProgressBar2)
        typedArray.run {
            mMax = getFloat(R.styleable.ZProgressBar2_zMax, mMax)
            mProgress = getFloat(R.styleable.ZProgressBar2_zProgress, mProgress)
            mProgressColor =
                getColor(R.styleable.ZProgressBar2_zProgressColor, DEFAULT_PROGRESS_COLOR)
            mRemainColor =
                getColor(R.styleable.ZProgressBar2_zRemainColor, DEFAULT_REMAIN_COLOR)
            mBarHeight = getDimension(R.styleable.ZProgressBar2_zBarHeight, 30f.dp)
            mTextColor = getColor(
                R.styleable.ZProgressBar2_zTextColor,
                DEFAULT_REMAIN_COLOR
            )
            mTextVisibility =
                getBoolean(R.styleable.ZProgressBar2_zTextVisibility, true)
        }
        typedArray.recycle()
    }
​
    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        calculateProgressRect()
        canvas?.run {
            mPaint.color = mRemainColor
            drawRoundRect(mRemainRectF, mBarHeight / 2, mBarHeight / 2, mPaint)
            mPaint.color = mProgressColor
            drawRoundRect(mProgressRectF, mBarHeight / 2, mBarHeight / 2, mPaint)
            mPaint.textSize = mBarHeight * 0.5f
            var mCurrentDrawText: String = progressFormat.format(mProgress * 100 / mMax)
            mPaint.color = mTextColor
            val mDrawTextWidth = mPaint.measureText(mCurrentDrawText)
            if (mTextVisibility && mProgress > 0 && mProgressRectF.right > mDrawTextWidth) {
                drawText(
                    mCurrentDrawText,
                    mProgressRectF.right - mDrawTextWidth - mBarHeight * 0.4f,
                    (height / 2.0f - (mPaint.descent() + mPaint.ascent()) / 2.0f).toInt().toFloat(),
                    mPaint
                )
            }
        }
​
    }
​
    private fun calculateProgressRect() {
        val ttop = (height - mBarHeight) / 2.0f
        val bbottom = (height + mBarHeight) / 2.0f
        mProgressRectF.run {
            left = paddingLeft.toFloat()
            top = ttop
            /**
             * 计算已完成进度的长度
             */
            right = (width - paddingLeft - paddingRight) / (mMax * 1.0f) * mProgress + paddingLeft
            bottom = bbottom
        }
        mRemainRectF.run {
            left = paddingLeft.toFloat()
            top = ttop
            right = (width - paddingRight).toFloat()
            bottom = bbottom
        }
    }
}
<com.nf.module_test.seekbar.custom.ZProgressBar2
    android:id="@+id/zpb_01"
    android:layout_width="match_parent"
    android:layout_height="40dp"
    app:zBarHeight="40dp"
    app:zTextColor="#ffffff"
    app:zMax="100"
    app:zProgress="50" />

效果如下

image-20230212184032248

3、圆形ProgressBar

嘻嘻嘻,直的写出来了,圆的也的有一个吧,交互也得有吧?就在这个基础上,我们再扩展下

(1)依然我们需要准备好Attribute

这里我们直接在原来的基础上加一点吧

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ZProgressBar2">
        <attr name="zTextColor" format="color" />
        <attr name="zBarHeight" format="dimension" />
        <attr name="zMax" format="float" />
        <attr name="zProgress" format="float" />
        <attr name="zProgressColor" format="string" />
        <attr name="zRemainColor" format="string" />
        <attr name="zTextVisibility" format="boolean" />
        <attr name="zShape" format="integer">
            <enum name="line" value="0"/>
            <enum name="oval" value="1"/>
        </attr>
        <attr name="zFullDegree" format="integer" />
        <attr name="zAnimationDuration" format="integer" />
        <attr name="zDragEnabled" format="boolean" />
    </declare-styleable>
</resources>

(2)继续在ZProgressBar2中添加一些变量声明

还有一些变量在后面看吧

/**
 * 样式
 */
private var mShape = DEFAULT_SHAPE
/**
 *  进度条的角度
 */
private var mMaxDegree: Int = DEFAULT_FULL_DEGREE
    set(value) {
        field = value.coerceIn(0,360)
    }
/**
 * Thumb对象
 */
private var mThumbDraw: Drawable? = null
/**
 * Thumb对象限制宽高
 */
private var mThumbRadius = DEFAULT_BAR_WIDTH * 1.2f
/**
 * TODO
 */
private var mAnimationDuration: Int = 0
/**
 * 是否可以拖动
 */
private var mDragEnabled: Boolean = true
/**
 * 圆弧的半径
 */
private var mCircleRadius: Float = 0f
/**
 * 圆弧圆心位置
 */
private var centerX: Int = 0
/**
 * 圆弧圆心位置
 */
private var centerY: Int = 0
private var mWidth: Int = 0
private var mHeight: Int = 0

(3)准备好读取Attribute的方法

这里我们接着之前的写好就行了

private fun initArr(attrs: AttributeSet?) {
    val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ZProgressBar2)
    typedArray.run {
        mMax = getFloat(R.styleable.ZProgressBar2_zMax, mMax)
        mProgress = getFloat(R.styleable.ZProgressBar2_zProgress, mProgress)
        mProgressColor = getColor(R.styleable.ZProgressBar2_zProgressColor, DEFAULT_PROGRESS_COLOR)
        mRemainColor = getColor(R.styleable.ZProgressBar2_zRemainColor, DEFAULT_REMAIN_COLOR)
        mBarHeight = getDimension(R.styleable.ZProgressBar2_zBarHeight, DEFAULT_BAR_WIDTH)
        mTextColor = getColor(R.styleable.ZProgressBar2_zTextColor,DEFAULT_REMAIN_COLOR)
        mTextVisibility = getBoolean(R.styleable.ZProgressBar2_zTextVisibility, tr
        mMaxDegree = getInteger(R.styleable.ZProgressBar2_zFullDegree, DEFAULT_FULL_DEGREE)
        mAnimationDuration = getInteger(R.styleable.ZProgressBar2_zAnimationDuration, 500)
        mDragEnabled = getBoolean(R.styleable.ZProgressBar2_zDragEnabled, DEFAULT_ENABLE_TOUCH)
        mShape = getInteger(R.styleable.ZProgressBar2_zShape, DEFAULT_SHAPE)
        mThumbDraw = getDrawable(R.styleable.ZProgressBar2_zThumbDrawable)
        mThumbRadius = getDimension(R.styleable.ZProgressBar2_zThumbRadius, DEFAULT_BAR_WIDTH * 1.2f)
    }
    typedArray.recycle()
}

(4)处理onMeasure

/**
 * left:矩形左边线条离 y 轴的距离
 * top:矩形上面线条离 x 轴的距离
 * right:矩形右边线条离 y 轴的距离
 * bottom:矩形底部线条离 x 轴的距离
 * 这里我没有专门去处理layout_height、width,只处理固定宽高,交给你自己做啦
 */
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    if (mWidth == 0 || mHeight == 0) {
        mWidth = measuredWidth
        mHeight = measuredHeight
        //两者取最大
        mCircleRadius = mWidth.coerceAtMost(mHeight) / 2f
        //给BarHeight一个默认值,如果没有
        if (mBarHeight <= 0) mBarHeight = mCircleRadius / 12f
        //实际的绘制需要考虑到Thumb可能会超出中心圆一点点,所以减掉,如果没有就去减掉进度条的
        mCircleRadius -= if (mThumbDraw == null) {
            mBarHeight * 0.6f
        } else {
            mThumbRadius * 0.6f
        }
        centerX = mWidth / 2
        centerY = mHeight / 2
       	//进度的弧形就是靠这个
        mProgressRectF.run {
            left = centerX - mCircleRadius
            top = centerY - mCircleRadius
            right = centerX + mCircleRadius
            bottom = centerY + mCircleRadius
        }
    }
}

(5)处理下直线和圆的onDraw

这里通过读取的mShape,来判断是显示哪个。onDrawDir就是前面写的直线bar啦

override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)
    if (mShape == 0) {
        onDrawDir(canvas)
    } else {
        onDrawArc(canvas)
    }
}

思考下,因为我们是支持指定最大弧度的,所以进度条其实就是一个扇形,然后只画边就行了(否则你也可以用两个圆重叠达到一样的效果)

一个扇形画当前进度,一个画剩余进度,完事。再到中间画一个文字,进度条前端画一个thumb~~~

private fun onDrawArc(canvas: Canvas?) {
    canvas?.run {
        /**
         * 进度条起始点
         * shr 1 就是/2的意思
         */
        val startAngle: Float = (90 + ((360 - mMaxDegree) shr 1)).toFloat()
        //当前进度的百分比
        val sweep1: Float = mMaxDegree * (mProgress / mMax)
        //剩余进度的百分比
        val sweep2: Float = mMaxDegree - sweep1
        //因为我们是画进度条的,只需要画周边就行了,所用空心 描边
        mBarPaint.style = Paint.Style.STROKE
        //画笔的strokeWidth就相当于进度条宽度了
        mBarPaint.strokeWidth = mBarHeight
        mBarPaint.color = mRemainColor
        //很简单,绘制区域我们已经准备好了mProgressRectF
        //剩余起点用’当前进度起点+初始起点‘就是剩余进度的起点,终点显然就是sweep2
        drawArc(mProgressRectF, startAngle + sweep1, sweep2, false, mBarPaint)
        mBarPaint.color = mProgressColor
        //当前进度起点当然就是初始起点,终点当然是sweep1
        drawArc(mProgressRectF, startAngle, sweep1, false, mBarPaint)
        /**
         * 根据三角函数来计算出thumb的XY值
         * PS:你可能需要一定的数学知识和Android的坐标系知识
         * 如果你不会,记住就行,因为都一样~或者留言
         */
        val progressRadians =
            (((360.0f - mMaxDegree) /2 + sweep1) / 180 * Math.PI).toFloat()
        val thumbX: Float =
            centerX - mCircleRadius * sin(progressRadians.toDouble()).toFloat()
        val thumbY: Float =
            centerY + mCircleRadius * cos(progressRadians.toDouble()).toFloat()
        /**
         * 根据thumb的半径画出drawable对象
         */
        mThumbDraw?.let {
            it.setBounds(
                (thumbX - mThumbRadius / 2).toInt(), (thumbY - mThumbRadius / 2).toInt(),
                (thumbX + mThumbRadius / 2).toInt(), (thumbY + mThumbRadius / 2).toInt())
            it.draw(canvas)
        }
        /**
         * 文字绘制
         */
        if (mTextVisibility) {
            mTextPaint.textSize = (mCircleRadius.toInt() shr 1).toFloat()
            mTextPaint.color = Color.parseColor("#FFCB47")
            val textProgress: String = progressFormat.format(100f * mProgress / mMax)
            val textLen: Float = mTextPaint.measureText(textProgress)
            mTextPaint.getTextBounds("8", 0, 1, mTextBounds)
            val textProgressHeight: Int = mTextBounds.height()
            val extra: Float =
                if (textProgress.startsWith("1")) -mTextPaint.measureText("1") / 2 else 0F
            /**
             * 主要是计算文字的左上角坐标
             * 让它画在中间
             */
            drawText(
                textProgress,
                centerX - textLen / 2 + extra,
                centerY + textProgressHeight / 2f,
                mTextPaint
            )
            /**
             * shr 2 就是/4啦
             */
            mTextPaint.textSize = (mCircleRadius.toInt() shr 2).toFloat()
            drawText(
                "%",
                centerX + textLen / 2 + extra + 5,
                centerY + textProgressHeight / 2f,
                mTextPaint
            )
        }
    }
}

到这儿,绘制就基本结束了。接下啦我们要考虑点击事件的问题了

(6)点击事件的处理

我们直接分为两种,一种点击,一种按住滑动。话不多说,全在注释里

private var isDragging = true
private var lastProgress = -1F
/**
 * mDragEnabled变量用于控制是否启用拖拽功能
 * checkOnArc方法判断点击是否在圆弧上;
 * thumbProgress方法根据点击的位置计算进度并将进度更新到控件上;
 * isDragging变量用于标识当前是否在拖拽。
 * 当用户点击时,如果检测到点击位置在圆弧上,则执行进度计算并设置isDragging变量为true。
 * 当用户拖动时,如果当前正在拖拽,则执行进度计算;
 * 当用户抬起手指时,将isDragging设置为false。
 * 在方法末尾,返回true,表示该方法已经处理了该事件,不需要再向上传递。
 */
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
    if (!mDragEnabled) {
        return super.onTouchEvent(event)
    }
    val currentX = event.x
    val currentY = event.y
    when (event.action) {
        MotionEvent.ACTION_DOWN ->
            if (checkOnArc(currentX, currentY)) {
                thumbProgress(currentX, currentY)
                isDragging = true
            }
        MotionEvent.ACTION_MOVE -> if (isDragging) {
            thumbProgress(currentX, currentY)
        }
        MotionEvent.ACTION_UP -> isDragging = false
    }
    return true
}
​
/**
 * 确保滑块的拖动不会在进度条的相反方向进行。
 * 首先,通过计算当前点相对于中心点的极角
 * 并将其转换为当前进度。
 * 接着,将该进度与当前最后一次进度(lastProgress)进行比较
 * 如果两者差值小于最大值的一半,则更新当前进度(mProgress)为该进度;
 * 否则,当前进度不变。
 */
private fun thumbProgress(currentX: Float, currentY: Float) {
    var nextProgress = calculateDegree(currentX, currentY) / mMaxDegree * mMax
    nextProgress = nextProgress.coerceAtMost(mMax).coerceAtLeast(mMin)
    val delta = abs(nextProgress - lastProgress)
    if (delta < mMax / 2 || lastProgress == -1F) {
        lastProgress = nextProgress
        mProgress = nextProgress
    }
}
​
/**
 * 检查距离是否在圆弧的内圈和外圈之间,并检查角度是否在圆弧的开始和结束位置的一定范围内
 * 首先调用calculateDistance函数计算给定点(currentX,currentY)与(centerX,centerY)之间的距离。
 * 然后调用calculateDegree函数计算给定点与圆心之间的角度。
 * 最后,该函数通过比较这个距离和角度与圆弧上的特定范围,来判断给定的点是否在圆弧范围内。
 * return 给定的点(currentX,currentY)是否在一个弧形范围内
 */
private fun checkOnArc(currentX: Float, currentY: Float): Boolean {
    val distance = calculateDistance(currentX, currentY, centerX.toFloat(), centerY.toFloat())
    val degree = calculateDegree(currentX, currentY)
    return distance > mCircleRadius - mBarHeight * 3 //点击区域弧内
            && distance < mCircleRadius + mBarHeight * 3 //点击区域弧外
            && degree >= -8 &&  //圆弧开始位置
            degree <= mMaxDegree + 8 //圆弧结束位置
}
​
/**
 * 计算两点(x1,y1)和(x2,y2)之间的距离
 * 通过公式√((x1-x2)²+(y1-y2)²)计算两点间的欧几里得距离
 */
private fun calculateDistance(x1: Float, y1: Float, x2: Float, y2: Float): Float {
    return sqrt(((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)).toDouble()).toFloat()
}
​
/**
 *  计算当前点(currentX,currentY)与圆心(centerX,centerY)之间的角度
 *  首先,通过反正切函数atan计算出角度a1
 *  接着,根据当前点的位置与圆心的关系,分三种情况讨论
 *  如果当前点在圆心下方,则在a1上加180°
 *  如果当前点在圆心上方且在圆心右方,则在a1上加360°
 *  最后,减去360°与mMaxDegree的差的一半,并返回结果
 *  这样就得到了当前点相对于圆心的角度值,并限制在了mMaxDegree范围内。
 */
private fun calculateDegree(currentX: Float, currentY: Float): Float {
    var a1 =
        (atan((1.0f * (centerX - currentX) / (currentY - centerY)).toDouble()) / Math.PI * 180).toFloat()
    if (currentY < centerY) {
        a1 += 180f
    } else if (currentY > centerY && currentX > centerX) {
        a1 += 360f
    }
    return a1 - (360 - mMaxDegree) / 2
}

(7)完成!

调用下~

        <LinearLayout
            android:orientation="horizontal"
            android:layout_width="match_parent"
            android:layout_gravity="center"
            android:gravity="center"
            android:layout_height="wrap_content">
            <com.nf.module_test.seekbar.custom.ZProgressBar2
                android:id="@+id/zpb_01"
                android:layout_width="100dp"
                android:layout_height="100dp"
                app:zBarHeight="5dp"
                app:zTextColor="#ffffff"
                app:zShape="oval"
                app:zFullDegree="90"
                app:zThumbDrawable="@drawable/icon_seekbar_thum"
                app:zMax="100"
                app:zProgress="50" />
​
            <com.nf.module_test.seekbar.custom.ZProgressBar2
                android:id="@+id/zpb_02"
                android:layout_width="100dp"
                android:layout_height="100dp"
                app:zBarHeight="5dp"
                app:zFullDegree="180"
                app:zMax="100"
                app:zProgress="50"
                app:zShape="oval"
                app:zTextColor="#ffffff"
                app:zThumbDrawable="@drawable/icon_seekbar_thum" />
​
            <com.nf.module_test.seekbar.custom.ZProgressBar2
                android:id="@+id/zpb_03"
                android:layout_width="100dp"
                android:layout_height="100dp"
                app:zBarHeight="5dp"
                app:zTextColor="#ffffff"
                app:zShape="oval"
                app:zFullDegree="360"
                app:zThumbDrawable="@drawable/icon_seekbar_thum"
                app:zMax="100"
                app:zProgress="50" />
        </LinearLayout>

看看效果

NkOKlAG6Cn

完整代码ZProgressBar2.kt完整代码attr.xml

还有很多功能,都没有做完,不过这些看完,相信你已经可以自己接着来了,个人认为,自定义View的核心都在如何准确的计算View的坐标,那么这个圆形的相关,基本都在三角函数上啦。

结尾

这个系列已经3篇了,还有很多奇怪的进度条我都没有写,因为写了3篇了我感觉我的脑子里都是进度条,所以先暂停一会啦,当然如果谁真的很需要下一篇,评论、私信、邮箱我,我会速更第四篇,嘻嘻x.x

那么下篇见

“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情