Android自定义柱状图

764 阅读4分钟

先上ui效果图

image.png

1. 分析UI稿 先定义一些属性

private var mouthTextColor = Color.BLACK  //底部月份文字颜色
private var bottomEndColor = Color.BLACK //柱状图渐变结束颜色
private var bottomStartColor = Color.BLACK //柱状图渐变起始颜色
private var chatTopTextColor = Color.BLACK//顶部月份文字颜色
//字体大小 
private var mouthTextSize: Float=DensityUtils.sp2px(context,12f).toFloat()
//月份最高31天  高度为 天数*倍数  31*4 
private val multiple:Int=DensityUtils.dp2px( 4f)
//柱状图宽度 
private var mChartWidth = DensityUtils.dp2px( 8f)
//柱状图底部距离布局底部距离 
private var mChartMarginBottom =DensityUtils.dp2px( 27f)
//柱状图顶部距离顶部文本的距离 
private var mChartMarginTopTitle =DensityUtils.dp2px( 4f)
//柱状图圆角 
private var mChartRadius =DensityUtils.dp2px( 8f).toFloat()
// 柱状图左右间距 
private var mStartWidth = DensityUtils.dp2px( 30f)

2. 测量出布局所需的width和height

高度:月份打卡数最高为31 依据蓝湖比例 一天等于4dp 柱状图最高 31*4 (dp)然后加上margin+padding+底部的月份文字间距

宽度:因为如果一屏显示不全,需要左右滑动,需要外层嵌套一个HorizontalScrollView 柱状图宽度12+左边间距12+最右边间距+margin+padding

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    val widthSize = MeasureSpec.getSize(widthMeasureSpec)
    val widthMode = MeasureSpec.getMode(widthMeasureSpec)
    val heightSize = MeasureSpec.getSize(heightMeasureSpec)
    val heightMode = MeasureSpec.getMode(heightMeasureSpec)

    var width = widthSize     
	var height = heightSize      
	// 如果宽度是 wrap_content 或者不是精确尺寸,需要计算实际需要的宽度     
	if (widthMode != MeasureSpec.EXACTLY) {
        width = calculateDesiredWidth()
    }

    // 高度如果不是精确尺寸,可以根据内容或者默认高度进行计算     
	if (heightMode != MeasureSpec.EXACTLY) {
        height = calculateDesiredHeight()
    }
    setMeasuredDimension(width, height)
}
private fun calculateDesiredWidth(): Int {
    val numColumns = 12 // 假设有12个月份     
	val columnWidth = mChartWidth  // 每个柱状图的宽度加上间距    
	// 计算内容所需的总宽度     
	val contentWidth = numColumns * (columnWidth + mStartWidth)
    // 加上空隙最右侧 宽度     
	val desiredWidth = contentWidth +mStartWidth+marginLeft+marginRight+paddingStart+paddingEnd      
	return desiredWidth 
	}

private fun calculateDesiredHeight(): Int {
    // 根据内容计算需要的高度,最高的为31个月  每月multiple4dp  再加上文本12sp 和间距4dp     
	val maxMouthDay = 31 // 31个月     r
	eturn ((maxMouthDay * multiple)+calculateTextHeight()+mChartMarginTopTitle+mChartMarginBottom+marginTop+
	marginBottom+paddingStart+paddingEnd).toInt()// 这里需要根据实际情况计算 
	}

开始画布局

1.先画柱状图 先定义出柱状图线性渐变 然后遍历12次 因为月份有12个月 startMargin 等于 startMargin+mStartWidth左边间距+ mChartWidth柱状图宽度 依次累加

  • var startMargin=0
  • startMargin += mStartWidth + mChartWidth
  • val rectF = RectF()
  • rectF.left = startMargin-mChartWidth.toFloat()
  • rectF.right = (startMargin ).toFloat()
  • rectF.bottom = (mHeight - mChartMarginBottom).toFloat()
  • rectF.top = (mHeight - mChartMarginBottom - list[i] * multiple).toFloat()
  • canvas.drawRoundRect(rectF, mChartRadius, mChartRadius, mChartPaint)

2.底部月份文本 先测量出文本的宽高 在通过mBound获取

  • mPaint.getTextBounds( "${i + 1}", 0, i.toString().length, mBound)
  • drawText (String text, float x, float y, Paint paint)
  • x 是文本左侧的 X 坐标。
  • y 是文本基线的 Y 坐标
  • canvas.drawText("${i + 1}月", ( startMargin-mChartWidth * 1 / 2 ).toFloat(), (mHeight - mBound.height() * 1 / 2).toFloat(), mPaint)

画柱状图顶部数字 和月份同理

  • val topTextBound = Rect()
  • mPaint.getTextBounds( "${list[i]}", 0, list[i].toString().length, topTextBound)
  • canvas.drawText("${list[i]}", ( startMargin-mChartWidth * 1 / 2).toFloat(), (mHeight - (list[i] * multiple)-topTextBound.height() * 1 / 2-mChartMarginBottom-mChartMarginTopTitle).toFloat(), mPaint)

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.LinearGradient
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.Shader
import android.util.AttributeSet
import android.view.View
import androidx.core.view.marginBottom
import androidx.core.view.marginLeft
import androidx.core.view.marginRight
import androidx.core.view.marginTop
import com.android.common.ui.R
import com.android.common.utils.DensityUtils

/**
 *
 */
class SingleChatView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    private lateinit var  mPaint: Paint
    private lateinit var  mTopTitlePaint: Paint
    private lateinit var mChartPaint: Paint
    private var mHeight = 0
    private var mWidth = 0
    private var mouthTextColor =  Color.BLACK //底部月份文字颜色
    private var bottomEndColor = Color.BLACK   //柱状图渐变结束颜色
    private var bottomStartColor = Color.BLACK //柱状图渐变起始颜色
    private var chatTopTextColor = Color.BLACK  //顶部月份文字颜色
    private var list: List<Int> = ArrayList()
    //字体大小
    private var mouthTextSize: Float=DensityUtils.sp2px(context,12f).toFloat()
    //月份最高31天  高度为 天数*倍数   31*4
    private val multiple:Int=DensityUtils.dp2px( 4f)
    //柱状图宽度
    private var mChartWidth = DensityUtils.dp2px( 8f)
    //柱状图底部距离布局底部距离
    private var mChartMarginBottom =DensityUtils.dp2px( 27f)
    //柱状图顶部距离顶部文本的距离
    private var mChartMarginTopTitle =DensityUtils.dp2px( 4f)
    //柱状图圆角
    private var mChartRadius =DensityUtils.dp2px( 8f).toFloat()
    // 柱状图左右间距
    private var mStartWidth = DensityUtils.dp2px( 30f)
    fun setList(list: List<Int>) {
        this.list = list
        invalidate()
    }

    init {
        val array = context.obtainStyledAttributes(attrs, R.styleable.SingleChartView)
        mChartWidth =
            array.getDimensionPixelSize(R.styleable.SingleChartView_chart_histogram_width, mChartWidth)
        mStartWidth =
            array.getDimensionPixelSize(R.styleable.SingleChartView_chart_item_space, mStartWidth)
        mouthTextSize =
            array.getDimension(R.styleable.SingleChartView_mouthTextSize, 12f)
        mouthTextColor=array.getColor(R.styleable.SingleChartView_mouthTextColor,Color.BLACK)
        bottomStartColor=array.getColor(R.styleable.SingleChartView_bottomStartColor,Color.BLACK)
        bottomEndColor=array.getColor(R.styleable.SingleChartView_bottomEndColor,Color.BLACK)
        chatTopTextColor=array.getColor(R.styleable.SingleChartView_chatTopTextColor,Color.BLACK)
        array.recycle()
        initPaint()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val widthSize = MeasureSpec.getSize(widthMeasureSpec)
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        val heightSize = MeasureSpec.getSize(heightMeasureSpec)
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)

        var width = widthSize
        var height = heightSize

        // 如果宽度是 wrap_content 或者不是精确尺寸,需要计算实际需要的宽度
        if (widthMode != MeasureSpec.EXACTLY) {
            width = calculateDesiredWidth()
        }

        // 高度如果不是精确尺寸,可以根据内容或者默认高度进行计算
        if (heightMode != MeasureSpec.EXACTLY) {
            height = calculateDesiredHeight()
        }

        setMeasuredDimension(width, height)
    }

    private fun calculateDesiredWidth(): Int {
        val numColumns = 12 // 假设有12个月份
        val columnWidth = mChartWidth  // 每个柱状图的宽度加上间距

        // 计算内容所需的总宽度
        val contentWidth = numColumns * (columnWidth + mStartWidth)

        // 加上空隙最右侧 宽度
        val desiredWidth = contentWidth +mStartWidth+marginLeft+marginRight+paddingStart+paddingEnd

        return desiredWidth
    }

    private fun calculateDesiredHeight(): Int {
        // 根据内容计算需要的高度,最高的为31个月  每月multiple4dp  再加上文本12sp 和间距4dp
        val maxMouthDay = 31 // 31个月
        return ((maxMouthDay * multiple)+calculateTextHeight()+mChartMarginTopTitle+mChartMarginBottom+marginTop+marginBottom+paddingStart+paddingEnd).toInt()// 这里需要根据实际情况计算
    }


    private fun calculateTextHeight(): Int {
        val textPaint = Paint()
        textPaint.textSize = DensityUtils.sp2px(context,12f).toFloat() // 假设文本大小为12sp
        val textBounds = Rect()
        textPaint.getTextBounds("SS", 0, "SS".length, textBounds)
        return textBounds.height()
    }
    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        super.onLayout(changed, left, top, right, bottom)
        mWidth = width
        mHeight = height
    }

    private fun initPaint() {
        mPaint = Paint()
        mPaint.isAntiAlias = true
        mPaint.isFakeBoldText = true

        mTopTitlePaint= Paint()
        mTopTitlePaint.isAntiAlias = true


        mChartPaint = Paint()
        mChartPaint.isAntiAlias = true
    }

    @SuppressLint("DrawAllocation")
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        var startMargin=0
        for (i in 0..11) {
            mChartPaint.style = Paint.Style.FILL
            if (list.isNotEmpty()) {
                startMargin += mStartWidth + mChartWidth
                var mBound = Rect()
                //画月份数字
                mPaint.color = mouthTextColor
                mPaint.textSize = mouthTextSize
                mPaint.textAlign = Paint.Align.CENTER
                mPaint.getTextBounds( "${i + 1}", 0, i.toString().length, mBound)
                canvas.drawText("${i + 1}月", ( startMargin-mChartWidth * 1 / 2 ).toFloat(),
                    (mHeight  - mBound.height() * 1 / 2).toFloat(), mPaint)


                //画柱状图顶部数字
                mTopTitlePaint.textSize = mouthTextSize
                mPaint.textAlign = Paint.Align.CENTER
                mPaint.color = chatTopTextColor
                val topTextBound = Rect()
                mPaint.getTextBounds( "${list[i]}", 0, list[i].toString().length, topTextBound)
                canvas.drawText("${list[i]}", ( startMargin-mChartWidth * 1 / 2).toFloat(),
                    (mHeight  -  (list[i] * multiple)-topTextBound.height() * 1 / 2-mChartMarginBottom-mChartMarginTopTitle).toFloat(), mPaint)

                //画柱状图
                val lg=LinearGradient(
                    mChartWidth.toFloat(),
                    (mHeight - mChartMarginBottom).toFloat(),
                    mChartWidth.toFloat(),
                    (mHeight - mChartMarginBottom - list[i] * multiple).toFloat(),
                    bottomStartColor,
                    bottomEndColor,
                    Shader.TileMode.CLAMP
                )
                mChartPaint.setShader(lg)
                val rectF = RectF()
                rectF.left = startMargin-mChartWidth.toFloat()
                rectF.right = (startMargin ).toFloat()
                rectF.bottom = (mHeight - mChartMarginBottom).toFloat()
                rectF.top = (mHeight - mChartMarginBottom - list[i] * multiple).toFloat()
                canvas.drawRoundRect(rectF, mChartRadius, mChartRadius, mChartPaint)
            }
        }
    }

}