时间:2022-10-26 09:56:47 | 栏目:Android代码 | 点击:次
本文实例为大家分享了Android绘制双折线图的具体代码,供大家参考,具体内容如下
自定义View实现双折线图,可点击,点击后带标签描述,暂未实现拖动的功能,实现效果如下:
代码如下:
首先,自定义布局属性:
<declare-styleable name="LineChart"> <!--type2.LineChart(双折线图)--> <attr name="maxYValue" format="integer" /> <attr name="yLabelCount" format="integer" /> <attr name="xLabelTextSize" format="dimension" /> <attr name="xLabelTextColor" format="color" /> <attr name="xLabelTextMarginTop" format="dimension" /> <attr name="showYLabelText" format="boolean" /> <attr name="yLabelTextSize" format="dimension" /> <attr name="yLabelTextColor" format="color" /> <attr name="yLabelTextMarginLeft" format="dimension" /> <attr name="axisWidth" format="dimension" /> <attr name="axisColor" format="color" /> <attr name="showScale" format="boolean" /> <attr name="scaleLength" format="dimension" /> <attr name="showGrid" format="boolean" /> <attr name="gridWidth" format="dimension" /> <attr name="gridDashInterval" format="dimension" /> <attr name="gridDashLength" format="dimension" /> <attr name="gridColor" format="color" /> <attr name="lineWidth" format="dimension" /> <attr name="lineColor1" format="color" /> <attr name="lineColor2" format="color" /> <attr name="labelWidth" format="dimension" /> <attr name="labelHeight" format="dimension" /> <attr name="labelBackgroundColor" format="color" /> <attr name="labelRadius" format="dimension" /> <attr name="labelTextSize" format="dimension" /> <attr name="labelTextColor" format="color" /> <attr name="labelArrowWidth" format="dimension" /> <attr name="labelArrowHeight" format="dimension" /> <attr name="labelArrowOffset" format="dimension" /> <attr name="labelArrowMargin" format="dimension" /> <attr name="clickAble" format="boolean" /> <attr name="leftMargin" format="dimension" /> <attr name="topMargin" format="dimension" /> <attr name="rightMargin" format="dimension" /> <attr name="bottomMargin" format="dimension" /> </declare-styleable>
LineChart的实现如下:
class LineChart @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { companion object { private val DEFAULT_MAX_YVALUE = 5000 private val DEFAULT_YLABEL_COUNT = 4 private val DEFAULT_XLABEL_TEXT_SIZE = SizeUtil.sp2px(10f) private val DEFAULT_XLABEL_TEXT_COLOR = Color.parseColor("#999999") private val DEFAULT_XLABEL_TEXT_MARGIN_TOP = SizeUtil.dp2px(10f) private val DEFAULT_SHOW_YLABEL_TEXT = false private val DEFAULT_YLABEL_TEXT_SIZE = SizeUtil.sp2px(11f) private val DEFAULT_YLABEL_TEXT_COLOR = Color.BLACK private val DEFAULT_YLABEL_TEXT_MARGIN_LEFT = SizeUtil.dp2px(15f) private val DEFAULT_AXIS_WIDTH = SizeUtil.dp2px(0.5f) private val DEFAULT_AXIS_COLOR = Color.parseColor("#F1F1F1") private val DEFAULT_SHOW_SCALE = true private val DEFAULT_SCALE_LENGTH = SizeUtil.dp2px(4f) private val DEFAULT_SHOW_GRID = true private val DEFAULT_GRID_WIDTH = SizeUtil.dp2px(0.5f) private val DEFAULT_GRID_DASH_INTERVAL = SizeUtil.dp2px(1f) private val DEFAULT_GRID_DASH_LENGTH = SizeUtil.dp2px(2f) private val DEFAULT_GRID_COLOR = Color.parseColor("#F1F1F1") private val DEFAULT_LINE_WIDTH = SizeUtil.dp2px(1.5f) private val DEFAULT_LINE_COLOR1 = Color.parseColor("#60BF56") private val DEFAULT_LINE_COLOR2 = Color.parseColor("#108EE9") private val DEFAULT_LABEL_WIDTH = SizeUtil.dp2px(135f) private val DEFAULT_LABEL_HEIGHT = SizeUtil.dp2px(78f) private val DEFAULT_LABEL_BACKGROUND_COLOR = Color.WHITE private val DEFAULT_LABEL_RADIUS = SizeUtil.dp2px(3f) private val DEFAULT_LABEL_TEXT_SIZE = SizeUtil.sp2px(11f) private val DEFAULT_LABEL_TEXT_COLOR = Color.parseColor("#333333") private val DEFAULT_LABEL_ARROW_WIDTH = SizeUtil.dp2px(8f) private val DEFAULT_LABEL_ARROW_HEIGHT = SizeUtil.dp2px(2f) private val DEFAULT_LABEL_ARROW_OFFSET = SizeUtil.dp2px(31f) private val DEFAULT_LABEL_ARROW_MARGIN = SizeUtil.dp2px(14.5f) private val DEFAULT_CLICKABLE = true private val DEFAULT_LEFT_MARGIN = SizeUtil.dp2px(15f) private val DEFAULT_TOP_MARGIN = SizeUtil.dp2px(118f) private val DEFAULT_RIGHT_MARGIN = SizeUtil.dp2px(15f) private val DEFAULT_BOTTOM_MARGIN = SizeUtil.dp2px(70f) } //Y轴最大值 var maxYValue: Int = DEFAULT_MAX_YVALUE //Y轴上的刻度值个数 var yLabelCount: Int = DEFAULT_YLABEL_COUNT //X轴刻度值文本字体大小 var xLabelTextSize: Float = DEFAULT_XLABEL_TEXT_SIZE //X轴刻度值文本字体颜色 var xLabelTextColor: Int = DEFAULT_XLABEL_TEXT_COLOR //X轴刻度值文本到X轴的上边距 var xLabelTextMarginTop: Float = DEFAULT_XLABEL_TEXT_MARGIN_TOP //是否显示Y轴刻度值文本 var showYLabelText: Boolean = DEFAULT_SHOW_YLABEL_TEXT //Y轴刻度值文本字体大小 var yLabelTextSize: Float = DEFAULT_YLABEL_TEXT_SIZE //Y轴刻度值文本字体颜色 var yLabelTextColor: Int = DEFAULT_YLABEL_TEXT_COLOR //Y轴刻度值文本到屏幕左侧的左边距 var yLabelTextMarginLeft: Float = DEFAULT_YLABEL_TEXT_MARGIN_LEFT //X轴宽度 var axisWidth: Float = DEFAULT_AXIS_WIDTH //X轴颜色 var axisColor: Int = DEFAULT_AXIS_COLOR //是否显示轴线上的小刻度线,默认显示 var showScale: Boolean = DEFAULT_SHOW_SCALE //X轴上的小刻度线长度 var scaleLength: Float = DEFAULT_SCALE_LENGTH //是否显示网格,默认显示 var showGrid: Boolean = DEFAULT_SHOW_GRID //网格线宽度 var gridWidth: Float = DEFAULT_GRID_WIDTH //网格线组成虚线的线段之间的间隔 var gridDashInterval: Float = DEFAULT_GRID_DASH_INTERVAL //网格线组成虚线的线段长度 var gridDashLength: Float = DEFAULT_GRID_DASH_LENGTH //网格线颜色 var gridColor: Int = DEFAULT_GRID_COLOR //折线宽度 var lineWidth: Float = DEFAULT_LINE_WIDTH //折线一颜色 var lineColor1: Int = DEFAULT_LINE_COLOR1 //折线二颜色 var lineColor2: Int = DEFAULT_LINE_COLOR2 //标签的矩形宽度 var labelWidth: Float = DEFAULT_LABEL_WIDTH //标签的矩形高度 var labelHeight: Float = DEFAULT_LABEL_HEIGHT //标签背景颜色 var labelBackgroundColor = DEFAULT_LABEL_BACKGROUND_COLOR //标签的矩形圆角 var labelRadius: Float = DEFAULT_LABEL_RADIUS //标签内文本字体大小 var labelTextSize: Float = DEFAULT_LABEL_TEXT_SIZE //标签内文本字体颜色 var labelTextColor: Int = DEFAULT_LABEL_TEXT_COLOR //标签的箭头宽度 var labelArrowWidth: Float = DEFAULT_LABEL_ARROW_WIDTH //标签的箭头高度 var labelArrowHeight: Float = DEFAULT_LABEL_ARROW_HEIGHT //标签的箭头到标签左侧或右侧的偏移量 var labelArrowOffset: Float = DEFAULT_LABEL_ARROW_OFFSET //标签的箭头到坐标轴最上方的下边距 var labelArrowMargin: Float = DEFAULT_LABEL_ARROW_MARGIN //是否可点击 var clickAble: Boolean = DEFAULT_CLICKABLE //坐标轴到View左侧的边距,多出来的空间可以用来绘制Y轴刻度文本 var leftMargin: Float = DEFAULT_LEFT_MARGIN //坐标轴到View顶部的边距,多出来的空间可以用来绘制标签信息 var topMargin: Float = DEFAULT_TOP_MARGIN //坐标轴到View右侧的边距 var rightMargin: Float = DEFAULT_RIGHT_MARGIN //坐标轴到View底部的边距,多出来的空间可以用来绘制X轴刻度文本 var bottomMargin: Float = DEFAULT_BOTTOM_MARGIN private var mCurrentDrawIndex = 0 private lateinit var mAxisPaint: Paint //绘制轴线和轴线上的小刻度线 private lateinit var mGridPaint: Paint //绘制网格线 private lateinit var mLinePaint: Paint //绘制折线 private lateinit var mLabelPaint: Paint //绘制最上方标签 private lateinit var mLabelBgPaint: Paint //绘制标签背景,带阴影效果 private lateinit var mTextPaint: Paint //绘制文本 private lateinit var mLabelRectF: RectF //最上方的标签对应的矩形 private var mWidth: Int = 0 private var mHeight: Int = 0 private var mXPoint: Float = 0f //原点的X坐标 private var mYPoint: Float = 0f //原点的Y坐标 private var mXScale: Float = 0f //X轴刻度长度 private var mYScale: Float = 0f //Y轴刻度长度 private var mXLength: Float = 0f //X轴长度 private var mYLength: Float = 0f //Y轴长度 private var mClickIndex: Int = 0 //点击时的下标 private var mDataList1: MutableList<Float> = mutableListOf() //折线一(交易收益)对应数据 private var mDataList2: MutableList<Float> = mutableListOf() //折线二(返现收益)对应数据 //记录每个数据点的X、Y坐标 private var mDataPointList1: MutableList<PointF> = mutableListOf() private var mDataPointList2: MutableList<PointF> = mutableListOf() private var mXLabelList: MutableList<String> = mutableListOf() //X轴刻度值 private var mYLabelList: MutableList<String> = mutableListOf() //Y轴刻度值 init { setLayerType(LAYER_TYPE_SOFTWARE, null) //关闭硬件加速,解决在部分手机无法实现虚线效果 attrs?.let { parseAttribute(getContext(), it) } initPaint() setYLable() } //初始化Y轴刻度值 private fun setYLable() { mYLabelList.clear() val increment = maxYValue / yLabelCount.toFloat() for (i in 0..yLabelCount) { var text = "" if (i == 0) { text = "0" } else { val value = (increment * i * 100).toInt() / 100f if (value == value.toInt().toFloat()) { text = value.toInt().toString() } else { text = value.toString() } } mYLabelList.add(text) } } //获取布局属性并设置属性默认值 private fun parseAttribute(context: Context, attrs: AttributeSet) { val ta = context.obtainStyledAttributes(attrs, R.styleable.LineChart) maxYValue = ta.getInt(R.styleable.LineChart_maxYValue, DEFAULT_MAX_YVALUE) yLabelCount = ta.getInt(R.styleable.LineChart_yLabelCount, DEFAULT_YLABEL_COUNT) xLabelTextSize = ta.getDimension(R.styleable.LineChart_xLabelTextSize, DEFAULT_XLABEL_TEXT_SIZE) xLabelTextColor = ta.getColor(R.styleable.LineChart_xLabelTextColor, DEFAULT_XLABEL_TEXT_COLOR) xLabelTextMarginTop = ta.getDimension(R.styleable.LineChart_xLabelTextMarginTop, DEFAULT_XLABEL_TEXT_MARGIN_TOP) showYLabelText = ta.getBoolean(R.styleable.LineChart_showYLabelText, DEFAULT_SHOW_YLABEL_TEXT) yLabelTextSize = ta.getDimension(R.styleable.LineChart_yLabelTextSize, DEFAULT_YLABEL_TEXT_SIZE) yLabelTextColor = ta.getColor(R.styleable.LineChart_yLabelTextColor, DEFAULT_YLABEL_TEXT_COLOR) yLabelTextMarginLeft = ta.getDimension(R.styleable.LineChart_yLabelTextMarginLeft, DEFAULT_YLABEL_TEXT_MARGIN_LEFT) axisWidth = ta.getDimension(R.styleable.LineChart_axisWidth, DEFAULT_AXIS_WIDTH) axisColor = ta.getColor(R.styleable.LineChart_axisColor, DEFAULT_AXIS_COLOR) showScale = ta.getBoolean(R.styleable.LineChart_showScale, DEFAULT_SHOW_SCALE) scaleLength = ta.getDimension(R.styleable.LineChart_scaleLength, DEFAULT_SCALE_LENGTH) showGrid = ta.getBoolean(R.styleable.LineChart_showGrid, DEFAULT_SHOW_GRID) gridWidth = ta.getDimension(R.styleable.LineChart_gridWidth, DEFAULT_GRID_WIDTH) gridDashInterval = ta.getDimension(R.styleable.LineChart_gridDashInterval, DEFAULT_GRID_DASH_INTERVAL) gridDashLength = ta.getDimension(R.styleable.LineChart_gridDashLength, DEFAULT_GRID_DASH_LENGTH) gridColor = ta.getColor(R.styleable.LineChart_gridColor, DEFAULT_GRID_COLOR) lineWidth = ta.getDimension(R.styleable.LineChart_lineWidth, DEFAULT_LINE_WIDTH) lineColor1 = ta.getColor(R.styleable.LineChart_lineColor1, DEFAULT_LINE_COLOR1) lineColor2 = ta.getColor(R.styleable.LineChart_lineColor2, DEFAULT_LINE_COLOR2) labelWidth = ta.getDimension(R.styleable.LineChart_labelWidth, DEFAULT_LABEL_WIDTH) labelHeight = ta.getDimension(R.styleable.LineChart_labelHeight, DEFAULT_LABEL_HEIGHT) labelBackgroundColor = ta.getColor(R.styleable.LineChart_labelBackgroundColor, DEFAULT_LABEL_BACKGROUND_COLOR) labelRadius = ta.getDimension(R.styleable.LineChart_labelRadius, DEFAULT_LABEL_RADIUS) labelTextSize = ta.getDimension(R.styleable.LineChart_labelTextSize, DEFAULT_LABEL_TEXT_SIZE) labelTextColor = ta.getColor(R.styleable.LineChart_labelTextColor, DEFAULT_LABEL_TEXT_COLOR) labelArrowWidth = ta.getDimension(R.styleable.LineChart_labelArrowWidth, DEFAULT_LABEL_ARROW_WIDTH) labelArrowHeight = ta.getDimension(R.styleable.LineChart_labelArrowHeight, DEFAULT_LABEL_ARROW_HEIGHT) labelArrowOffset = ta.getDimension(R.styleable.LineChart_labelArrowMargin, DEFAULT_LABEL_ARROW_OFFSET) labelArrowMargin = ta.getDimension(R.styleable.LineChart_labelArrowMargin, DEFAULT_LABEL_ARROW_MARGIN) clickAble = ta.getBoolean(R.styleable.LineChart_clickAble, DEFAULT_CLICKABLE) leftMargin = ta.getDimension(R.styleable.LineChart_leftMargin, DEFAULT_LEFT_MARGIN) topMargin = ta.getDimension(R.styleable.LineChart_topMargin, DEFAULT_TOP_MARGIN) rightMargin = ta.getDimension(R.styleable.LineChart_rightMargin, DEFAULT_RIGHT_MARGIN) bottomMargin = ta.getDimension(R.styleable.LineChart_bottomMargin, DEFAULT_BOTTOM_MARGIN) ta.recycle() } //初始化画笔 private fun initPaint() { mAxisPaint = Paint() with(mAxisPaint) { isAntiAlias = true color = axisColor strokeWidth = axisWidth } mGridPaint = Paint() with(mGridPaint) { isAntiAlias = true color = gridColor strokeWidth = gridWidth setPathEffect(DashPathEffect(floatArrayOf(gridDashLength, gridDashInterval), 0f)) //设置虚线效果 } mLinePaint = Paint() with(mLinePaint) { isAntiAlias = true strokeWidth = lineWidth style = Paint.Style.STROKE } mLabelPaint = Paint() with(mLabelPaint) { isAntiAlias = true } mLabelBgPaint = Paint() with(mLabelBgPaint) { isAntiAlias = true color = labelBackgroundColor } mTextPaint = Paint() with(mTextPaint) { isAntiAlias = true textAlign = Paint.Align.CENTER } mLabelRectF = RectF() } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) val heightMode = MeasureSpec.getMode(heightMeasureSpec) val heightSize = MeasureSpec.getSize(heightMeasureSpec) var height = 0 if (heightMode == MeasureSpec.EXACTLY) { height = heightSize } else { height = SizeUtil.dp2px(308f).toInt() if (heightMode == MeasureSpec.AT_MOST) { height = Math.min(height, heightSize) } } setMeasuredDimension(measuredWidth, height) } override fun onTouchEvent(event: MotionEvent?): Boolean { val touchX = event?.getX() ?: 0f for (i in 0..mDataPointList1.size - 1) { val centerX = mDataPointList1[i].x var beginX = centerX - mXScale / 2f var endX = centerX + mXScale / 2f if (i == 0) { beginX = 0f } if (i == mDataPointList1.size - 1) { endX = mWidth.toFloat() } if (beginX < touchX && touchX < endX) { mClickIndex = i invalidate() break } } return true } override fun onDraw(canvas: Canvas?) { canvas?.let { initSize(width, height) //初始化尺寸信息 drawCoordinate(it) //绘制坐标轴 drawLine(it) //绘制折线 drawLabel(it) //绘制点击后的效果 drawBottomDescription(it) //绘制底部类型说明 } } //初始化尺寸信息 private fun initSize(width: Int, height: Int) { mWidth = width mHeight = height mXLength = mWidth - leftMargin - rightMargin mYLength = mHeight - topMargin - bottomMargin mXPoint = leftMargin mYPoint = mHeight - bottomMargin mXScale = mXLength / (mXLabelList.size - 1) mYScale = mYLength / yLabelCount mDataPointList1.clear() if (hasOnlyOneData()) { mDataPointList1.add(PointF(mXPoint + mXLength / 2f, calculateYPosition(mDataList1.get(0)))) //居中 } else { for (i in 0..mDataList1.size - 1) { mDataPointList1.add(PointF(mXPoint + i * mXScale, calculateYPosition(mDataList1.get(i)))) } } mDataPointList2.clear() if (hasOnlyOneData()) { mDataPointList2.add(PointF(mXPoint + mXLength / 2f, calculateYPosition(mDataList2.get(0)))) //居中 } else { for (i in 0..mDataList2.size - 1) { mDataPointList2.add(PointF(mXPoint + i * mXScale, calculateYPosition(mDataList2.get(i)))) } } } //绘制坐标轴 private fun drawCoordinate(canvas: Canvas) { //绘制X轴 canvas.drawLine(mXPoint - axisWidth / 2f, mYPoint, mXPoint + mXLength + axisWidth / 2f, mYPoint, mAxisPaint) with(mTextPaint) { textSize = xLabelTextSize color = xLabelTextColor } val fm = mTextPaint.getFontMetrics() val yOffset = mYPoint + xLabelTextMarginTop - fm.ascent for (i in 0..mXLabelList.size - 1) { //绘制X轴的刻度值文本 if (i == 0) { //第一个刻度值文本 if (hasOnlyOneData()) { //只有一条数据时居中显示 mTextPaint.textAlign = Paint.Align.CENTER canvas.drawText(mXLabelList[i], mDataPointList1[i].x, yOffset, mTextPaint) } else { mTextPaint.textAlign = Paint.Align.LEFT canvas.drawText(mXLabelList[i], mXPoint, yOffset, mTextPaint) } } else if (i == mXLabelList.size - 1) { //最后一个刻度值文本 mTextPaint.textAlign = Paint.Align.RIGHT canvas.drawText(mXLabelList[i], mXPoint + mXLength, yOffset, mTextPaint) } else { mTextPaint.textAlign = Paint.Align.CENTER canvas.drawText(mXLabelList[i], mXPoint + i * mXScale, yOffset, mTextPaint) } //绘制X轴上的小刻度线 if (showScale) { canvas.drawLine( mXPoint + i * mXScale, mYPoint, mXPoint + i * mXScale, mYPoint - scaleLength, mAxisPaint ) } } for (i in 0..yLabelCount - 1) { //绘制网格线:横刻线 if (showGrid) { mGridPaint.color = gridColor canvas.drawLine( mXPoint, mYPoint - (i + 1) * mYScale, mXPoint + mXLength, mYPoint - (i + 1) * mYScale, mGridPaint ) } //绘制Y轴上的刻度值 if (showYLabelText) { with(mTextPaint) { textSize = yLabelTextSize color = yLabelTextColor textAlign = Paint.Align.LEFT } if (i == 0) { canvas.drawText(mYLabelList[i], yLabelTextMarginLeft, mYPoint, mTextPaint) } val yLabelFm = mTextPaint.getFontMetrics() val yLabelYOffset = mYPoint + (yLabelFm.descent - yLabelFm.ascent) / 2f - yLabelFm.descent - (i + 1) * mYScale canvas.drawText(mYLabelList[i + 1], yLabelTextMarginLeft, yLabelYOffset, mTextPaint) } } } //绘制折线 private fun drawLine(canvas: Canvas) { if (mDataList1 == null || mDataList1.size <= 0 || mDataList2 == null || mDataList2.size <= 0) { return } if (hasOnlyOneData()) { //处理只有一条数据的情况 //绘制第一条直线 mLinePaint.color = lineColor1 canvas.drawLine(mXPoint, mDataPointList1[0].y, mXPoint + mXLength, mDataPointList1[0].y, mLinePaint) //绘制第二条直线 mLinePaint.color = lineColor2 canvas.drawLine(mXPoint, mDataPointList2[0].y, mXPoint + mXLength, mDataPointList2[0].y, mLinePaint) return } for (i in 0..mDataPointList1.size - 2) { if (i <= mCurrentDrawIndex) { //绘制第一条折线 //绘制折线 mLinePaint.color = lineColor1 canvas.drawLine( mDataPointList1[i].x, mDataPointList1[i].y, mDataPointList1[i + 1].x, mDataPointList1[i + 1].y, mLinePaint ) //绘制折线交点 canvas.drawCircle(mDataPointList1[i].x, mDataPointList1[i].y, lineWidth * 1.5f, mLinePaint) mLinePaint.color = Color.WHITE canvas.drawCircle(mDataPointList1[i].x, mDataPointList1[i].y, lineWidth * 0.5f, mLinePaint) //绘制第二条折线 //绘制折线 mLinePaint.color = lineColor2 canvas.drawLine( mDataPointList2[i].x, mDataPointList2[i].y, mDataPointList2[i + 1].x, mDataPointList2[i + 1].y, mLinePaint ) //绘制折线交点 canvas.drawCircle(mDataPointList2[i].x, mDataPointList2[i].y, lineWidth * 1.5f, mLinePaint) mLinePaint.color = Color.WHITE canvas.drawCircle(mDataPointList2[i].x, mDataPointList2[i].y, lineWidth * 0.5f, mLinePaint) //绘制最后一个折线交点 if (i == mDataPointList1.size - 2) { mLinePaint.color = lineColor1 canvas.drawCircle( mDataPointList1[mDataPointList1.size - 1].x, mDataPointList1[mDataPointList1.size - 1].y, lineWidth * 1.5f, mLinePaint ) mLinePaint.color = Color.WHITE canvas.drawCircle( mDataPointList1[mDataPointList1.size - 1].x, mDataPointList1[mDataPointList1.size - 1].y, lineWidth * 0.5f, mLinePaint ) mLinePaint.color = lineColor2 canvas.drawCircle( mDataPointList2[mDataPointList2.size - 1].x, mDataPointList2[mDataPointList2.size - 1].y, lineWidth * 1.5f, mLinePaint ) mLinePaint.color = Color.WHITE canvas.drawCircle( mDataPointList2[mDataPointList2.size - 1].x, mDataPointList2[mDataPointList2.size - 1].y, lineWidth * 0.5f, mLinePaint ) } } } } //计算数值对应的Y坐标 private fun calculateYPosition(data: Float): Float = mYPoint - data / maxYValue * mYLength //绘制点击后的详情展示 private fun drawLabel(canvas: Canvas) { if (clickAble && mDataList1.size > 0) { //绘制点击后的竖刻线 mLabelPaint.color = Color.parseColor("#EBEBEB") mLabelPaint.strokeWidth = DEFAULT_GRID_WIDTH * 2 canvas.drawLine( mDataPointList1[mClickIndex].x, mYPoint, mDataPointList1[mClickIndex].x, topMargin - labelArrowMargin, mLabelPaint ) //绘制点击后的折线交点 mLabelPaint.color = lineColor1 canvas.drawCircle( mDataPointList1[mClickIndex].x, mDataPointList1[mClickIndex].y, lineWidth * 2.3f, mLabelPaint ) mLabelPaint.color = lineColor2 canvas.drawCircle( mDataPointList2[mClickIndex].x, mDataPointList2[mClickIndex].y, lineWidth * 2.3f, mLabelPaint ) //绘制最上方标签信息 with(mLabelRectF) { bottom = topMargin - labelArrowMargin - labelArrowHeight top = bottom - labelHeight; left = mDataPointList1[mClickIndex].x - labelArrowWidth / 2f - labelArrowOffset right = left + labelWidth //处理点击第一项出现标签偏离整个折线图现象 if (left < 0) { left = SizeUtil.dp2px(5f) right = left + labelWidth } //处理点击最后一项出现标签偏离整个折线图现象 if (right > mWidth) { right = mWidth.toFloat() - SizeUtil.dp2px(5f) left = right - labelWidth } } //绘制圆角矩形 mLabelBgPaint.setShadowLayer( SizeUtil.dp2px(12f), //阴影效果 SizeUtil.dp2px(2.5f), SizeUtil.dp2px(1.5f), Color.parseColor("#C7C7C7") ) canvas.drawRoundRect(mLabelRectF, labelRadius, labelRadius, mLabelBgPaint) //绘制箭头 val arrowPath = Path() with(arrowPath) { moveTo(mDataPointList1[mClickIndex].x, topMargin - labelArrowMargin) val baseY = topMargin - labelArrowMargin - labelArrowHeight - SizeUtil.dp2px(1f) lineTo(mDataPointList1[mClickIndex].x - labelArrowWidth / 2f, baseY) lineTo(mDataPointList1[mClickIndex].x + labelArrowWidth / 2f, baseY) close() } mLabelPaint.color = labelBackgroundColor canvas.drawPath(arrowPath, mLabelPaint) mLabelPaint.color = Color.parseColor("#F1F1F1") mLabelPaint.strokeWidth = gridWidth canvas.drawLine( mLabelRectF.left + SizeUtil.dp2px(10f), mLabelRectF.bottom - SizeUtil.dp2px(52f), mLabelRectF.right - SizeUtil.dp2px(10f), mLabelRectF.bottom - SizeUtil.dp2px(52f), mLabelPaint ) //绘制文字 with(mTextPaint) { color = labelTextColor textSize = labelTextSize textAlign = Paint.Align.LEFT } canvas.drawText( mXLabelList[mClickIndex], mLabelRectF.left + SizeUtil.dp2px(9.5f), mLabelRectF.bottom - SizeUtil.dp2px(61f), mTextPaint ) canvas.drawText( "交易收益 ?${mDataList1[mClickIndex]}", mLabelRectF.left + SizeUtil.dp2px(19.5f), mLabelRectF.bottom - SizeUtil.dp2px(32.5f), mTextPaint ) canvas.drawText( "返现收益 ?${mDataList2[mClickIndex]}", mLabelRectF.left + SizeUtil.dp2px(19.5f), mLabelRectF.bottom - SizeUtil.dp2px(12.5f), mTextPaint ) mTextPaint.color = lineColor1 canvas.drawCircle( mLabelRectF.left + SizeUtil.dp2px(12.5f), mLabelRectF.bottom - SizeUtil.dp2px(36f), SizeUtil.dp2px(2.5f), mTextPaint ) mTextPaint.color = lineColor2 canvas.drawCircle( mLabelRectF.left + SizeUtil.dp2px(12.5f), mLabelRectF.bottom - SizeUtil.dp2px(16f), SizeUtil.dp2px(2.5f), mTextPaint ) } } //绘制底部类型说明 private fun drawBottomDescription(canvas: Canvas) { if (mDataList1 == null || mDataList1.size == 0 || mDataList2 == null || mDataList2.size == 0) { return } mTextPaint.color = lineColor1 val centerX1 = mWidth / 2f - SizeUtil.dp2px(75.5f) canvas.drawCircle( centerX1, mHeight - SizeUtil.dp2px(20f), SizeUtil.dp2px(3.5f), mTextPaint ) mTextPaint.color = lineColor2 val centerX2 = mWidth / 2f + SizeUtil.dp2px(16f) canvas.drawCircle( centerX2, mHeight - SizeUtil.dp2px(20f), SizeUtil.dp2px(3.5f), mTextPaint ) mTextPaint.color = Color.WHITE canvas.drawCircle( centerX1, mHeight - SizeUtil.dp2px(20f), SizeUtil.dp2px(2.2f), mTextPaint ) canvas.drawCircle( centerX2, mHeight - SizeUtil.dp2px(20f), SizeUtil.dp2px(2.2f), mTextPaint ) with(mTextPaint) { color = labelTextColor textSize = SizeUtil.sp2px(12f) } canvas.drawText( "交易收益", centerX1 + SizeUtil.dp2px(8f), mHeight - SizeUtil.dp2px(15.5f), mTextPaint ) canvas.drawText( "返现收益", centerX2 + SizeUtil.dp2px(8f), mHeight - SizeUtil.dp2px(15.5f), mTextPaint ) } //格式化标签内的数值文本 private fun formatValue(value: Float): String { val scale = maxYValue / yLabelCount.toFloat() if (scale < 10 && (value != value.toInt().toFloat()) && (value >= 0.01f)) { return "${(value * 100).toInt().toFloat() / 100}" //保留2位小数,但不四舍五入 } return "${value.toInt()}" } //是否只有一条数据 private fun hasOnlyOneData(): Boolean = mDataList1.size == 1 && mDataList2.size == 1 && mXLabelList.size == 1 //设置数据,startAnim:是否开启动画,动画默认一条一条折线的画 //list1和list2的数据个数要相同,dateList的数据个数大于等于list1和list2的数据个数 fun drawData(list1: MutableList<Float>, list2: MutableList<Float>, dateList: MutableList<String>, startAnim: Boolean = false) { if (list1.size != list2.size) { throw RuntimeException("the size of list1 must be equal to the size of list2") } if (dateList.size < list1.size) { throw RuntimeException("the size of dateList can not less than the size of list1") } var maxValue = 0f for (item in list1) { if (maxValue <= item) { maxValue = item } } for (item in list2) { if (maxValue <= item) { maxValue = item } } mDataList1 = list1 mDataList2 = list2 mXLabelList = dateList maxYValue = calculateMaxValue(maxValue) mClickIndex = 0 setYLable() //重新设置Y轴刻度值 if (startAnim) { val animator = ValueAnimator.ofInt(0, mDataList1.size - 2) animator.setDuration(1500) animator.addUpdateListener { mCurrentDrawIndex = it.getAnimatedValue() as Int invalidate() } animator.interpolator = LinearInterpolator() animator.start() } else { mCurrentDrawIndex = mDataList1.size - 2 invalidate() } } //计算Y轴最大值和单位,计算规则:最高位数加1取整 private fun calculateMaxValue(value: Float): Int { val valueStr = value.toLong().toString() val length = valueStr.length //整数的位数 val unit = Math.pow(10.0, (length - 1).toDouble()).toInt() if (value == 0f) { return DEFAULT_MAX_YVALUE //如果最大值是0,即所有数据都是0,取默认的最大值 } else if (value % unit == 0f) { return value.toInt() } else { return ((value / unit).toInt() + 1) * unit } } }
使用举例:
private fun createType2Data(count: Int, isDateMore: Boolean = false, startAnim: Boolean = false, showYLabelText: Boolean = false) { val list1: MutableList<Float> = mutableListOf() val list2: MutableList<Float> = mutableListOf() val dateList: MutableList<String> = mutableListOf() for (i in 0..count) { list1.add(Random.nextDouble(80.0).toFloat()) list2.add(Random.nextDouble(80.0).toFloat()) dateList.add(DateUtil.getDistanceDateByDay(i - count, DateUtil.M_D)) } if (isDateMore) { dateList.add(DateUtil.getDistanceDateByDay(1, DateUtil.M_D)) } if (showYLabelText) { binding.type2Lc.leftMargin = SizeUtil.dp2px(40f) } else { binding.type2Lc.leftMargin = SizeUtil.dp2px(15f) } binding.type2Lc.showYLabelText = showYLabelText binding.type2Lc.drawData(list1, list2, dateList, startAnim) }