需求
如下,实现一个圆形水波纹,带进度,两层水波纹需要渐变显示,且外围有一个圆弧进度。
思路
外围圆弧进度:可以通过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 自定义球型水波纹带圆弧进度效果(实例代码),希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对得得之家网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!