Android custom spherical water ripple belt arc progress effect (example code)

demand

As follows, a circular water ripple with progress is realized. Two layers of water ripple need to be displayed gradually, and there is an arc progress on the periphery.

thinking

Peripheral arc progress: it can be realized through canvas. Drawarc(). Because the arc needs to realize gradient, you can set shader (sweepgradient) rendering for the brush. In order to ensure that the color value at the beginning of the arc is always consistent, you need to dynamically adjust the parameters of the shader. See for details

SweepGradient(centerX.toFloat(),centerY.toFloat(),circleColors[0],floatArrayOf(0f,value / 100f))

The fourth parameter needs to fill in the corresponding data proportion according to the current progress. Students who do not understand can consult Baidu by themselves.

Realization of water ripple: it is directly realized by using Bezier curve path. Quadto (), and the wave effect is drawn by stretching a horizontal line. You can control the wave height by controlling the height of the stretching point (waveamplitude) from the horizontal line. As for the movement of waves, it can be realized by moving the starting position of the translation horizontal line. When using the animation cycle, in order to display stably, it is necessary to draw waves with integer multiple periods strictly.

Implementation of circular shape: draw a complete circle, and then combine and cut the water ripple path through path. Op(). Note that there is a pit in Android 6, and there will be obvious jitter using this method. In order to solve this problem, I draw an additional layer of arc to cover up the jitter.

Life cycle control: in order to reduce CPU consumption at some time, the start and pause of animation are controlled by controlling the variable user-defined lifedelegate (implemented based on kotlin proxy mode). Because the framework used by the author is based on MVVM, the code does not use attrs control attribute, so there will be no too many modifications here.

Overall implementation

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,waveColors[1],Shader.TileMode.CLAMP
     )
     SweepGradient(
       centerX.toFloat(),value / 100f)
     )
    }
    in 47..54 -> {
     fWaveShader = LinearGradient(
       0f,waveColors[2],waveColors[3],circleColors[1],value / 100f)
     )
    }
    else -> {
     fWaveShader = LinearGradient(
       0f,waveColors[4],waveColors[5],circleColors[2],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(),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(),innerRadius + ringWidth / 4,ringPaint)
 }
 private fun drawText(canvas: Canvas) {
  //绘制进度文字
  textPaint.isFakeBoldText = true
  textPaint.textSize = percentSize
  canvas.drawText(
    percent.toString(),centerY.toFloat() + textPaint.textSize / 2,textPaint
  )
  textPaint.isFakeBoldText = false
  textPaint.textSize = greedSize
  canvas.drawText(
    greedTip,centerY.toFloat() - textPaint.textSize * 2,textPaint
  )
 }
 private fun drawCircle(canvas: Canvas) {
  //绘制外围进度圆圈
  canvas.drawArc(outerCircleRectf,360f,false,outerNormalCirclePaint)
  canvas.drawArc(outerCircleRectf,90f,percent * 3.6f,outerCirclePaint)
  canvas.drawCircle(
    centerX.toFloat(),bgCirclePaint
  )
 }
}

summary

The above is the Android custom spherical water ripple belt arc progress effect (example code) introduced by Xiaobian. I hope it will be helpful to you. If you have any questions, please leave me a message and Xiaobian will reply to you in time. Thank you very much for your support to our website! If you think this article is helpful to you, welcome to reprint, please indicate the source, thank you!

The content of this article comes from the network collection of netizens. It is used as a learning reference. The copyright belongs to the original author.
THE END
分享
二维码
< <上一篇
下一篇>>