记录一个温度曲线的View

image-20220713155216246.png 最近做项目需求的看到需要自定义一个温度曲线的图。由于之前的同事理解需求的时候没有很好的理解产品的需求,将温度的折线图分成了两个View,温度高的在一个View,温度低的在一个View。这样的做法其实是没有很好的理解产品的需求的。为什么这么说,因为一旦拆成两个View,那么哪些相交的点绘制就会有缺陷了。什么意思,看图。

image-20220713155901206.png

如果按照两个View去做,就会有这种局限性。相交的点就会被切。所以这里就重新修改了这个自定义View。

有了上面的需求,那么就开始我们的设计了。首先为了我们自定义View的能比较好的通用性,我们需要把一些可能会变的东西提取出来。这里只是提取一些很常用的属性,其余需要自定义的,可自己加上。直接看代码

<declare-styleable name="NewWeatherChartView">
    <!--开始的x坐标-->
    <attr name="new_start_point_x" format="dimension"/>
    <!--两点之间x坐标的间隔-->
    <attr name="new_point_x_margin" format="dimension"/>
    <!--显示温度的字体大小-->
    <attr name="temperature_text_size" format="dimension"/>
    <!--圆点的半径-->
    <attr name="point_radius" format="dimension"/>
​
    <!--选中天气项,温度字体的颜色-->
    <attr name="select_temperature_text_color" format="reference|color"/>
    <!--未选中天气项,温度字体的颜色-->
    <attr name="unselect_temperature_text_color" format="reference|color"/>
    <!--选中天气项,圆点的颜色-->
    <attr name="select_point_color" format="reference|color"/>
    <!--未选中天气项,圆点的颜色-->
    <attr name="unselect_point_color" format="reference|color"/>
    <!--连接线的颜色-->
    <attr name="line_color" format="reference|color"/>
    <!--连接线的类型,可以是实线,也可以是虚线,默认是虚线。0虚线,1实线-->
    <attr name="line_type" format="integer"/></declare-styleable>
复制代码
public class NewWeatherChartView extends View {
    private final static String TAG = "NewWeatherChartView";
    private List<WeatherInfo> items;//温度的数据源
​
    //都是可以在XML里面配置的属性,目前项目里面都是用的默认配置。
    private int mLineColor;
    private int mSelectTemperatureColor;
    private int mUnSelectTemperatureColor;
    private int mSelectPointColor;
    private int mUnselectPointColor;
    private int mLineType;
    private int mTemperatureTextSize;
    private int mPointStartX = 0;
    private int mPointXMargin = 0;
    private int mPointRadius;
​
​
    
    private Point[] mHighPoints; //高温的点的坐标
    private Point[] mLowPoints; //低温的点的坐标
​
    //这里是为了方便写代码,多创建了几个画笔,也可以用一个画笔,然后配置不同的属性
    private Paint mLinePaint;   //用于画线画笔
    private Paint mTextPaint; // 用于画小圆点旁边的温度文字的画笔
    private Paint mCirclePaint;//用来画小圆点的画笔
   
​
    private Float mMaxTemperature = Float.MIN_VALUE;//最高温度
    private Float mMinTemperature = Float.MAX_VALUE;//最低温度
    private Path mPath;//连接线的路径
    
    private DecimalFormat mDecimalFormat;
​
​
    private int mTodayIndex = -1;//用于判断哪一个被选中
​
    private Context mContext;
    ...
}
复制代码

以上就是一些初始化的东西了,那么现在就来思考一下,怎么去画这些东西,上面的初始化也说明了,我们主要是画线,画文字,然后画圆点。那么应该从哪开始呢?首先是从点坐标开始,因为无论是线,还是文字,他们的位置和点都有关系。那么找到点的坐标就是首要的工作。怎么找点的坐标,以及最开始的X坐标是多少。第一个点的X坐标是根据我们的配置来的,那么第二个点的x坐标呢?,第二个点的x坐标就是第一个点的x坐标加上他们之间的在X方向上距离,而在x方向上的距离也是根据属性配置的。所以我们可以很容易得到所有点的x坐标。那么圆点的y坐标呢?首先我们看一张图。

image-20220713172903532.png

我们的点,应该是均匀分布在剩余高度里面的。

剩余高度 = 控件高度-2*文字的高度。

点的y坐标为

*剩余高度-((当前温度-最低温度)/(最高温度-最低温度)剩余高度)+文字高度

看起来有点复杂,但是有公式的话,代码会比较简单。接下来就需要看初始化的代码了和计算点坐标的代码了

代码如下:

//首先从两个参数的构造函数里面获取各种配置的值
public NewWeatherChartView(Context context, AttributeSet attrs) {
    super(context, attrs);
    TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.NewWeatherChartView);
    mPointStartX = (int) typedArray.getDimension(R.styleable.NewWeatherChartView_new_start_point_x, 0);
    mPointXMargin = (int) typedArray.getDimension(R.styleable.NewWeatherChartView_new_point_x_margin, 0);
    mTemperatureTextSize = (int) typedArray.getDimension(R.styleable.NewWeatherChartView_temperature_text_size, 20);
    mPointRadius = (int) typedArray.getDimension(R.styleable.NewWeatherChartView_point_radius, 8);
​
    mSelectPointColor = typedArray.getColor(R.styleable.NewWeatherChartView_select_point_color, context.getResources().getColor(R.color.weather_select_point_color));
    mUnselectPointColor = typedArray.getColor(R.styleable.NewWeatherChartView_unselect_point_color, context.getResources().getColor(R.color.weather_unselect_point_color));
    mLineColor = typedArray.getColor(R.styleable.NewWeatherChartView_line_color, context.getResources().getColor(R.color.weather_line_color));
    mSelectTemperatureColor = typedArray.getColor(R.styleable.NewWeatherChartView_select_temperature_text_color, context.getResources().getColor(R.color.weather_select_temperature_color));
    mUnSelectTemperatureColor = typedArray.getColor(R.styleable.NewWeatherChartView_unselect_temperature_text_color, context.getResources().getColor(R.color.weather_unselect_temperature_color));
​
    mLineType = typedArray.getInt(R.styleable.NewWeatherChartView_line_type, 0);
​
    this.mContext = context;
    typedArray.recycle();
}
​
private void initData() {
    //初始化线的画笔
    mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mLinePaint.setStyle(Paint.Style.STROKE);
    mLinePaint.setStrokeWidth(2);
    mLinePaint.setDither(true);
    //配置虚线
    if (mLineType == 0) {
        DashPathEffect pathEffect = new DashPathEffect(new float[]{10, 5}, 1);
        mLinePaint.setPathEffect(pathEffect);
    }
    mPath = new Path();
​
    //初始化文字的画笔
    mTextPaint = new Paint();
    mTextPaint.setAntiAlias(true);
    mTextPaint.setTextSize(sp2px(mTemperatureTextSize));
    mTextPaint.setTextAlign(Paint.Align.CENTER);
​
    // 初始化圆点的画笔
    mCirclePaint = new Paint();
    mCirclePaint.setStyle(Paint.Style.FILL);
​
    mDecimalFormat = new DecimalFormat("0");
​
    for (int i = 0; i < items.size(); i++) {
        float highY = items.get(i).getHigh();
        float lowY = items.get(i).getLow();
        if (highY > mMaxTemperature) {
            mMaxTemperature = highY;
        }
        if (lowY < mMinTemperature) {
            mMinTemperature = lowY;
        }
        if (DateUtil.fromTodayDate(items.get(i).getDate()) == 0) {
            mTodayIndex = i;
        }
    }
    float span = mMaxTemperature - mMinTemperature;
    //这种情况是为了防止所有温度都一样的情况
    if (span == 0) {
        span = 6.0f;
    }
    mMaxTemperature = mMaxTemperature + span / 6.0f;
    mMinTemperature = mMinTemperature - span / 6.0f;
​
    mHighPoints = new Point[items.size()];
    mLowPoints = new Point[items.size()];
}
​
public int sp2px(float spValue) {
    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spValue, Resources.getSystem().getDisplayMetrics());
}
​
public int dip2px(float dpValue) {
    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, Resources.getSystem().getDisplayMetrics());
​
}
复制代码

这些准备工作昨晚之后,我们就可以去onDraw里面画图了。

protected void onDraw(Canvas canvas) {
    Logging.d(TAG, "onDraw: ");
    if (items == null) {
        return;
    }
    int pointX = mPointStartX; // 开始的X坐标
    int textHeight = sp2px(mTemperatureTextSize);//文字的高度
    int remainingHeight = getHeight() - textHeight * 2;//除去文字后,剩余的高度
​
    // 计算每一个点的X和Y坐标
    for (int i = 0; i < items.size(); i++) {
        int x = pointX + mPointXMargin * i;
        float highTemp = items.get(i).getHigh();
        float lowTemp = items.get(i).getLow();
        int highY = remainingHeight - (int) (remainingHeight * ((highTemp - mMinTemperature) / (mMaxTemperature - mMinTemperature))) + textHeight;
        int lowY = remainingHeight - (int) (remainingHeight * ((lowTemp - mMinTemperature) / (mMaxTemperature - mMinTemperature))) + textHeight;
        mHighPoints[i] = new Point(x, highY);
        mLowPoints[i] = new Point(x, lowY);
    }
​
    // 画线
    drawLine(mHighPoints, canvas);
    drawLine(mLowPoints, canvas);
    for (int i = 0; i < mHighPoints.length; i++) {
        // 画文本度数 例如:3°
        String yHighText = mDecimalFormat.format(items.get(i).getHigh());
        String yLowText = mDecimalFormat.format(items.get(i).getLow());
        int highDrawY = mHighPoints[i].y - dip2px(mPointRadius + 8);
        int lowDrawY = mLowPoints[i].y + dip2px(mPointRadius + 8 + sp2px(mTemperatureTextSize));
​
        if (i == mTodayIndex) {
            mTextPaint.setColor(mSelectTemperatureColor);
            mCirclePaint.setColor(mSelectPointColor);
        } else {
            mTextPaint.setColor(mUnSelectTemperatureColor);
            mCirclePaint.setColor(mUnselectPointColor);
        }
        canvas.drawText(yHighText + "°", mHighPoints[i].x, highDrawY, mTextPaint);
        canvas.drawText(yLowText + "°", mLowPoints[i].x, lowDrawY, mTextPaint);
        canvas.drawCircle(mHighPoints[i].x, mHighPoints[i].y, mPointRadius, mCirclePaint);
        canvas.drawCircle(mLowPoints[i].x, mLowPoints[i].y, mPointRadius, mCirclePaint);
​
    }
}
​
​
private void drawLine(Point[] ps, Canvas canvas) {
    Point startp;
    Point endp;
    mPath.reset();
    mLinePaint.setAntiAlias(true);
    for (int i = 0; i < ps.length - 1; i++) {
        startp = ps[i];
        endp = ps[i + 1];
        mLinePaint.setColor(mLineColor);
        canvas.drawLine(startp.x, startp.y, endp.x, endp.y, mLinePaint);
    }
}
复制代码

以上就是所有关键代码了,当然,还有一个赋值的代码

public void setData(List<WeatherInfo> list) {
    this.items = list;
    initData();
}
复制代码

来看一下最后的效果图吧。

image-20220713194524550.png 以上就是一个简单的温度图了,但是这个图有很多地方可以优化,也有很多地方可以提取出来当作属性。比如我举一个优化的点,文字的测量,上面的代码对文字的测量其实是非常粗糙的。仔细观察会发现上面一条线,文字距离点的距离和下面一条线文字距离点的距离是不一样的。这就是上面没有进行文字测量的结果,我这里进行了一轮文字测量的优化,如下图: image-20220713194423946.png 这里是不是好很多了呢?大家还可以进行很多地方的优化。以上就是这篇文章的全部内容了。

猜你喜欢

转载自juejin.im/post/7119826029463470088