解析一下:package com.tplink.design.indicator
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.LinearGradient
import android.graphics.Paint
import android.graphics.Path
import android.graphics.Shader
import android.util.AttributeSet
import android.view.View
import android.view.animation.LinearInterpolator
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.IntDef
import androidx.appcompat.content.res.AppCompatResources
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.widget.TextViewCompat
import com.tplink.design.R
import com.tplink.design.util.accessibility.colorcontrast.wrapHighColorContrast
import kotlin.math.abs
import kotlin.math.min
/**
* Created by yujingzhi@tp-link.com.cn on 2021/11/18.
*/
open class TPWaveBallProgressIndicator @JvmOverloads constructor(
private var context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = R.style.Widget_TPDesign_WaveBallProgressIndicator
) : ConstraintLayout(
context.wrapHighColorContrast(attrs, defStyleAttr, defStyleRes),
attrs,
defStyleAttr,
defStyleRes
), TPWaveBallProgressView.OnProgressChangeListener {
val progressView: TPWaveBallProgressView
val progressTextView: TextView
var onProgressListener: OnProgressListener? = null
init {
//参考实现:com.google.android.material.appbar.MaterialToolbar.MaterialToolbar(android.content.Context, android.util.AttributeSet, int)
// Ensure we are using the correctly themed context rather than the context that was passed in.
// 避免materialThemeOverlay属性覆盖的配置失效
context = getContext()
val rootView = View.inflate(getContext(), R.layout.tpds_indicator_wave_ball_progress, this)
progressView = rootView.findViewById(R.id.progress_wave_ball)
progressTextView = rootView.findViewById(R.id.progress_tv)
progressView.setOnProgressChangeListener(this)
val ta = getContext().obtainStyledAttributes(
attrs,
R.styleable.TPWaveBallProgressIndicator,
defStyleAttr,
defStyleRes
)
if (ta.hasValue(R.styleable.TPWaveBallProgressIndicator_waveProgressSize)) {
val size = ta.getDimensionPixelSize(R.styleable.TPWaveBallProgressIndicator_waveProgressSize, -1)
progressView.minimumWidth = size
progressView.minimumHeight = size
}
progressView.progress = ta.getFloat(R.styleable.TPWaveBallProgressIndicator_waveProgress, 0f)
progressView.maxProgress = ta.getInteger(R.styleable.TPWaveBallProgressIndicator_waveMaxProgress, 100)
if (ta.hasValue(R.styleable.TPWaveBallProgressIndicator_wave1ColorStart)) {
progressView.wave1ColorStart =
ta.getColor(R.styleable.TPWaveBallProgressIndicator_wave1ColorStart, R.attr.colorPrimary)
}
if (ta.hasValue(R.styleable.TPWaveBallProgressIndicator_wave1ColorEnd)) {
progressView.wave1ColorEnd =
ta.getColor(R.styleable.TPWaveBallProgressIndicator_wave1ColorEnd, R.attr.colorPrimary)
}
if (ta.hasValue(R.styleable.TPWaveBallProgressIndicator_wave1Cycle)) {
progressView.wave1Cycle = ta.getDimensionPixelOffset(R.styleable.TPWaveBallProgressIndicator_wave1Cycle, 0)
}
if (ta.hasValue(R.styleable.TPWaveBallProgressIndicator_wave1Amplitude)) {
progressView.wave1Amplitude =
ta.getDimensionPixelOffset(R.styleable.TPWaveBallProgressIndicator_wave1Amplitude, 0)
}
if (ta.hasValue(R.styleable.TPWaveBallProgressIndicator_wave1Direction)) {
progressView.wave1Direction =
ta.getInt(R.styleable.TPWaveBallProgressIndicator_wave1Direction, TPWaveBallProgressView.REVERSE)
}
if (ta.hasValue(R.styleable.TPWaveBallProgressIndicator_wave2ColorStart)) {
progressView.wave2ColorStart = ta.getColor(
R.styleable.TPWaveBallProgressIndicator_wave2ColorStart, R.attr.colorPrimary
)
}
if (ta.hasValue(R.styleable.TPWaveBallProgressIndicator_wave2ColorEnd)) {
progressView.wave2ColorEnd = ta.getColor(
R.styleable.TPWaveBallProgressIndicator_wave2ColorEnd, R.attr.colorPrimary
)
}
if (ta.hasValue(R.styleable.TPWaveBallProgressIndicator_wave2Cycle)) {
progressView.wave2Cycle = ta.getDimensionPixelOffset(R.styleable.TPWaveBallProgressIndicator_wave2Cycle, 0)
}
if (ta.hasValue(R.styleable.TPWaveBallProgressIndicator_wave2Amplitude)) {
progressView.wave2Amplitude =
ta.getDimensionPixelOffset(R.styleable.TPWaveBallProgressIndicator_wave2Amplitude, 0)
}
if (ta.hasValue(R.styleable.TPWaveBallProgressIndicator_wave2Direction)) {
progressView.wave2Direction =
ta.getInt(R.styleable.TPWaveBallProgressIndicator_wave2Direction, TPWaveBallProgressView.FORWARD)
}
progressView.outerCircleColor =
ta.getColor(R.styleable.TPWaveBallProgressIndicator_outerCircleColor, Color.WHITE)
progressView.ballRadius = ta.getDimensionPixelOffset(R.styleable.TPWaveBallProgressIndicator_ballRadius, 0)
val textAppearanceId = ta.getResourceId(R.styleable.TPWaveBallProgressIndicator_android_textAppearance, 0)
val textColorId = ta.getResourceId(R.styleable.TPWaveBallProgressIndicator_android_textColor, 0)
if (textAppearanceId != 0) {
TextViewCompat.setTextAppearance(progressTextView, textAppearanceId)
}
if (textColorId != 0) {
progressTextView.setTextColor(AppCompatResources.getColorStateList(getContext(), textColorId))
}
onProgressChange(progressView, progressView.progress)
ta.recycle()
}
override fun onProgressChange(view: TPWaveBallProgressView, progress: Float) {
progressTextView.text = String.format("%s%%", progress.toInt())
onProgressListener?.onProgress(progress)
}
interface OnProgressListener {
fun onProgress(mProgress: Float)
}
}
class TPWaveBallProgressView @JvmOverloads constructor(
private var context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : View(
context.wrapHighColorContrast(attrs, defStyleAttr, defStyleRes),
attrs,
defStyleAttr,
defStyleRes
) {
companion object {
const val FORWARD = 0
const val REVERSE = 1
private const val DEFAULT_SIZE = 1000
private const val DEFAULT_AMPLITUDE = 30
private const val DEFAULT_CYCLE = 400
private const val DEFAULT_COLOR_START = Color.BLUE
private const val DEFAULT_COLOR_END = Color.RED
}
private val waveAnimator = ValueAnimator.ofFloat(0f, 1f).also {
it.duration = 2000
it.interpolator = LinearInterpolator()
it.repeatCount = ValueAnimator.INFINITE
it.addUpdateListener { animation: ValueAnimator ->
offsetFraction = animation.animatedFraction
ballRotateAngle = 360 * (animation.animatedValue as Float)
postInvalidate()
}
}
private val wave1Paint = Paint(Paint.ANTI_ALIAS_FLAG).also {
it.style = Paint.Style.FILL
}
private val wave2Paint = Paint(Paint.ANTI_ALIAS_FLAG).also {
it.style = Paint.Style.FILL
}
private val outerCirclePaint = Paint(Paint.ANTI_ALIAS_FLAG).also {
it.style = Paint.Style.FILL
}
private val ballPaint = Paint(Paint.ANTI_ALIAS_FLAG).also {
it.style = Paint.Style.FILL
}
private val wave1Path = Path()
private val wave2Path = Path()
private val outerCirclePath = Path()
private val innerCirclePath = Path()
private var offsetFraction = 0f
private var containerWidth = 0
private var containerHeight = 0
private var containerStart = 0
private var containerEnd = 0
private var containerTop = 0
private var containerBottom = 0
private var outerCircleWidth = 0
private var outerCircleHeight = 0
private var outerCircleStart = 0
private var outerCircleEnd = 0
private var outerCircleTop = 0
private var outerCircleBottom = 0
private var innerCircleWidth = 0
private var innerCircleHeight = 0
private var innerCircleStart = 0
private var innerCircleEnd = 0
private var innerCircleTop = 0
private var innerCircleBottom = 0
private var ballRotateAngle = 0f
var wave1Cycle: Int
var wave1Amplitude: Int
@ColorInt
var wave1ColorStart: Int
@ColorInt
var wave1ColorEnd: Int
@WaveDirection
var wave1Direction: Int
var wave2Cycle: Int
var wave2Amplitude: Int
@ColorInt
var wave2ColorStart: Int
@ColorInt
var wave2ColorEnd: Int
@WaveDirection
var wave2Direction: Int
@ColorInt
var outerCircleColor: Int = Color.WHITE
set(value) {
field = value
outerCirclePaint.color = outerCircleColor
}
var ballRadius: Int
private var _progress: Float
private var _maxProgress: Int
private var onProgressChangeListener: OnProgressChangeListener? = null
private var progressAnimator: ValueAnimator? = null
var progressAnimatorDuration = 0
var maxProgress: Int
get() = _maxProgress
set(value) {
var maxProgress = value
if (maxProgress == _maxProgress) {
return
}
if (maxProgress < 0) {
maxProgress = 0
}
_maxProgress = maxProgress
postInvalidate()
}
var progress: Float
get() = _progress
set(progress) {
setProgressInternal(progress)
}
init {
//参考实现:com.google.android.material.appbar.MaterialToolbar.MaterialToolbar(android.content.Context, android.util.AttributeSet, int)
// Ensure we are using the correctly themed context rather than the context that was passed in.
// 避免materialThemeOverlay属性覆盖的配置失效
context = getContext()
val ta =
getContext().obtainStyledAttributes(attrs, R.styleable.TPWaveBallProgressView, defStyleAttr, defStyleRes)
_progress = ta.getFloat(R.styleable.TPWaveBallProgressIndicator_waveProgress, 0f)
_maxProgress = ta.getInteger(R.styleable.TPWaveBallProgressIndicator_waveMaxProgress, 100)
wave1ColorStart = ta.getColor(R.styleable.TPWaveBallProgressIndicator_wave1ColorStart, DEFAULT_COLOR_START)
wave1ColorEnd = ta.getColor(R.styleable.TPWaveBallProgressIndicator_wave1ColorEnd, DEFAULT_COLOR_END)
wave1Cycle = ta.getDimensionPixelOffset(R.styleable.TPWaveBallProgressIndicator_wave1Cycle, DEFAULT_CYCLE)
wave1Amplitude =
ta.getDimensionPixelOffset(R.styleable.TPWaveBallProgressIndicator_wave1Amplitude, DEFAULT_AMPLITUDE)
wave1Direction = ta.getInt(R.styleable.TPWaveBallProgressIndicator_wave1Direction, REVERSE)
wave2ColorStart = ta.getColor(R.styleable.TPWaveBallProgressIndicator_wave2ColorStart, DEFAULT_COLOR_START)
wave2ColorEnd = ta.getColor(R.styleable.TPWaveBallProgressIndicator_wave2ColorEnd, DEFAULT_COLOR_END)
wave2Cycle = ta.getDimensionPixelOffset(R.styleable.TPWaveBallProgressIndicator_wave2Cycle, DEFAULT_CYCLE)
wave2Amplitude =
ta.getDimensionPixelOffset(R.styleable.TPWaveBallProgressIndicator_wave2Amplitude, DEFAULT_AMPLITUDE)
wave2Direction = ta.getInt(R.styleable.TPWaveBallProgressIndicator_wave2Direction, FORWARD)
outerCircleColor = ta.getColor(R.styleable.TPWaveBallProgressIndicator_outerCircleColor, Color.WHITE)
ballRadius = ta.getDimensionPixelOffset(R.styleable.TPWaveBallProgressIndicator_ballRadius, 0)
ta.recycle()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
startWave()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
waveAnimator.cancel()
progressAnimator?.cancel()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val width = when {
MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY -> {
MeasureSpec.getSize(widthMeasureSpec)
}
minimumWidth > 0 -> {
minimumWidth
}
else -> {
DEFAULT_SIZE
}
}
val height = when {
MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY -> {
MeasureSpec.getSize(heightMeasureSpec)
}
minimumHeight > 0 -> {
minimumHeight
}
else -> {
DEFAULT_SIZE
}
}
val viewSize = min(width, height)
setMeasuredDimension(viewSize, viewSize)
containerWidth = width - paddingStart - paddingEnd
containerHeight = height - paddingTop - paddingBottom
containerStart = paddingStart
containerEnd = width - paddingEnd
containerTop = paddingTop
containerBottom = height - paddingBottom
outerCircleWidth = (containerWidth * 0.6).toInt()
outerCircleHeight = (containerHeight * 0.6).toInt()
val borderWidth = (containerWidth - outerCircleWidth) / 2
val borderHeight = (containerHeight - outerCircleHeight) / 2
outerCircleStart = containerStart + borderWidth
outerCircleEnd = containerEnd - borderWidth
outerCircleTop = containerTop + borderHeight
outerCircleBottom = containerBottom - borderHeight
innerCircleWidth = (outerCircleWidth * 0.8).toInt()
innerCircleHeight = (outerCircleHeight * 0.8).toInt()
val gapWidth = (outerCircleWidth - innerCircleWidth) / 2
val gapHeight = (outerCircleHeight - innerCircleHeight) / 2
innerCircleStart = outerCircleStart + gapWidth
innerCircleEnd = outerCircleEnd - gapWidth
innerCircleTop = outerCircleTop + gapHeight
innerCircleBottom = outerCircleBottom - gapHeight
innerCirclePath.reset()
innerCirclePath.addCircle(
(innerCircleStart + innerCircleWidth / 2).toFloat(),
(innerCircleTop + innerCircleHeight / 2).toFloat(),
(min(innerCircleWidth, innerCircleHeight) / 2).toFloat(),
Path.Direction.CCW
)
outerCirclePath.reset()
outerCirclePath.addCircle(
(outerCircleStart + outerCircleWidth / 2).toFloat(),
(outerCircleTop + outerCircleHeight / 2).toFloat(),
(min(outerCircleHeight, outerCircleWidth) / 2).toFloat(),
Path.Direction.CCW
)
setPaintShader()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val measuredHeight = measuredHeight
val measuredWidth = measuredWidth
if (measuredHeight < 0 || measuredWidth < 0) {
return
}
//步骤顺序不能变
drawBall(canvas)
drawOuterCircle(canvas)
drawWave(canvas, wave2Path, wave2Cycle, wave2Amplitude, 0, wave2Direction, wave2Paint)
drawWave(canvas, wave1Path, wave1Cycle, wave1Amplitude, 0, wave1Direction, wave1Paint)
}
fun setOnProgressChangeListener(l: OnProgressChangeListener?) {
onProgressChangeListener = l
}
private fun startWave() {
waveAnimator.cancel()
waveAnimator.start()
}
private fun setProgressInternal(progress: Float) {
var newProgress = progress
if (newProgress < 0) {
newProgress = 0f
}
if (newProgress > maxProgress) {
newProgress = maxProgress.toFloat()
}
if (newProgress == _progress) {
return
}
val lastProgress = _progress
progressAnimator?.cancel()
progressAnimator = ValueAnimator.ofFloat(lastProgress, newProgress)
progressAnimator?.duration = progressAnimatorDuration.toLong()
progressAnimator?.interpolator = LinearInterpolator()
progressAnimator?.addUpdateListener { valueAnimator: ValueAnimator ->
_progress = valueAnimator.animatedValue as Float
if (onProgressChangeListener != null) {
onProgressChangeListener!!.onProgressChange(this, _progress)
}
postInvalidate()
}
progressAnimator?.start()
}
private fun setPaintShader() {
wave1Paint.shader = LinearGradient(
innerCircleStart.toFloat(),
innerCircleTop.toFloat() + innerCircleHeight * (_progress / _maxProgress),
innerCircleEnd.toFloat(),
innerCircleBottom.toFloat(),
wave1ColorStart,
wave1ColorEnd,
Shader.TileMode.CLAMP
)
wave2Paint.shader = LinearGradient(
innerCircleStart.toFloat(),
innerCircleTop.toFloat(),
innerCircleEnd.toFloat(),
innerCircleBottom.toFloat(),
wave2ColorStart,
wave2ColorEnd,
Shader.TileMode.CLAMP
)
ballPaint.shader = LinearGradient(
innerCircleStart.toFloat(),
innerCircleTop.toFloat() + innerCircleHeight * (_progress / _maxProgress),
innerCircleEnd.toFloat(),
innerCircleBottom.toFloat(),
wave1ColorStart,
wave1ColorEnd,
Shader.TileMode.CLAMP
)
}
private fun drawBall(canvas: Canvas) {
canvas.save()
canvas.rotate(
-ballRotateAngle, (containerStart + containerWidth / 2).toFloat(),
(containerTop + containerHeight / 2).toFloat()
)
val ballTrackRadius = min(containerWidth / 2f, containerHeight / 2f)
val cx: Float = containerStart + containerWidth / 2f
val cy: Float = containerTop + ballRadius + containerHeight / 2f - ballTrackRadius
canvas.drawCircle(cx, cy, ballRadius.toFloat(), ballPaint)
canvas.restore()
}
private fun drawOuterCircle(canvas: Canvas) {
canvas.drawPath(outerCirclePath, outerCirclePaint)
}
private fun drawWave(
canvas: Canvas,
path: Path,
cycle: Int,
amplitude: Int,
offsetY: Int,
@WaveDirection direction: Int,
paint: Paint
) {
var offset =
if (direction == REVERSE) (cycle * offsetFraction).toInt() else (cycle * (1 - offsetFraction)).toInt()
val halfCycle = cycle / 2
var amplitudeValue = if (abs(offset) >= halfCycle) amplitude else amplitude.inv()
offset = if (abs(offset) >= halfCycle) offset - halfCycle else offset
var startX = innerCircleStart - offset
val startY = (innerCircleTop + innerCircleHeight * (1 - _progress / _maxProgress) + offsetY).toInt()
path.reset()
path.moveTo(startX.toFloat(), startY.toFloat())
var endX = startX + halfCycle
do {
path.quadTo(
(startX + halfCycle / 2).toFloat(),
(startY + amplitudeValue).toFloat(),
endX.toFloat(),
startY.toFloat()
)
amplitudeValue = amplitudeValue.inv()
startX = endX
endX += halfCycle
} while (startX < innerCircleEnd)
path.lineTo(innerCircleEnd.toFloat(), innerCircleBottom.toFloat())
path.lineTo(innerCircleStart.toFloat(), innerCircleBottom.toFloat())
path.lineTo(innerCircleStart.toFloat(), startY.toFloat())
canvas.clipPath(innerCirclePath)
canvas.drawPath(path, paint)
}
@IntDef(FORWARD, REVERSE)
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
internal annotation class WaveDirection
interface OnProgressChangeListener {
fun onProgressChange(view: TPWaveBallProgressView, progress: Float)
}
}
最新发布