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)
 }

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • MPAndroidChart开源图表库的使用介绍之饼状图、折线图和柱状图

    MPAndroidChart开源图表库之饼状图 为大家介绍一款图标开源库MPAndroidChart,它不仅可以在Android设备上绘制各种统计图表,而且可以对图表进行拖动和缩放操作,用起来非常灵活.MPAndroidChart同样拥有常用的图表类型:线型图.饼图.柱状图和散点图. mpandroidchartlibrary.jar包下载地址: https://github.com/PhilJay/MPAndroidChart/releases 下面主要实现以下饼状图: 1.从上面的地址中下载

  • Android自定义View实现折线图效果

    下面就是结果图(每种状态用一个表情图片表示): 一.主页面的布局文件如下: <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height=&quo

  • Android绘制动态折线图

    所谓动态折线图,就是折线图能随着手指的滑动进行动态绘制,这里很定会产生动画效果.基于这个效果,这里使用SurfaceView进行制图. 实现步奏如下: (1): 这里新建一个绘图ChartView,继承SurfaceView并实现SurfaceHolder.Callback , Runnable接口,主要绘图工作在子线程中完成. (2):现实 SurfaceHolder.Callback接口的三个方法,并在 surfaceCreated中开启子线程进行绘图. (3):重写onTouchEvent

  • Android自定义View简易折线图控件(二)

    继续练习自定义View,这次带来的是简易折线图,支持坐标点点击监听,效果如下: 画坐标轴.画刻度.画点.连线..x.y轴的数据范围是写死的 1 <= x <= 7 ,1 <= y <= 70 ..写活的话涉及到坐标轴刻度的动态计算.坐标点的坐标修改,想想就头大,这里只练习自定义View. 1.在res/values文件夹下新建attrs.xml文件,编写自定义属性: <?xml version="1.0" encoding="utf-8"

  • Android开发之天气趋势折线图

    先来看下效果: 控件内容比较简单,就是一个普通的折线图,上下分别带有数字,点击的时候显示当天温度的差值. 创建一个类继承自View,并添加两个构造方法: public class TrendGraph extends View { public TrendGraph(Context context) { // 在java代码中创建调用 super(context); } public TrendGraph(Context context, AttributeSet attrs) { // 在xm

  • Android HelloChart开源库图表之折线图的实例代码

    前面我们介绍了开源图表库MPAndroidChart,请参考: Android MPAndroidChart开源库图表之折线图的实例代码 我们今天介绍的将是一个更为优秀的图表库,比MPAndroidChart性能更好,功能更完善,UI风格更美观,坐标轴更精细. 支持缩放.滑动以及平移.Zoom(pinch to zoom, double tap zoom), scroll and fling 支持自定义坐标轴(比如坐标轴位置:上下左右内部),支持自动生成坐标轴.Custom and auto-g

  • Android自定义可左右滑动和点击的折线图

    前言 前几天有小盆友让我写一个折线图,可以点击,可以左右滑动.对于折线肯定有很多项目都使用过,所以网上肯定也有很多demo,像AndroidChart.HelloChart之类的,功能相当丰富,效果也很赞,但是太重了,其他的小demo又不符合要求,当然了,我写的自定义折线图的思想也有来自这些小demo,对他们表示感谢. 效果图 废话不多说,先上效果图: 效果是不是很赞,如果上图满足你的需求,那就继续往下看. 自定义折线图的步骤: 1.自定义view所需要的属性 确定所需要的自定义view的属性,

  • Android MPAndroidChart开源库图表之折线图的实例代码

    本文讲述了Android MPAndroidChart开源库图表之折线图的实例代码.分享给大家供大家参考,具体如下: 承接上一篇文章,请参考Android HelloChart开源库图表之折线图的实例代码 1. 将mpandroidchartlibrary-2-0-8.jar包copy到项目的libs中: 2. 定义xml文件. 3.  主要Java逻辑代码如下,注释已经都添加上了. package com.example.mpandroidlinechart; import java.util

  • Android自定义控件实现折线图

    本文实例实现一个如下图所示的Android折线图,供大家参考,具体内容如下 首先是控件绘图区域的划分,控件左边取一小部分(控件总宽度的八分之一)绘制表头,右边剩余的部分绘制表格 确定表格的行列数,首先绘制一个三行八列的网格,设置好行列的坐标后开始绘制 /*绘制三条横线*/ for(int i=0;i<3;i++){ canvas.drawLine(textWide, mLineYs[i], totalWidth, mLineYs[i], mPaintLine); } /*绘制八条竖线*/ for

  • 详解Android图表 MPAndroidChart折线图

    1.介绍 MPAndroidChart GitHub地址 MPAndroidChart的强大之处就不在多说了,目前最新的版本是3.0.1,在新版本中很多方法都被弃用了,这个要注意一下,在网上查到的大多数资料都是关于旧版本的,今天来实现一下折线图,把过程记录下来,分享给大家. 效果图: 2.引入开源库 在项目根目录的build.gradle文件中加入如下代码 allprojects { repositories { maven { url "https://jitpack.io" } }

随机推荐