/**
*
* 日图表
* zrj 2020/9/1
*/
class BODayChart(context: Context, attrs: AttributeSet?) : View(context, attrs) {
//屏幕宽高
private var scrWidth = 0f
private var scrHeight = 0f
private var xData: Array<String> = arrayOf("00:00", "06:00", "12:00", "18:00", "00:00")
private var yData: Array<Int> = arrayOf(100, 90, 85, 80, 70)
private var boData = mutableListOf<Int>()
private lateinit var paintLine: Paint
private lateinit var paintGradientLine: Paint
private lateinit var paintXText: Paint
private lateinit var paintYText: Paint
private lateinit var paintPillar: Paint
private lateinit var paintRound: Paint
private lateinit var paintBessel: Paint
private var animDuration = 500L
private var anim: ValueAnimator? = null
private var mPercent = 0f //动画进度
private var xSlider = 0f //滑块的x轴位置
private var mPath: Path
private val curveCircleRadius = 12f.dp
// the coordinates of the first curve
private val mFirstCurveStartPoint = Point()
private val mFirstCurveEndPoint = Point()
private val mFirstCurveControlPoint1 = Point()
private val mFirstCurveControlPoint2 = Point()
//the coordinates of the second curve
private var mSecondCurveStartPoint = Point()
private val mSecondCurveEndPoint = Point()
private val mSecondCurveControlPoint1 = Point()
private val mSecondCurveControlPoint2 = Point()
init {
setLayerType(LAYER_TYPE_SOFTWARE, null)
mPath = Path()
initPaint()
}
/**
* 初始化画笔
*/
private fun initPaint() {
paintLine = Paint()
paintLine.style = Paint.Style.STROKE
paintLine.strokeWidth = 1f
paintLine.color = context.colorCompat(R.color.e6e6e6_2e2e2e)
paintGradientLine = Paint()
paintGradientLine.style = Paint.Style.STROKE
paintGradientLine.strokeWidth = 2f
paintXText = Paint()
paintXText.isAntiAlias = true
paintXText.strokeWidth = 1f
paintXText.textSize = 12f.sp
paintXText.textAlign = Paint.Align.CENTER
paintXText.color = context.colorCompat(R.color.color_on_surface)
paintYText = Paint()
paintYText.isAntiAlias = true
paintYText.textSize = 12f.sp
paintYText.strokeWidth = 1f
paintYText.textAlign = Paint.Align.RIGHT
paintYText.color = context.colorCompat(R.color.secondary_666666_808080)
paintPillar = Paint()
paintPillar.style = Paint.Style.FILL
paintPillar.isAntiAlias = true
paintPillar.color = context.colorCompat(R.color.fc355c_fc3159)
paintRound = Paint()
paintRound.style = Paint.Style.FILL
paintRound.isAntiAlias = true
paintRound.color = context.colorCompat(R.color.ffffff_6e6e6e)
paintBessel = Paint()
paintBessel.style = Paint.Style.FILL
paintBessel.isAntiAlias = true
paintBessel.color = context.colorCompat(R.color.f2f2f2_1d1d1d)
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
parent.requestDisallowInterceptTouchEvent(true)
return super.dispatchTouchEvent(ev)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
scrWidth = width.toFloat()
scrHeight = height.toFloat()
ySpacing = scrHeight / 10f //y轴分10份
//底部圆滑块可以滑动的范围
xWithStart = margin + paintXText.measureText(xData[0]) / 2
xWithEnd = scrWidth - margin - paintYText.measureText(yData[0].toString()) * 2.5f
xTextSpacing = (xWithEnd - xWithStart) / (xData.size - 1)
xSpacing = xTextSpacing / 36 //x轴等分4份 144
}
private var mDownX = 0f
private var mDownY = 0f
private var isSlider = false
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
mDownX = event.x
mDownY = event.y
isSlider = abs(event.x - xSlider) < 60f && abs(event.y - ySpacing * 9) < 60f
}
MotionEvent.ACTION_MOVE ->
if (abs(event.y - mDownY) < abs(event.x - mDownX)) {
if (isSlider) {
xSlider = event.x
invalidate()
}
}
MotionEvent.ACTION_UP -> {
if (isSlider) {
if (xSlider < xWithStart) {
xSlider = xWithStart
invalidate()
}
if (xSlider > xWithEnd) {
xSlider = xWithEnd
invalidate()
}
boData.forEachIndexed { index, _ ->
val x = xWithStart + xSpacing * index
val dis = abs(x - xSlider)
if (dis < xSpacing / 2) {
xSlider = x
invalidate()
return@forEachIndexed
}
}
} else {
if (abs(event.x - mDownX) > xSpacing) {
onDayMoveListener?.invoke(event.x > mDownX)
} else {
boData.forEachIndexed { index, _ ->
val x = xWithStart + xSpacing * index
val dis = abs(x - event.x)
if (dis < xSpacing) {
xSlider = x
invalidate()
return@forEachIndexed
}
}
}
}
}
}
return true
}
private val margin = 20f.dp //左右两边距离
private var xWithStart = 0f //x轴的起始点
private var xWithEnd = 0f //x轴结束点
private var ySpacing = 0f //高度分割份数后间距
private var xSpacing = 0f //x轴柱子分割份数后间距
private var xTextSpacing = 0f //x轴文字分割份数后间距
@SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//画y轴方向横线与文字
drawY(canvas)
//垂直渐变线
drawGradientLine(canvas)
//画柱子
drawPillar(canvas)
//底部
drawBessel(canvas)
//画x轴方向文字
drawX(canvas)
}
private fun drawX(canvas: Canvas) {
xData.forEachIndexed { index, s ->
val x = xWithStart + xTextSpacing * index
val dis = abs(x - xSlider)
var y = ySpacing * 9 - 10f
if (dis < xTextSpacing / 2) {
paintXText.typeface = Typeface.DEFAULT_BOLD
y -= 40f * (1 - dis / xTextSpacing)
} else {
paintXText.typeface = Typeface.DEFAULT
}
canvas.drawText(s, x, y, paintXText)
}
}
private fun drawPillar(canvas: Canvas) {
boData.forEachIndexed { index, i ->
if (xSlider < xWithStart + xSpacing * index + xSpacing / 2 && xSlider > xWithStart + xSpacing * index - xSpacing / 2) {
onDaySelectListener?.invoke(index, i)
}
if (i > 89) {
paintPillar.color = Color.parseColor("#00d656")
} else {
paintPillar.color = Color.parseColor("#fdc221")
}
if (i > 0) {
canvas.drawRoundRect(
RectF(
xWithStart + xSpacing * index - xSpacing / 2,
ySpacing + ySpacing * ((100 - i) / 5f) * mPercent,
xWithStart + xSpacing * index + xSpacing / 2,
ySpacing * 7
), 2f, 2f, paintPillar
)
}
}
}
private fun drawY(canvas: Canvas) {
var k = 1
for (i in 0..4) {
if (i > 0) {
k = i + 2
}
if (i == 4) {
k = 7
}
canvas.drawLine(
margin,
ySpacing * k,
scrWidth - margin,
ySpacing * k,
paintLine
)
canvas.drawText(
"${yData[i]}%",
scrWidth - margin,
ySpacing * k - 10f,
paintYText
)
}
}
private fun drawBessel(canvas: Canvas) {
// 第一条曲线开始点
mFirstCurveStartPoint[(xSlider - curveCircleRadius * 3).toInt()] = (ySpacing * 9).toInt()
// 第一条曲线结束点
mFirstCurveEndPoint[xSlider.toInt()] =
(ySpacing * 9 - curveCircleRadius - curveCircleRadius / 4).toInt()
// 第二条开始点
mSecondCurveStartPoint = mFirstCurveEndPoint
mSecondCurveEndPoint[(xSlider + curveCircleRadius * 3).toInt()] = (ySpacing * 9).toInt()
// 第一条控制点
mFirstCurveControlPoint1[(mFirstCurveStartPoint.x + curveCircleRadius + curveCircleRadius / 4).toInt()] =
mFirstCurveStartPoint.y
mFirstCurveControlPoint2[(mFirstCurveEndPoint.x - curveCircleRadius * 2 + curveCircleRadius).toInt()] =
mFirstCurveEndPoint.y
// 第二条控制点
mSecondCurveControlPoint1[(mSecondCurveStartPoint.x + curveCircleRadius * 2 - curveCircleRadius).toInt()] =
mSecondCurveStartPoint.y
mSecondCurveControlPoint2[(mSecondCurveEndPoint.x - curveCircleRadius - curveCircleRadius / 4).toInt()] =
mSecondCurveEndPoint.y
mPath.reset()
mPath.moveTo(0f, ySpacing * 9)
mPath.lineTo(mFirstCurveStartPoint.x.toFloat(), mFirstCurveStartPoint.y.toFloat())
mPath.cubicTo(
mFirstCurveControlPoint1.x.toFloat(), mFirstCurveControlPoint1.y.toFloat(),
mFirstCurveControlPoint2.x.toFloat(), mFirstCurveControlPoint2.y.toFloat(),
mFirstCurveEndPoint.x.toFloat(), mFirstCurveEndPoint.y.toFloat()
)
mPath.cubicTo(
mSecondCurveControlPoint1.x.toFloat(), mSecondCurveControlPoint1.y.toFloat(),
mSecondCurveControlPoint2.x.toFloat(), mSecondCurveControlPoint2.y.toFloat(),
mSecondCurveEndPoint.x.toFloat(), mSecondCurveEndPoint.y.toFloat()
)
mPath.lineTo(scrWidth, ySpacing * 9)
mPath.lineTo(scrWidth, scrHeight)
mPath.lineTo(0f, scrHeight)
mPath.close()
//底部灰色
canvas.drawPath(mPath, paintBessel)
//底部滑块
canvas.drawCircle(xSlider, ySpacing * 9 + 5f, curveCircleRadius, paintRound)
}
fun setValue(value: MutableList<Int>, time: Int): BODayChart {
boData.clear()
boData.addAll(value)
xSlider = xSpacing * time + xWithStart
startAnimation()
return this
}
private fun startAnimation() {
anim = ValueAnimator.ofObject(AngleEvaluator(), 0f, 1f)
anim?.interpolator = AccelerateDecelerateInterpolator()
anim?.addUpdateListener { animation ->
mPercent = animation.animatedValue as Float
postInvalidate()
}
anim?.duration = animDuration
anim?.start()
}
private fun drawGradientLine(canvas: Canvas) {
val mLinearGradient = LinearGradient(
xSlider, ySpacing, xSlider, ySpacing * 8,
intArrayOf(
context.colorCompat(R.color.ffffff_262626),
context.colorCompat(R.color.fc355c_fc3159),
context.colorCompat(R.color.ffffff_262626)
), null, Shader.TileMode.MIRROR
)
paintGradientLine.shader = mLinearGradient
if (ySpacing > 0) {
canvas.drawLine(xSlider, ySpacing, xSlider, ySpacing * 8, paintGradientLine)
}
}
private var onDaySelectListener: ((index: Int, value: Int) -> Unit)? = null
fun setOnDaySelectListener(l: ((index: Int, value: Int) -> Unit)): BODayChart {
this.onDaySelectListener = l
return this
}
private var onDayMoveListener: ((isPre: Boolean) -> Unit)? = null
fun setOnDayMoveListener(l: ((index: Boolean) -> Unit)): BODayChart {
this.onDayMoveListener = l
return this
}
}
转自https://juejin.cn/post/6944670773520891912?from=search-suggest