注册

记录一个温度曲线的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 这里是不是好很多了呢?大家还可以进行很多地方的优化。以上就是这篇文章的全部内容了。


作者:爱海贼的小码农
链接:https://juejin.cn/post/7119826029463470088
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册