需求
如下,實現一個圓形水波紋,帶進度,兩層水波紋需要漸變顯示,且外圍有一個圓弧進度。
思路
外圍圓弧進度:可以通過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
)
}
}
總結
整體實現不會很複雜,都很好理解。對你有幫助的點個贊鼓勵下,謝謝啦~~~