Android 自定义降水/降雨概率View

137 阅读2分钟

直接上代码

class RainProbabilityView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {

    //view顶部降水量和概率文字所占的高度
    private val viewTopTextHeight = PxUtils.dip2px(60f)

    //view底部时间文字所占的高度
    private val viewBottomTextHeight = PxUtils.dip2px(30f)

    //每个item的宽度
    private val itemWidth = PxUtils.dip2px(60f)

    //概率view的高度:减去上方文字高度
    private var viewHeight = 0
    private var viewBottom = 0

    private val itemList = arrayListOf<ItemData>()
    private val viewPaint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val dashPaint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val timePaint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val valuePaint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val probabilityPaint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val viewPath = Path()
    private val borderLinePath = Path()

    private val viewColor = Color.parseColor("#FF00A3FF")
    private val dashColor = Color.parseColor("#FFB2DDFF")
    private val valueColor = Color.parseColor("#FF9E9E9E")
    private val timeColor = Color.parseColor("#FF9E9E9E")
    private val timeBoldColor = Color.parseColor("#FF333333")
    private val lineColor = Color.parseColor("#FFF5F5F5")
    private val valueTextSize = PxUtils.dip2px(12f).toFloat()
    private val timeTextSize = PxUtils.dip2px(14f).toFloat()

    init {
        genTestData()

        viewPaint.color = viewColor
        viewPaint.style = Paint.Style.FILL
        viewPaint.isDither = true
        viewPaint.strokeWidth = 3f
        viewPaint.pathEffect = CornerPathEffect(25f)

        dashPaint.color = dashColor
        dashPaint.strokeWidth = 1f
        dashPaint.pathEffect = DashPathEffect(floatArrayOf(10f, 5f), 0f)

        linePaint.color = lineColor
        linePaint.style = Paint.Style.STROKE
        linePaint.strokeWidth = 1f
        linePaint.pathEffect = CornerPathEffect(25f)

        timePaint.textAlign = Paint.Align.CENTER
        timePaint.color = timeColor
        timePaint.textSize = timeTextSize
        timePaint.style = Paint.Style.FILL

        valuePaint.textAlign = Paint.Align.CENTER
        valuePaint.color = valueColor
        valuePaint.textSize = valueTextSize
        valuePaint.style = Paint.Style.FILL

        probabilityPaint.textAlign = Paint.Align.CENTER
        probabilityPaint.color = viewColor
        probabilityPaint.textSize = valueTextSize
        probabilityPaint.style = Paint.Style.FILL
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val heightSpecSize = MeasureSpec.getSize(heightMeasureSpec)
        val width = itemWidth * itemList.size
        setMeasuredDimension(width, heightSpecSize)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        this.viewHeight = h - viewTopTextHeight - viewBottomTextHeight
        this.viewBottom = viewTopTextHeight + viewHeight

        var maxValue = 0.0
        itemList.forEach { item ->
            maxValue = max(item.value, maxValue)
        }
        //根据最大值确定每个降雨点高度
        itemList.forEachIndexed { index, itemData ->
            val x = itemWidth * index + itemWidth / 2
            val y =
                viewTopTextHeight + (viewHeight - (itemData.value * 1.0 / maxValue) * viewHeight)
            itemData.topPoint = Point(x, y.toInt())
        }
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        //绘制曲线&填充
        drawPath(canvas)
        //绘制虚线
        drawDash(canvas)
        //绘制降水量/降水概率/时间
        drawText(canvas)
    }

    private fun drawText(canvas: Canvas?) {
        itemList.forEachIndexed { index, itemData ->
            val x = itemData.topPoint.x.toFloat()
            val y = itemData.topPoint.y.toFloat()

            val probabilityY = y - PxUtils.dip2px(15f).toFloat()
            val valueY = probabilityY - PxUtils.dip2px(15f).toFloat()
            val timeY = viewBottom + PxUtils.dip2px(20f).toFloat()
            //绘制降水量
            canvas?.drawText("${itemData.value}mm", x, valueY, valuePaint)
            //绘制降水概率
            canvas?.drawText("${itemData.probability}%", x, probabilityY, probabilityPaint)
            //绘制时间
            canvas?.drawText(itemData.time, x, timeY, timePaint)
        }
    }

    private fun drawDash(canvas: Canvas?) {
        itemList.forEachIndexed { index, itemData ->
            val x = itemData.topPoint.x.toFloat()
            val y = itemData.topPoint.y.toFloat() - PxUtils.dip2px(10f).toFloat()
            canvas?.drawLine(x, y, x, viewBottom.toFloat(), dashPaint)
            canvas?.drawCircle(x, viewBottom.toFloat(), PxUtils.dip2px(4f).toFloat(), dashPaint)
        }
    }

    private fun drawPath(canvas: Canvas?) {
        viewPath.reset()
        itemList.forEachIndexed { index, itemData ->
            val x = itemData.topPoint.x.toFloat()
            val y = itemData.topPoint.y.toFloat()
            if (index == 0) {
                borderLinePath.reset()
                borderLinePath.moveTo(x - itemWidth / 2 + 2, 0f)
                borderLinePath.lineTo(x - itemWidth / 2 + 2, viewBottom.toFloat() - 2)
                borderLinePath.lineTo(x, viewBottom.toFloat() - 2)
                canvas!!.drawPath(borderLinePath, linePaint)

                viewPath.moveTo(x - itemWidth / 2, viewBottom.toFloat())
                viewPath.lineTo(x - itemWidth / 2, (viewBottom - (viewBottom - y) * 0.7).toFloat())
                viewPath.lineTo(x, y)
            } else {
                viewPath.lineTo(x, y)
            }
            if (index == itemList.size - 1) {
                borderLinePath.reset()
                borderLinePath.moveTo(x + itemWidth / 2 - 2, 0f)
                borderLinePath.lineTo(x + itemWidth / 2 - 2, viewBottom.toFloat() - 2)
                borderLinePath.lineTo(x, viewBottom.toFloat() - 2)
                canvas!!.drawPath(borderLinePath, linePaint)

                viewPath.lineTo(x + itemWidth / 2, (viewBottom - (viewBottom - y) * 0.7).toFloat())
                viewPath.lineTo(x + itemWidth / 2, viewBottom.toFloat())
                viewPath.close()
            }
        }
        canvas!!.drawPath(viewPath, viewPaint)
    }


    fun genTestData() {
        itemList.clear()
        val rain = arrayOf(
            12.0,
            6.0,
            4.0,
            18.0,
            18.0,
            4.0,
            10.0,
            9.0,
            4.0,
            10.0,
            20.0,
            10.0,
            12.0,
            6.0,
            4.0,
            18.0,
            18.0,
            12.0,
            10.0,
            0.0,
            0.0,
            0.0,
            0.0,
            6.0
        )
        rain.forEachIndexed { index, d ->
            itemList.add(ItemData("${index}时", d, 80, false, Point()))
        }
    }
}

class ItemData(
    val time: String,
    val value: Double,//降水量
    val probability: Int,//降雨概率
    val bold: Boolean,//时间参数是否粗体
    var topPoint: Point//顶点坐标
)

PxUtils 工具类

public class PxUtils {
    private static float sDensity = 1.0f;

    static {
        Resources resources = Resources.getSystem();
        if (resources != null) {
            sDensity = resources.getDisplayMetrics().density;
        }
    }

    public static int dip2px(float dipVlue) {
        return (int) (dipVlue * sDensity + 0.5f);
    }

    public static int px2dip(float pxValue) {
        final float scale = sDensity;
        return (int) (pxValue / scale + 0.5f);
    }
}

xml布局

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <HorizontalScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintTop_toTopOf="parent">

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

            <com.aqian.tech.test.weather.RainProbabilityView
                android:id="@+id/rain_probability_view"
                android:layout_width="2000dp"
                android:layout_height="150dp"
                android:layout_marginHorizontal="20dp" />

        </LinearLayout>
    </HorizontalScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>