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

手把手教你实现Android中智能设备数据表格绘制_第1张图片

手把手教你实现Android中智能设备数据表格绘制_第2张图片

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

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);    }

手把手教你实现Android中智能设备数据表格绘制_第3张图片

接下来绘制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);        }    }

上图:
手把手教你实现Android中智能设备数据表格绘制_第4张图片

上面的逻辑和计算并不复杂,就是将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);        }    }

效果图:
手把手教你实现Android中智能设备数据表格绘制_第5张图片
显然,画线的逻辑并不复杂,只是计算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不就行了?
手把手教你实现Android中智能设备数据表格绘制_第6张图片
看到了吧,感觉从圆中间穿过去了,是不是觉得不爽啊,于是有人就说,我把圆的半径算出来就行了,画线的时候减去这个半径,哥哥,如果前后两点不在同一直线上你还得算夹角,你慢慢算吧。算好了告诉我!

手把手教你实现Android中智能设备数据表格绘制_第7张图片

                是不是美观很多啊?骚年?

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

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

更多相关文章

  1. Android之ListView滚动到底后自动加载数据
  2. 使用AsyncHttpClient框架来完成网络数据的请求
  3. Android聊天客户端Demo,开源了.基本功能都有,数据库,服务器都Ok
  4. [翻译]Android教程-保存数据-支持不同的平台版本
  5. Android——Json和Gson分别是什么,以及Json 数据的解析方法
  6. Android客户端和服务器交换数据的一种方案
  7. Android中Http传递参数json参数请求数据

随机推荐

  1. android系统中“关于设备”中android版本
  2. this android sdk requires android deve
  3. 【Android】Android取消EditText自动获取
  4. Android NDK学习记录(一)
  5. android应用程序键盘事件机制
  6. 报错You need to use a Theme.AppCompat
  7. 【android初级】之android布局属性详解
  8. Android Layout属性详解
  9. 布局技巧和列表控件
  10. Android ScrollView滚动条