Android 自定义球型水波纹带圆弧进度效果(实例代码)
作者:榴莲&&山竹
需求
如下,实现一个圆形水波纹,带进度,两层水波纹需要渐变显示,且外围有一个圆弧进度。
思路
外围圆弧进度:可以通过canvas.drawArc()
实现。由于圆弧需要实现渐变,可以通过给画笔设置shader(SweepGradient)
渲染,为了保证圆弧起始的颜色值始终一致,需要动态调整shader的参数。具体参见
SweepGradient(centerX.toFloat(), centerY.toFloat(), circleColors[0], floatArrayOf(0f, value / 100f))
第四个参数需要根据当前进度填写对应数据比例。不懂的同学可以自行百度查阅。
水波纹的实现:直接使用贝塞尔曲线Path.quadTo()实现,通过拉伸水平直线绘制波浪效果。可以通过控制拉伸点(waveAmplitude)距离水平线的高度,达到波浪高度的控制。至于波浪的移动,可以通过移动平移水平线的起始位置来实现,在使用动画循环即可,为了能够稳定的显示,绘制波浪时需要严格绘制整数倍周期的波浪。
园形的实现:绘制一个完整的圆形,然后通过Path.op()合并裁剪水波纹path。注意点就是Android6有个坑,使用该方法会有明显的抖动,为了解决该问题,我的做法是多画一层圆弧以掩盖此抖动。
生命周期的控制:为了减少某些时刻CPU的损耗,通过控制变量自定义lifeDelegate(基于kotlin的代理模式实现)来控制动画的开始暂停。由于笔者使用的框架基于MVVM,所以代码就没有使用attrs控制属性,这里就不做过多的修改了。
整体实现
class WaveView(context: Context, attributeSet: AttributeSet? = null) : View(context, attributeSet) { companion object { const val RESUME = 0x1 const val STOP = 0x2 const val DESTROY = 0x3 } private var mWidth = 0 //控件整体宽度 private var mHeight = 0 //控件整体高度 //控件中心位置,x,y坐标 private var centerX = 0 private var centerY = 0 private var outerRadius = 0//外圈圆环的半径 private var innerRadius = 250f//内部圆圈的半径 private var radiusDist = 50f//内外圆圈的半径差距 private var fWaveShader: LinearGradient? = null private var sWaveShader: LinearGradient? = null private var wavePath = Path() private var waveCirclePath = Path() private val waveNum = 2 //波浪的渐变颜色数组 private val waveColors by lazy { arrayListOf( //深红色 intArrayOf(Color.parseColor("#E8E6421A"), Color.parseColor("#E2E96827")), intArrayOf(Color.parseColor("#E8E6421A"), Color.parseColor("#E2F19A7F")), //橙色 intArrayOf(Color.parseColor("#E8FDA085"), Color.parseColor("#E2F6D365")), intArrayOf(Color.parseColor("#E8FDA085"), Color.parseColor("#E2F5E198")), //绿色 intArrayOf(Color.parseColor("#E8009EFD"), Color.parseColor("#E22AF598")), intArrayOf(Color.parseColor("#E8009EFD"), Color.parseColor("#E28EF0C6")) ) } //外围圆环的渐变色 private val circleColors by lazy { arrayListOf( //深红色 intArrayOf(Color.parseColor("#FFF83600"), Color.parseColor("#FFF9D423")), //橙色 intArrayOf(Color.parseColor("#FFFDA085"), Color.parseColor("#FFF6D365")), //绿色 intArrayOf(Color.parseColor("#FF2AF598"), Color.parseColor("#FF009EFD")) ) } private val wavePaint by lazy { val paint = Paint() paint.isAntiAlias = true paint.strokeWidth = 1f paint } //波浪高度比例 private var waveWaterLevelRatio = 0f //波浪的振幅 private var waveAmplitude = 0f //波浪最大振幅高度 private var maxWaveAmplitude = 0f //外围圆圈的画笔 private val outerCirclePaint by lazy { val paint = Paint() paint.strokeWidth = 20f paint.strokeCap = Paint.Cap.ROUND paint.style = Paint.Style.STROKE paint.isAntiAlias = true paint } private val outerNormalCirclePaint by lazy { val paint = Paint() paint.strokeWidth = 20f paint.color = Color.parseColor("#FFF2F3F3") paint.style = Paint.Style.STROKE paint.isAntiAlias = true paint } private val bgCirclePaint by lazy { val paint = Paint() paint.color = Color.parseColor("#FFF6FAFF") paint.style = Paint.Style.FILL paint.isAntiAlias = true paint } private val textPaint by lazy { val paint = Paint() paint.style = Paint.Style.FILL paint.textAlign = Paint.Align.CENTER paint.isFakeBoldText = true paint.isAntiAlias = true paint } private val ringPaint by lazy { val paint = Paint() paint.style = Paint.Style.STROKE paint.color = Color.WHITE paint.isAntiAlias = true paint } //外围圆圈所在的矩形 private val outerCircleRectf by lazy { val rectF = RectF() rectF.set( centerX - outerRadius + outerCirclePaint.strokeWidth, centerY - outerRadius + outerCirclePaint.strokeWidth, centerX + outerRadius - outerCirclePaint.strokeWidth, centerY + outerRadius - outerCirclePaint.strokeWidth ) rectF } //外围圆圈的颜色渐变器矩阵,用于从90度开启渐变,由于线条头部有个小圆圈会导致显示差异,因此从88度开始绘制 private val sweepMatrix by lazy { val matrix = Matrix() matrix.setRotate(88f, centerX.toFloat(), centerY.toFloat()) matrix } //进度 0-100 var percent = 0 set(value) { field = value waveWaterLevelRatio = value / 100f //y = -4 * x2 + 4x抛物线计算振幅,水波纹振幅规律更加真实 waveAmplitude = (-4 * (waveWaterLevelRatio * waveWaterLevelRatio) + 4 * waveWaterLevelRatio) * maxWaveAmplitude // waveAmplitude = if (value < 50) 2f * waveWaterLevelRatio * maxWaveAmplitude else (-2 * waveWaterLevelRatio + 2) * maxWaveAmplitude val shader = when (value) { in 0..46 -> { fWaveShader = LinearGradient( 0f, mHeight.toFloat(), 0f, mHeight * (1 - waveWaterLevelRatio), waveColors[0], null, Shader.TileMode.CLAMP ) sWaveShader = LinearGradient( 0f, mHeight.toFloat(), 0f, mHeight * (1 - waveWaterLevelRatio), waveColors[1], null, Shader.TileMode.CLAMP ) SweepGradient( centerX.toFloat(), centerY.toFloat(), circleColors[0], floatArrayOf(0f, value / 100f) ) } in 47..54 -> { fWaveShader = LinearGradient( 0f, mHeight.toFloat(), 0f, mHeight * (1 - waveWaterLevelRatio), waveColors[2], null, Shader.TileMode.CLAMP ) sWaveShader = LinearGradient( 0f, mHeight.toFloat(), 0f, mHeight * (1 - waveWaterLevelRatio), waveColors[3], null, Shader.TileMode.CLAMP ) SweepGradient( centerX.toFloat(), centerY.toFloat(), circleColors[1], floatArrayOf(0f, value / 100f) ) } else -> { fWaveShader = LinearGradient( 0f, mHeight.toFloat(), 0f, mHeight * (1 - waveWaterLevelRatio), waveColors[4], null, Shader.TileMode.CLAMP ) sWaveShader = LinearGradient( 0f, mHeight.toFloat(), 0f, mHeight * (1 - waveWaterLevelRatio), waveColors[5], null, Shader.TileMode.CLAMP ) SweepGradient( centerX.toFloat(), centerY.toFloat(), circleColors[2], floatArrayOf(0f, value / 100f) ) } } shader.setLocalMatrix(sweepMatrix) outerCirclePaint.shader = shader invalidate() } private val greedTip = "Greed Index" //文本的字体大小 private var percentSize = 80f private var greedSize = 30f private var textColor = Color.BLACK //外围圆圈的画笔大小 private var outerStrokeWidth = 10f private var fAnimatedValue = 0f private var sAnimatedValue = 0f //动画 private val fValueAnimator by lazy { val valueAnimator = ValueAnimator() valueAnimator.duration = 1500 valueAnimator.repeatCount = ValueAnimator.INFINITE valueAnimator.interpolator = LinearInterpolator() valueAnimator.setFloatValues(0f, waveWidth) valueAnimator.addUpdateListener { animation -> fAnimatedValue = animation.animatedValue as Float invalidate() } valueAnimator } private val sValueAnimator by lazy { val valueAnimator = ValueAnimator() valueAnimator.duration = 2000 valueAnimator.repeatCount = ValueAnimator.INFINITE valueAnimator.interpolator = LinearInterpolator() valueAnimator.setFloatValues(0f, waveWidth) valueAnimator.addUpdateListener { animation -> sAnimatedValue = animation.animatedValue as Float invalidate() } valueAnimator } //一小段完整波浪的宽度 private var waveWidth = 0f var lifeDelegate by Delegates.observable(0) { _, old, new -> when (new) { RESUME -> onResume() STOP -> onPause() DESTROY -> onDestroy() } } //设置中间进度文本的字体大小 fun setPercentSize(size: Float) { percentSize = size invalidate() } //设置中间提示文本的字体大小 fun setGreedSize(size: Float) { greedSize = size invalidate() } //设置文本颜色 fun setTextColor(color: Int) { textColor = color textPaint.color = textColor invalidate() } //设置外围圆圈的宽度 fun setOuterStrokeWidth(width: Float) { outerStrokeWidth = width outerCirclePaint.strokeWidth = outerStrokeWidth outerNormalCirclePaint.strokeWidth = outerStrokeWidth invalidate() } //设置内圆半径 fun setInnerRadius(radius: Float) { innerRadius = radius invalidate() } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) mWidth = width - paddingStart - paddingEnd mHeight = height - paddingTop - paddingBottom centerX = mWidth / 2 centerY = mHeight / 2 outerRadius = mWidth.coerceAtMost(mHeight) / 2 radiusDist = outerRadius - innerRadius waveWidth = mWidth * 1.8f maxWaveAmplitude = mHeight * 0.15f } private fun onResume() { if (fValueAnimator.isStarted) { animatorResume() } else { fValueAnimator.start() sValueAnimator.start() } } private fun animatorResume() { if (fValueAnimator.isPaused || !fValueAnimator.isRunning) { fValueAnimator.resume() } if (sValueAnimator.isPaused || !sValueAnimator.isRunning) { sValueAnimator.resume() } } private fun onPause() { if (fValueAnimator.isRunning) { fValueAnimator.pause() } if (sValueAnimator.isRunning) { sValueAnimator.pause() } } private fun onDestroy() { fValueAnimator.cancel() sValueAnimator.cancel() } //当前窗口销毁时,回收动画资源 override fun onDetachedFromWindow() { onDestroy() super.onDetachedFromWindow() } override fun onDraw(canvas: Canvas) { drawCircle(canvas) drawWave(canvas) drawText(canvas) } private fun drawWave(canvas: Canvas) { //波浪当前高度 val level = (1 - waveWaterLevelRatio) * innerRadius * 2 + radiusDist //绘制所有波浪 for (num in 0 until waveNum) { //重置path wavePath.reset() waveCirclePath.reset() var startX = if (num == 0) {//第一条波浪的起始位置 wavePath.moveTo(-waveWidth + fAnimatedValue, level) -waveWidth + fAnimatedValue } else {//第二条波浪的起始位置 wavePath.moveTo(-waveWidth + sAnimatedValue, level) -waveWidth + sAnimatedValue } while (startX < mWidth + waveWidth) { wavePath.quadTo( startX + waveWidth / 4, level + waveAmplitude, startX + waveWidth / 2, level ) wavePath.quadTo( startX + waveWidth / 4 * 3, level - waveAmplitude, startX + waveWidth, level ) startX += waveWidth } wavePath.lineTo(startX, mHeight.toFloat()) wavePath.lineTo(0f, mHeight.toFloat()) wavePath.close() waveCirclePath.addCircle( centerX.toFloat(), centerY.toFloat(), innerRadius, Path.Direction.CCW ) waveCirclePath.op(wavePath, Path.Op.INTERSECT) //绘制波浪渐变色 wavePaint.shader = if (num == 0) { sWaveShader } else { fWaveShader } canvas.drawPath(waveCirclePath, wavePaint) } //Fixme android6设置Path.op存在明显抖动,因此多画一圈圆环 val ringWidth = outerRadius - outerStrokeWidth - innerRadius ringPaint.strokeWidth = ringWidth / 2 canvas.drawCircle(centerX.toFloat(), centerY.toFloat(), innerRadius + ringWidth / 4, ringPaint) } private fun drawText(canvas: Canvas) { //绘制进度文字 textPaint.isFakeBoldText = true textPaint.textSize = percentSize canvas.drawText( percent.toString(), centerX.toFloat(), centerY.toFloat() + textPaint.textSize / 2, textPaint ) textPaint.isFakeBoldText = false textPaint.textSize = greedSize canvas.drawText( greedTip, centerX.toFloat(), centerY.toFloat() - textPaint.textSize * 2, textPaint ) } private fun drawCircle(canvas: Canvas) { //绘制外围进度圆圈 canvas.drawArc(outerCircleRectf, 0f, 360f, false, outerNormalCirclePaint) canvas.drawArc(outerCircleRectf, 90f, percent * 3.6f, false, outerCirclePaint) canvas.drawCircle( centerX.toFloat(), centerY.toFloat(), innerRadius, bgCirclePaint ) } }
总结
以上所述是小编给大家介绍的Android 自定义球型水波纹带圆弧进度效果(实例代码),希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对脚本之家网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!