自定义 Android 天气趋势图

1,457 阅读4分钟
原文链接: blog.lovemma.xyz

初学自定义View,没什么好的想法(其实是难的做不来,简单的不想做),加上以前想做个天气APP的项目,所以就做了个这个练练手。
初次学习使用,自定义View的很多东西还不是很熟悉,大家凑合着看吧。

先来看看效果:
Screenshot_1492696413.png
控件内容比较简单,就是一个普通的折线图,上下分别带有数字。

因为这里不需要考虑 wrap_content 的情况,所以onMeasure方法不需重写,关键的是onDraw,而onDraw方法其实也不困难,只需要确定好每个点的具体位置就好,因为连线也是需要点的坐标,代码逻辑有点混乱,可以略过,这里我没有用drawLine直接绘制线条,而是记录折线要经过的路径然后再绘制,代码如下:

@Override
  protected void onDraw(Canvas canvas) {
      super.onDraw(canvas);
      if (mWeathers == null || mWeathers.size() == 0) {
          return;
      }
      max = getMaxTmp();
      min = getMinTmp();
      //每个像素占的比率
      float ratio = (float) mHeight / (max - min);    
      xInterval = (float) mWidth / (mWeathers.size() - 2);
      float X, Y;
      float Y1;
      String tmp;
      //记录高温折线路径
      for (int i = 1; i < mWeathers.size() - 1; i++) {
          tmp = mWeathers.get(i).getHighTmp() + "°";
          X = (i - 1) * xInterval + xInterval / 2;
          Y = (max - mWeathers.get(i).getHighTmp()) * ratio + mMargin + DensityUtils.sp2px(getContext(), mTmpTextSize);
          if (i == 1) {
              Y1 = ((max - mWeathers.get(0).getHighTmp()) * ratio + mMargin + Y) / 2;
              mLinePath.moveTo(0, Y1);
          }
          mLinePath.lineTo(X, Y);
          if (i == mWeathers.size() - 2) {
              Y1 = ((max - mWeathers.get(i + 1).getHighTmp()) * ratio + mMargin + Y) / 2;
              mLinePath.lineTo(getWidth(), Y1);
          }
          //绘制每一个温度点
          canvas.drawCircle(X, Y, mCircleRadius, mCirclePaint);
          //绘制分隔线
          canvas.drawLine(X + xInterval / 2, 0,
                  X + xInterval / 2, getHeight(),
                  mAxesPaint);
          //绘制温度
          canvas.drawText(tmp, X - mTextPaint.measureText(tmp) / 2, Y - 2 * mCircleRadius, mTextPaint);
          //绘制时间
          canvas.drawText(mWeathers.get(i).getWeek(),
                  X - mTextPaint.measureText(mWeathers.get(i).getWeek()) / 2,
                  DensityUtils.sp2px(getContext(), mTmpTextSize),
                  mTextPaint);
          //绘制天气所对应的图片
          Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.sunny);
          switch (mWeathers.get(i).getCond()) {
              case "晴":
                  bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.sunny);
                  break;
              case "阴":
                  bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.overcast);
                  break;
              case "雨":
                  bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.rain);
                  break;
              case "多云":
                  bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.cloudy);
                  break;
          }
          canvas.drawBitmap(bitmap, X - bitmap.getWidth() / 2, 0, mCirclePaint);
          if (!bitmap.isRecycled()) {
              bitmap.recycle();
          }
      }
      
      //记录低温折线路径
      for (int i = mWeathers.size() - 2; i > 0; i--) {
          tmp = mWeathers.get(i).getLowTmp() + "°";
          X = (i - 1) * xInterval + xInterval / 2;
          Y = (max - mWeathers.get(i).getLowTmp()) * ratio + mMargin + DensityUtils.sp2px(getContext(), mTmpTextSize);
          if (i == mWeathers.size() - 2) {
              Y1 = ((max - mWeathers.get(i + 1).getLowTmp()) * ratio + mMargin + Y) / 2;
              mLinePath.lineTo(getWidth(), Y1);
          }
          mLinePath.lineTo(X, Y);
          if (i == 1) {
              Y1 = ((max - mWeathers.get(0).getLowTmp()) * ratio + mMargin + Y) / 2;
              mLinePath.lineTo(0, Y1);
              mLinePath.lineTo(0, 0);
              mLinePath.close();
          }
          canvas.drawCircle(X, Y, mCircleRadius, mCirclePaint);
          canvas.drawText(tmp,
                  X - mTextPaint.measureText(tmp) / 2,
                  Y + 4 * mCircleRadius,
                  mTextPaint);
      }
      canvas.drawPath(mLinePath, mPaintShader);
  }

考虑到需要允许用户进行简单的设置,例如点的大小,文字大小等等,所以定义一些自定义属性(res/values/attr.xml):


       < attr name="circle_radius" format="dimension"/>< !--坐标点半径-->
       < attr name="circle_color" format="color"/>< !--坐标点颜色-->
       < attr name="line_color" format="color"/>< !--折线颜色-->
       < attr name="line_width" format="dimension"/>< !--折线宽度-->
       < attr name="axes_color" format="color"/> < !--坐标轴颜色-->
       < attr name="axes_width" format="dimension"/>< !--坐标轴宽度-->
       < attr name="shadow_color" format="color"/> < !--坐标轴颜色-->
       < attr name="tmp_text_size" format="dimension"/>
   

format指该属性的格式,指定为dimension则是尺寸,取值单位是dp、sp或px等等,color是颜色,还有其他类型,可以自行查找文档。

对自定义属性进行解析得到,这个解析需要在构造方法中进行,代码如下:

TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.WeatherTrendGraph);
mLineColor = typedArray.getColor(R.styleable.WeatherTrendGraph_line_color, Color.BLACK);
mLineWidth = typedArray.getDimensionPixelSize(R.styleable.WeatherTrendGraph_line_width, 2);
mAxesColor = typedArray.getColor(R.styleable.WeatherTrendGraph_axes_color, Color.BLACK);
mAxesWidth = typedArray.getDimensionPixelSize(R.styleable.WeatherTrendGraph_axes_width, 1);
mCircleRadius = typedArray.getDimensionPixelSize(R.styleable.WeatherTrendGraph_circle_radius, 12);
mCircleColor = typedArray.getColor(R.styleable.WeatherTrendGraph_circle_color, Color.GRAY);
mShaderColor = typedArray.getColor(R.styleable.WeatherTrendGraph_shadow_color, Color.GRAY);
mTmpTextSize = typedArray.getDimension(R.styleable.WeatherTrendGraph_tmp_text_size, 35);
typedArray.recycle();

getDimensionPixelSize方法则是通过传入的值,转换为具体的像素(px)值,也就免去我们手动转换的麻烦。但是要注意,参数中的defaultValue依然是px。

天气实体类:

public class Weather {
    private int highTmp;    //高温
    private int lowTmp;     //低温
    private String date;    //日期
    private String cond;    //天气状况
    public Weather(int highTmp, int lowTmp) {
        this.highTmp = highTmp;
        this.lowTmp = lowTmp;
    }
    public Weather(int highTmp, int lowTmp, String date) {
        this.highTmp = highTmp;
        this.lowTmp = lowTmp;
        this.date = date;
    }
    public Weather(int highTmp, int lowTmp, String date, String cond) {
        this.highTmp = highTmp;
        this.lowTmp = lowTmp;
        this.date = date;
        this.cond = cond;
    }
    public int getHighTmp() {
        return highTmp;
    }
    public void setHighTmp(int highTmp) {
        this.highTmp = highTmp;
    }
    public int getLowTmp() {
        return lowTmp;
    }
    public void setLowTmp(int lowTmp) {
        this.lowTmp = lowTmp;
    }
    public String getDate() {
        return date;
    }
    public void setDate(String date) {
        this.date = date;
    }
    public String getCond() {
        return cond;
    }
    public void setCond(String cond) {
        this.cond = cond;
    }
    public String getWeek() {
        String[] weeks = {"周日", "周一", "周二", "周三", "周四", "周五", "周六"};
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        Date date = null;
        try {
            date = sdf.parse(this.date);
            Calendar calendar = Calendar.getInstance();
            calendar.setTime(date);
            int week_index = calendar.get(Calendar.DAY_OF_WEEK) - 1;
            if (week_index < 0) {
                week_index = 0;
            }
            return weeks[week_index];
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return this.date;
    }
}

MainActivity中使用:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    mWeatherTrendGraph = (WeatherTrendGraph) findViewById(R.id.weatherTrendGraph);
    weather = new Weather(28, 16, "2017-04-18", "晴");
    mWeathers.add(weather);
    weather = new Weather(30, 17, "2017-04-19", "晴");
    mWeathers.add(weather);
    weather = new Weather(23, 13, "2017-04-20", "阴");
    mWeathers.add(weather);
    weather = new Weather(20, 13, "2017-04-21", "多云");
    mWeathers.add(weather);
    weather = new Weather(25, 17, "2017-04-22", "雨");
    mWeathers.add(weather);
    weather = new Weather(27, 17, "2017-04-23", "晴");
    mWeathers.add(weather);
    weather = new Weather(22, 16, "2017-04-24", "阴");
    mWeathers.add(weather);
    weather = new Weather(20, 14, "2017-04-25", "晴");
    mWeathers.add(weather);
    mWeatherTrendGraph.setWeathers(mWeathers);
}

整个项目源码地址:Github地址