手把手教你实现Android中智能设备数据表格绘制

最近做一个android智能手表的app,要给用户呈现的就是用户每天,每周,每月数据信息,既然要使得用户能一眼就看出自己的数据趋势,当然最好的就是折线统计图或者柱状图了。
要实现这要的功能就需要借助于android强大的自定义控件了。
闲话休提,言归正传:
惯例先上效果,如下:

这里写图片描述
这里写图片描述

下面开始自定义控件的第一步:

1.在工程目录res/values下新建attrs文件
2.在文件中声明需要的属性

    <!--坐标轴线条粗细-->
    <attr name="coordinatesLineWidth" format="dimension"/>
    <!--坐标轴字体大小-->
    <attr name="coordinatesTextSize" format="dimension" />
    <!--坐标轴字体颜色-->
    <attr name="coordinatesTextColor" format="color" />
    <!--折线颜色-->
    <attr name="lineColor" format="color" />
    <!--折线粗细-->
    <attr name="lineWidth" format="dimension" />
    <!--小圆点半径-->
    <attr name="averageCircleradius" format="dimension" />
    <!--表格的数据类型-->
    <attr name="tableType" format="string" />
    <!--大圆点的颜色-->
    <attr name="maxcircleColor" format="color" />
    <!--小圆点的颜色-->
    <attr name="mincircleColor" format="color" />
    <!--背景色-->
    <attr name="bgColor" format="color" />

    <declare-styleable name="HealthyTableView">
        <attr name="coordinatesLineWidth"/>
        <attr name="coordinatesTextSize"/>
        <attr name="coordinatesTextColor"/>
        <attr name="lineColor"/>
        <attr name="lineWidth"/>
        <attr name="averageCircleradius"/>
        <attr name="tableType"/>
        <attr name="maxcircleColor"/>
        <attr name="mincircleColor"/>
        <attr name="bgColor"/>
    </declare-styleable>

3.在工程目录指定包名下创建自定义控件的类:

public class HealthyTablesView extends View {
    public HealthyTablesView(Context context) {
        this(context,null);
    }

    public HealthyTablesView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public HealthyTablesView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}

该类声明了三个参数的构造函数,让一个参数的构造函数调用二个参数的构造函数,让两个参数的构造函数调用三个参数的构造函数,接下来在第三个参数的构造函数中获取我们自定义控件的属性值:
老板,我贴代码了哦!

TypedArray array = context.getTheme().obtainStyledAttributes(attrs,
    R.styleable.HealthyTableView, defStyleAttr, 0);
  int index = array.getIndexCount();
  for (int i = 0; i < index; i++)
  {
   int attr = array.getIndex(i);

   switch (attr)
   {
   case R.styleable.HealthyTableView_coordinatesLineWidth:
    // 这里将以px为单位,默认值为2px;
    mCoordinatesLineWidth = array.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 2, getResources().getDisplayMetrics()));
    break;
   case R.styleable.HealthyTableView_coordinatesTextColor:mCoordinatesTextColor = array.getColor(attr, Color.parseColor("#808080"));
    break;
   case R.styleable.HealthyTableView_coordinatesTextSize:
    mCoordinatesTextSize = array.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 11, getResources().getDisplayMetrics()));
    break;
   case R.styleable.HealthyTableView_lineColor:
    mLineColor = array.getColor(attr, Color.BLUE);
    break;
   case R.styleable.HealthyTableView_averageCircleradius:
    mCircleradius = array.getDimensionPixelSize(attr,(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, getResources().getDisplayMetrics()));
    break;
   case R.styleable.HealthyTableView_bgColor:
    mBgColor = array.getColor(attr, Color.WHITE);
    break;
   case R.styleable.HealthyTableView_lineWidth:
    mLineWidth = array.getDimensionPixelSize(attr,(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 11, getResources().getDisplayMetrics()));
    break;
   case R.styleable.HealthyTableView_maxcircleColor:
    mMaxcircleColor = array.getColor(attr, Color.GREEN);
    break;
   case R.styleable.HealthyTableView_mincircleColor:
    mMincircleColor = array.getColor(attr, Color.WHITE);
    break;
   case R.styleable.HealthyTableView_tableType:
    mDrawType = array.getString(attr);
    break;
   }
  }
  // 记得释放资源
  array.recycle();
 }

好了,准备工作差不多了,然后呢?然后测量宽高后就开始画图了。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        /**
         * 自定义控件的宽高必须由调用者自己指定具体的数值
         */
        if (widthSpecMode == MeasureSpec.EXACTLY)
        {
            mWidth = widthSpecSize;
        }
        else
        {
            mWidth = 300;

        }

        if (heightSpecMode == MeasureSpec.EXACTLY)
        {
            //高是宽的3/5,这样好吗?
            mHeight = (mWidth / 5) * 3;
        }
        else
        {
            mHeight = 230;
        }
        Log.i(TAG, "width=" + mWidth + "...height=" + mHeight);
        setMeasuredDimension(mWidth, mHeight);
    }

开始画图了:
重写onDraw(),在里面绘制坐标系:

 /**
  * 画坐标系
  * 
  * @param canvas
  */
 private void drawCoordinates(Canvas canvas)
 {

  // X轴
  Log.i(TAG, "drawCoordinates");
  canvas.drawLine(getPaddingLeft(), mHeight - getPaddingBottom(),
    mWidth - getPaddingRight(), mHeight - getPaddingBottom(),
    xyPaint);
  // X轴上的箭头
  canvas.drawLine(mWidth - getPaddingRight() - 20,
    mHeight - getPaddingBottom() - 10,
    mWidth - getPaddingRight(), mHeight - getPaddingBottom(),
    xyPaint);
  canvas.drawLine(mWidth - getPaddingRight() - 20,
    mHeight - getPaddingBottom() + 10,
    mWidth - getPaddingRight(), mHeight - getPaddingBottom(),
    xyPaint);

  // 绘制Y轴
  canvas.drawLine(getPaddingLeft(), getPaddingTop(), getPaddingLeft(),
    mHeight - getPaddingBottom(), xyPaint);

  // Y轴上的箭头
  canvas.drawLine(getPaddingLeft() - 10, getPaddingTop() + 20 ,
    getPaddingLeft(), getPaddingTop(), xyPaint);
  canvas.drawLine(getPaddingLeft() + 10, getPaddingTop() + 20 ,
    getPaddingLeft(), getPaddingTop(), xyPaint);
 }
这里写图片描述

接下来绘制X轴上的时间值,这里以周为例,因为没有真实的数据,此次讲义都已模拟数据为主;
定义一个数组,然后将X轴等分为7等分,画上间断线,写上数值

//02号到8号,一周的时间
weeks = new String[]{"02","03","04","05","06","07","08"};

/**
  * 绘制X轴上的数值
  * 
  * @param canvas
  */
 private void drawCoordinatesXvalues(Canvas canvas)
 {

  // -40 为X轴留点边界。 /6分成7等分

  for (int i = 0; i < weeks.length; i++)
  {
   textPaint.getTextBounds(weeks[i], 0, weeks[i].length(), textBound);
   // 画间断线
   canvas.drawLine(getPaddingLeft() + (i * XScale),
     mHeight - getPaddingBottom() - 10,
     getPaddingLeft() + (i * XScale),
     mHeight - getPaddingBottom(), xyPaint);
   // -textBound.width()/2 是为了让字体和间断线居中
   canvas.drawText(weeks[i],
     getPaddingLeft() + (i * XScale) - textBound.width() / 2,
     mHeight - getPaddingBottom() + 30, textPaint);
  }
 }

上图:


这里写图片描述

上面的逻辑和计算并不复杂,就是将X轴的距离等分7等分,然后画上间断线和数值就OK了。

接下来计算Y轴上的要画得数值,因为Y轴上的数值要根据用户的真实数据来确定,所以幅度很大,不确定性因素也很多。这样就需要我们动态的计算Y轴上的数值区间:

1.首先计算出用户数据中的最大值和最小值来确定区间:
2.将计算出的最大值和最小值向上向下取一定幅度的值,比如最大值123,最小值63,最大值就可以取123+10,最小值取60-10,

 /**
  * 最高位 为什么要取出最高值,这里主要是通过计算动态的算出Y轴上的数值区间,
  * 比如心率是60-100,不计算写死就是0-180,这样折线的所有点就全部落在中间一点的地带,上下都有较大的空白,影响美观(心率一般在60-100之间)
  * 比如计步的幅度很大,如果不通过动态计算就不知道Y轴画的数值给多少合适,比如Y轴数值写死为0-20000,
  * 那么如果运动量偏少,比如都是1000步左右,折线就显得几乎和X=0平齐了
  * @param num
  * @return
  */
 private int getResultNum(float num)
 {
  int resultNum;
  int gw = 0; // 个位
  int sw = 0; // 十位
  int bw = 0; // 百位
  int qw = 0; // 千位
  int ww = 0; // 万位

  if (num > 0)
  {
   gw = (int) (num % 10 / 1);
  }
  if (num > 10)
  {
   sw = (int) (num % 100 / 10);
  }

  if (num > 100)
  {
   bw = (int) (num % 1000 / 100);
  }

  if (num > 1000)
  {
   qw = (int) (num % 10000 / 1000);
  }

  if (num > 10000)
  {
   ww = (int) (num % 100000 / 10000);
  }
  /*********************************/
  if (ww >= 1)
  {
    resultNum=qw>5? ww * 10000 + 10000: ww * 10000 + 5000;
  }
  else if (qw >= 1)
  {
   resultNum=bw>5?qw*1000+1000:qw*1000+500;
  }
  else if (bw >= 1)
  {
   resultNum = bw * 100 + sw * 10 + 10;

  }
  else if (sw >= 1)
  {

   resultNum=gw>5?sw * 10 + 20:sw * 10 + 10;
  }
  else
  {
   resultNum = 0;
  }

  return resultNum;
 }

上面的代码显然是统一加上了某个数值,这个数值可以根据你的项目需求自己定义,但取下限的时候显然就要减去某个数值:具体为什么要这么做注释写得比较详细。

真正意义上的计算Y轴上数值刻度了:

/**
  * 传入数组中的最大值和最小值,计算出在Y轴上数值的区间
  * 
  * @param max
  * @param min
  * @return
  */
 private int[] cacluterYValues(float max, float min)
 {
  int[] values;
  int min1;
  int max1;
  int resultNum = getResultNum(min); // 计算出的最小值
  max1 = getResultNum(max); // 计算出最大值
  if (resultNum <= 20) // 如果小于等于20 就不要减20,否则Y最小值是0了
  {
   min1 = resultNum - 10;
  }
  else
  {

   min1 = resultNum - 20;
  }

  if (resultNum <= 10 || resultNum == 0) // 如果小于10 就不用再减了,否则就是负数了
  {
   min1 = 0;
  }

  // 将计算出的数值均分为5等分
  double ceil = Math.ceil((max1 - min1) / 4);
  values = new int[]
  { min1, (int) (min1 + ceil), (int) (min1 + ceil * 2),
    (int) (min1 + ceil * 3), (int) (min1 + ceil * 4) };
  return values;

 }

这样就计算出来了Y轴需要动态画的数值。

接下来就开始画吧:模拟数据的代码这里就不贴了,后面会给出整个项目的源码,感兴趣的自己看看就懂了。

/**
  * 画Y轴上的数值
  * 
  * @param canvas
  */
 private void drawYValues(Canvas canvas, float max, int[] value)
 {
 //这里除以max这个最大值是为了有多大的去见就分成多少等分,是的后面折线的点更精准,否者就会对不齐刻度,
  float YScale = ((float) mHeight - getPaddingBottom() - getPaddingTop()
    - 40) / max;
  for (int i = 0; i < value.length; i++)
  {
   String text = value[i] + "";
   int scale = value[i] - value[0];
   canvas.drawLine(getPaddingLeft(),
     mHeight - getPaddingBottom() - (YScale * scale),
     getPaddingLeft() + 10,
     mHeight - getPaddingBottom() - (YScale * scale), textPaint);
   textPaint.getTextBounds(text, 0, text.length(), textBound);
   // +textBound.height()/2 主要是为了让字体和间断线居中
   canvas.drawText(text,
     getPaddingLeft() - 40, mHeight - getPaddingBottom()
       - (YScale * scale) + textBound.height() / 2,
     textPaint);
  }

 }

效果图:


这里写图片描述

显然,画线的逻辑并不复杂,只是计算Y轴上的值花了一定精力。

现在画折线了:

1.首先画出小圆点,然后将各个小圆点收尾相连接就是折线效果了:

 private void drawLine(Canvas canvas, float arraymax, float yMin)
 {

  //这里是整个Y轴可用高度除以最大值,就是每个值占有刻度上的几等分;
  float YScale = ((mHeight - getPaddingBottom() - getPaddingTop() - 40))/ arraymax;
  for (int i = 0; i < values.length; i++)
  {
   //为什么是values[i] - arraymin(数据值-Y坐标最小值)? 
   //因为圆点是以数据值来画得,数据值和Y轴坐标最小值的差就是整个数据的区间;
   int scale = (int) (values[i] - yMin);

   int j;
   /**
    * 画折线
    */
   if (i < 6)
   {
    int textScale = (int) (values[i + 1] - yMin);
    j = i + 1;
    canvas.drawLine(getPaddingLeft() + (XScale * i),
      mHeight - getPaddingBottom() - (YScale * scale),
      getPaddingLeft() + (XScale * j),
      mHeight - getPaddingBottom() - (YScale * textScale),
      linePaint);
   }

   String text = String.valueOf(values[i]);
   textPaint.getTextBounds(text, 0, text.length(), textBound);
   canvas.drawText(text,
     getPaddingLeft() + (XScale * i) - textBound.width() / 2,
     mHeight - getPaddingBottom() - (YScale * scale) - 15,
     textPaint);

   /**
    * 两个小圆点
    */
   canvas.drawCircle(getPaddingLeft() + (XScale * i),
     mHeight - getPaddingBottom() - (YScale * scale), 10,
     maxCirclePaint);
   canvas.drawCircle(getPaddingLeft() + (XScale * i),
     mHeight - getPaddingBottom() - (YScale * scale), 10 - 2,
     minCirclePaint);

  }

 }

注意上面的arraymax yMin两个值的含义。arraymax一定是Y轴上区间的差值,比如轴上的数组为[60,70,80,90,100],那么arrayma就是100-60;yMin见注释。
这里为什么要画两个圆?两个同心圆能够达到大圆是空心的效果,那画笔设置为STROKE不就行了?


这里写图片描述

看到了吧,感觉从圆中间穿过去了,是不是觉得不爽啊,于是有人就说,我把圆的半径算出来就行了,画线的时候减去这个半径,哥哥,如果前后两点不在同一直线上你还得算夹角,你慢慢算吧。算好了告诉我!

这里写图片描述
 是不是美观很多啊?骚年?

这里的工作基本就完了,至于睡眠要画两条线,获取不同的数据 调用两次画圆点和线的方法就OK了。
至于代码里如果觉得部分逻辑混乱冗余,那就将就一下吧。

最后附上源码地址:源码下载(https://git.oschina.net/xy001/anroidwatchtable.git)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容