源码
class LineView : View {
private var mContext: Context
//坐标系画笔
private lateinit var xyPaint: Paint
//坐标文字画笔
private lateinit var textPaint: Paint
private var mViewWidth = 0
private var mViewHeight = 0
private lateinit var mShader: LinearGradient
//坐标轴上下左右距离边界的距离
private var margin = 100f
//y轴坐标初始值
private val yNumber = mutableListOf<String>()
//x轴坐标值
private val xNumber = mutableListOf<String>()
//阴影路径
private var mShaderPath = Path()
//阴影画笔
private lateinit var shaderPaint: Paint
//最大Y轴值
var maxY = 0
//数据集合
private val mDatas = mutableListOf<EverydayBean.DayBean>()
//坐标点和折线画笔
private lateinit var linePaint: Paint
//点击marker选中的坐标点位置
private var selectPoint = 0
//marker画笔
private lateinit var markerPaint: Paint
//marker透明背景画笔
private lateinit var tmPaint: Paint
//marker文字画笔
private lateinit var markerTextPaint: Paint
//虚线画笔
private lateinit var dottedPaint: Paint
//是否点击了
private var isClick = false
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
mContext = context
//初始化画笔
initPaint()
}
private fun initPaint() {
xyPaint = Paint()
xyPaint.style = Paint.Style.FILL
xyPaint.isAntiAlias = true
xyPaint.strokeWidth = DisplayUtils.dp2px(mContext, 1f).toFloat()
xyPaint.color = ContextCompat.getColor(mContext, R.color.black_333)
textPaint = Paint()
textPaint.textAlign = Paint.Align.CENTER
textPaint.isAntiAlias = true
textPaint.color = ContextCompat.getColor(mContext, R.color.black_333)
textPaint.textSize = 30f
linePaint = Paint()
linePaint.textAlign = Paint.Align.CENTER
linePaint.isAntiAlias = true
linePaint.color = ContextCompat.getColor(mContext, R.color.blue_007)
linePaint.strokeWidth = DisplayUtils.dp2px(mContext, 1f).toFloat()
shaderPaint = Paint()
textPaint.isAntiAlias = true
markerPaint = Paint()
markerPaint.textAlign = Paint.Align.CENTER
markerPaint.style = Paint.Style.STROKE
markerPaint.isAntiAlias = true
markerPaint.color = ContextCompat.getColor(mContext, R.color.gray_ddd)
markerPaint.strokeWidth = DisplayUtils.dp2px(mContext, 1f).toFloat()
markerPaint.textSize = DisplayUtils.sp2px(mContext, 10f).toFloat()
markerTextPaint = Paint()
markerTextPaint.textAlign = Paint.Align.CENTER
markerTextPaint.isAntiAlias = true
markerTextPaint.color = ContextCompat.getColor(mContext, R.color.black_333)
markerTextPaint.strokeWidth = DisplayUtils.dp2px(mContext, 1f).toFloat()
markerTextPaint.textSize = DisplayUtils.sp2px(mContext, 10f).toFloat()
tmPaint = Paint()
tmPaint.textAlign = Paint.Align.CENTER
tmPaint.style = Paint.Style.FILL
tmPaint.isAntiAlias = true
tmPaint.color = Color.parseColor("#ccffffff")
dottedPaint = Paint()
dottedPaint.isAntiAlias = true
dottedPaint.pathEffect = DashPathEffect(floatArrayOf(4f, 4f), 0f)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
setMeasuredDimension(measureSpec(widthMeasureSpec), measureSpec(heightMeasureSpec))
}
private fun measureSpec(heightMeasureSpec: Int): Int {
var result: Int
val specSize = View.MeasureSpec.getSize(heightMeasureSpec) //获取高的高度 单位 为px
val specMode = View.MeasureSpec.getMode(heightMeasureSpec)//获取测量的模式
//如果是精确测量,就将获取View的大小设置给将要返回的测量值
if (specMode == View.MeasureSpec.EXACTLY) {
result = specSize
} else {
result = 400
//如果设置成wrap_content时,给高度指定一个值
if (specMode == View.MeasureSpec.AT_MOST) {
result = Math.min(result, specSize)
}
}
return result
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
//获取当前View的宽高
mViewWidth = w
mViewHeight = h
//渐变
mShader = LinearGradient(
mViewWidth.toFloat(),
mViewHeight.toFloat(),
mViewWidth.toFloat(),
0f,
intArrayOf(Color.YELLOW, Color.TRANSPARENT),
null,
Shader.TileMode.REPEAT
)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (xNumber.size == 0 || yNumber.size == 0) {
return
}
//绘制坐标点
drawPoint(canvas)
//绘制圆点 坐标轴 坐标轴值
drawXY(canvas)
//绘制marker
if (isClick) {
drawMarker(canvas)
}
}
private fun drawXY(canvas: Canvas) {
//绘制圆点
canvas.drawCircle(margin, (mViewHeight - margin), 6f, xyPaint)
//绘制x轴
canvas.drawLine(
margin,
(mViewHeight - margin),
(mViewWidth - margin),
(mViewHeight - margin),
xyPaint
)
//绘制y轴
canvas.drawLine(margin, mViewHeight - margin, margin, margin, xyPaint)
//绘制x轴上坐标点
//每个点的间隔 和文字
var xSpace = (mViewWidth - margin * 2) / 29
var mXBound = Rect()
var mYBound = Rect()
for (i in 0..29) {
if (i == 0 || i == 15 || i == 29) {
canvas.drawCircle(margin + xSpace * i, (mViewHeight - margin), 6f, xyPaint)
val xText = xNumber[i]
textPaint.getTextBounds(xText, 0, xText.length, mXBound)
canvas.drawText(
xText,
margin + xSpace * i,
mViewHeight - margin + 2 * mXBound.height(),
textPaint
)
}
}
//绘制y轴上坐标点 和文字
var ySpace = (mViewHeight - margin * 2) / 5
for (i in 0..5) {
canvas.drawCircle(margin, (mViewHeight - margin - ySpace * i), 6f, xyPaint)
canvas.drawLine(
margin,
(mViewHeight - margin - ySpace * i),
mViewWidth - margin ,
(mViewHeight - margin - ySpace * i),
dottedPaint
)
var yText = yNumber[i]
textPaint.getTextBounds(yText, 0, yText.length, mYBound)
canvas.drawText(
yText,
margin - mYBound.width() / 2 - 10,
(mViewHeight - margin - ySpace * i + mYBound.height() / 2),
textPaint
)
}
canvas.drawText(
"人数",
margin - mYBound.width() / 2 - 10,
margin / 2,
textPaint
)
}
private fun drawPoint(canvas: Canvas) {
var xSpace = (mViewWidth - margin * 2) / 29
var ySpace = (mViewHeight - margin * 2) / maxY
mShaderPath.moveTo(
margin,
(mViewHeight - margin)
)
var startX = 0F
var startY = 0F
var endX = 0F
var endY = 0F
//画折线
for (i in 0..29) {
if (startY == 0F) {
startX = margin + xSpace * i
startY = mViewHeight - mDatas[i].tote_count * ySpace - margin
} else {
startX = endX
startY = endY
}
if (i != 29) {
endX = margin + xSpace * (i + 1)
endY = mViewHeight - mDatas[i + 1].tote_count * ySpace - margin
canvas.drawCircle(startX, startY, 3f, linePaint)
canvas.drawCircle(endX, endY, 3f, linePaint)
//绘制线
// Log.e("TAG", "$startX-----$startY------$endX-----$endY")
canvas.drawLine(startX, startY, endX, endY, linePaint)
} else {
canvas.drawCircle(startX, startY, 3f, linePaint)
}
mShaderPath.lineTo(startX, startY)
if (i == 29) {
mShaderPath.lineTo(endX, endY)
mShaderPath.lineTo(endX, mViewHeight - margin)
val shader = LinearGradient(
mViewWidth.toFloat(),
mViewHeight.toFloat(),
mViewWidth.toFloat(),
0f,
intArrayOf(Color.YELLOW, Color.TRANSPARENT),
null,
Shader.TileMode.REPEAT
)
shaderPaint.shader = shader
canvas.drawPath(mShaderPath, shaderPaint)
}
}
startX = 0F
startY = 0F
endX = 0F
endY = 0F
for (i in 0..29) {
if (startY == 0F) {
startX = margin + xSpace * i
startY = mViewHeight - mDatas[i].tote_count * ySpace - margin
} else {
startX = endX
startY = endY
}
if (i != 29) {
endX = margin + xSpace * (i + 1)
endY = mViewHeight - mDatas[i + 1].tote_count * ySpace - margin
//绘制点
canvas.drawCircle(startX, startY, 3f, linePaint)
canvas.drawCircle(endX, endY, 3f, linePaint)
} else {
canvas.drawCircle(startX, startY, 3f, linePaint)
}
}
}
private fun drawMarker(canvas: Canvas) {
var xSpace = (mViewWidth - margin * 2) / 29
var ySpace = (mViewHeight - margin * 2) / 5
//1.绘制虚线
canvas.drawLine(
margin + xSpace * selectPoint,
mViewHeight - margin,
margin + xSpace * selectPoint,
margin,
dottedPaint
)
//2.先绘制内容边框,如果虚线距离y轴的距离小于内容框的大小,内容框位置不变
//如果大于内容框的距离,内容框跟着虚线左右移动
var startX = if (margin + xSpace * selectPoint > 2 * margin + 2 * ySpace) {
2 * margin + margin + xSpace * selectPoint - 2 * (margin + ySpace) - dpToPx(10)
} else {
2 * margin
}
var startY = margin + ySpace
var tmF = RectF(startX, margin, startX + 2 * ySpace, startY)
canvas.drawRoundRect(tmF, 10f, 10f, tmPaint)
var rectF = RectF(startX, margin, startX + 2 * ySpace, startY)
canvas.drawRoundRect(rectF, 10f, 10f, markerPaint)
//3.绘制日期
var timeRect = Rect()
var timeText = mDatas[selectPoint].date_time
markerPaint.getTextBounds(timeText, 0, timeText.length, timeRect)
canvas.drawText(
timeText,
(startX + ySpace),
margin + ySpace / 2 - timeRect.height() / 2,
markerTextPaint
)
//4.绘制图例和数据
var numRect = Rect()
var numText = "进场:${mDatas[selectPoint].tote_count}人"
markerPaint.getTextBounds(numText, 0, numText.length, numRect)
canvas.drawText(
numText,
(startX + ySpace),
margin + ySpace - numRect.height() / 2,
markerTextPaint
)
//图例
canvas.drawCircle(
(startX + ySpace - timeRect.width() / 2),
margin + ySpace - ySpace / 4,
3f,
linePaint
)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_UP -> {
//点击事件
clickAction(event)
}
}
return true
}
/**
* 根据点击位置,计算出离点击位置最近的点
* 显示该点的详细信息
*/
private fun clickAction(event: MotionEvent) {
isClick = true
var eventX = event.x
var eventY = event.y
var xSpace = (mViewWidth - margin * 2) / 30
var index = 0
for (i in 0..29) {
if (i * xSpace >= (eventX - margin)) {
index = i
break
}
}
selectPoint =
if ((index * xSpace - (eventX - margin) > ((eventX - margin) - ((index - 1) * xSpace)))) {
index - 1
} else {
index
}
//超出点击范围隐藏弹窗
if (eventY < margin || eventY > mViewHeight - margin || eventX < margin || eventX > mViewWidth - margin) {
isClick = false
}
invalidate()
}
/**
* 当最大人数小于100时,最大值设置成100
* 当最大值大于200时,最大值最高为+1,然后拼接最大值位数减1个领,拼接
* 后的数当最大值
* Y轴坐标始终为5个
*/
fun setData(datas: MutableList<EverydayBean.DayBean>) {
mDatas.clear()
mDatas.addAll(datas)
mDatas.reverse()
datas.forEachIndexed { index, dayBean ->
// if (index == 0 || index == 14 || index == 29) {
xNumber.add(dayBean.date_time)
// }
if (dayBean.tote_count > maxY) {
maxY = dayBean.tote_count
}
}
xNumber.reverse()
if (maxY == 0 || maxY <= 100) {
yNumber.add("0")
yNumber.add("20")
yNumber.add("40")
yNumber.add("60")
yNumber.add("80")
yNumber.add("100")
maxY = 100
} else {
var maxStr = maxY.toString()
var num = maxStr.substring(0, 1).toInt()
Log.e("TAG", "max---->$maxY----->$num")
val sb = StringBuffer()
sb.append("${num + 1}")
for (i in maxStr.indices - 1) {
sb.append("0")
}
maxY = sb.toString().toInt()
yNumber.add("0")
yNumber.add("${maxY / 5}")
yNumber.add("${maxY * 2 / 5}")
yNumber.add("${maxY * 3 / 5}")
yNumber.add("${maxY * 4 / 5}")
yNumber.add("$maxY")
}
invalidate()
}
private fun dpToPx(dp: Int): Int {
var density = mContext.resources.displayMetrics.density
return (dp * density + 0.5f * if (dp >= 0) 1 else -1).toInt()
}
}
bean 类
class DayBean(
val date_time: String,
val tote_count: Int
)