Android 自定义 View - 折线图

自定义折线图的需求在 APP 开发中好像很常见了,事实上用一些第三方库来绘制折线图很容易实现,但感觉一个类能搞定的事情去接入一个库好像有点不和谐啊。本着学习的目的,绘制了一个折线图,下面上效果图:

效果图.png

我的自定义 view

  • 1.新建 Zhexiantu 继承之 View ,重写下面两个构造函数,并在里面做一些初始化操作。
public Zhexiantu(Context context) {
        super(context);
        this.mContext = context;
        init();
    }

 public Zhexiantu(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        this.mContext = context;
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.Zhexiantu);
        myBg = array.getColor(R.styleable.Zhexiantu_my_bg, 0xFFF7F7F7);  //默认值必须
        linechartColor = array.getColor(R.styleable.Zhexiantu_linechart_color,0xFF90E7FD);
        linechartSize = (int) array.getDimension(R.styleable.Zhexiantu_linechart_size,3);
        linechartBg = array.getColor(R.styleable.Zhexiantu_linechart_bg,0xFFFFFFFF);
        padding = (int) array.getDimension(R.styleable.Zhexiantu_linechart_padding,16);

        init();
        array.recycle(); //取完值,记得回收
    }

第二个构造函数中相比第一个构造函数多了一些自定义属性取值的操作,自定义属性需要在 res/values 文件夹下的 styles.xml 文件下定义属性,eg:

<!--自定义属性-->
    <declare-styleable name="Zhexiantu">
        <attr name="my_bg" format="color"/>
        <attr name="linechart_color" format="color"/>
        <attr name="linechart_size" format="dimension"/>
        <attr name="linechart_bg" format="color"/>
        <attr name="linechart_padding" format="dimension"/>
    </declare-styleable>

以上两步都完成就可以在我们自己的自定义控件上使用自定义属性了,eg:

  <com.lisheny.lenovo.myviewstudy.canvas.Zhexiantu
            android:id="@+id/zhexiantu"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:linechart_color="@color/color2"
            app:linechart_bg="@color/color13"/>

另外,附上 Paint 属性设置的详解 http://wuxiaolong.me/2016/08/20/Paint/

  • 2.重写 onMeasure 方法,计算获取画布的宽高并计算尺 X、Y轴的区间间隔
 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //这里的 mode 只使用了宽的值,要求严谨的活应该宽高都应该考虑进去,比如:
//        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
//        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
//        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
//        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
//        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
//        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
//        }

        int mode = MeasureSpec.getMode(widthMeasureSpec);
        switch (mode) {
            case MeasureSpec.AT_MOST:                             //(wrap_content)
                mWidth = MeasureSpec.getSize(widthMeasureSpec);
                mHeigt = 3 * mWidth / 5;
                break;
            case MeasureSpec.EXACTLY:                             //固定尺寸(如100dp、match_parent),
                mWidth = MeasureSpec.getSize(widthMeasureSpec);
                mHeigt = MeasureSpec.getSize(heightMeasureSpec);
                break;
            case MeasureSpec.UNSPECIFIED:                         //比重 (layout_weight="1")
                mWidth = MeasureSpec.getSize(widthMeasureSpec);
                mHeigt = 3 * mWidth / 5;
                break;
        }

        //X轴区间间隔
        xDistance = (mWidth - 4 * dp2px(padding)) / (xText.length - 1);
        //Y轴区间间隔
        yDistance = (mHeigt - 2 * dp2px(padding)) / (yText.length - 1);
        setMeasuredDimension((int) mWidth, (int) mHeigt);
    }

通过 int mode = MeasureSpec.getMode(widthMeasureSpec); 获取测量模式,根据不同的测量模式将宽高设置为合理的相应值。关于这三种测量模式的介绍:

  1. UNSPECIFIED ==》父容器没有对当前View有任何限制,当前View可以任意取尺寸
  2. EXACTLY ==》当前的尺寸就是当前View应该取的尺寸
  3. AT_MOST ==》当前尺寸是当前View能取的最大尺寸

理解否?不理解没事,我们知道什么情况下调用什么模式就行:

  1. xml 中 设置 wrap_content 时,会走 AT_MOST 模式;
  2. xml 中 设置 固定尺寸(如100dp) 时,会走 EXACTLY 模式;
  3. 这种使用比较少,xml 中 设置 比重 layout_weight="1" 时,会走 EXACTLY 模式;

注:onMeasure 函数会多次调用

  • 3.接下来就是重写 onDraw 函数绘制 View
@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //绘制画布颜色
        canvas.drawColor(myBg);

        drawTextAndGrid(canvas, textPaint, gridPaint);
        drawLineChart(canvas);
        drawDots(canvas);
    }

绘制步骤:

  1. 绘制画布颜色,背景
  2. 绘制X、Y轴的坐标值和表格
  3. 绘制折线
  4. 绘制小圆点
  5. 完成!!!

特别的:计算Y轴的刻度

 /**
     * 根据输入数据值得到对应Y轴坐标
     *
     * 这里需求:Y轴坐标增长 随屏幕自上而下增加  注:变量尽量用浮点型,避免精度丢失
     * 公式:Y = Y轴开始值 + 每份坐标刻度所占Y轴长度的量{Y轴长度/总刻度} * 当前刻度值{value-min}
     * 即:Y = Y轴开始值{ dp2px(padding) + dp2px(5)} + 每份坐标刻度所占Y轴长度的量{(yDistance * (yText.length - 1)) / (yText[yText.length-1]-yText[0])} * 当前刻度值{(value - yText[0])}
     *
     * @param value
     * @return
     */
    private float getYcoordinate(float value) {
        return dp2px(padding) + dp2px(5) + (yDistance * (yText.length - 1)) / (yText[yText.length - 1] - yText[0]) * (value - yText[0]);
    }

其中:总刻度 = 最大Y刻度值(max) - 最小Y刻度值(min)

  • 最后:折线图的绘制总的来说很简单,主要点是载入数据的刻度值要和Y轴相对应,理解计算出Y轴坐标的公式,折线图的绘制就简单了,以上代码不全,若要查看完成 Demo ,点击此处传送。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,992评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,212评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,535评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,197评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,310评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,383评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,409评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,191评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,621评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,910评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,084评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,763评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,403评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,083评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,318评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,946评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,967评论 2 351

推荐阅读更多精彩内容

  • 拿到效果图,手机端需要实现的效果是这样: 先分析:x轴是时间轴,长度固定,区间不固定;y轴是数值轴,区间不固定,需...
    Vonelone阅读 988评论 0 5
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,932评论 25 707
  • 瓶子部分画的有点糟。
    海里一只虾阅读 208评论 0 1
  • 153days 什么表情。 哪里学的~这么搞笑,这么可爱~ 以前我娘看了我搞怪的表情就开始瞪我和骂我了…… 我想看...
    sueva阅读 220评论 0 0
  • 起风了 有一捧蝴蝶飘上了岸 天空在一边呆着 堆着闲云 三两座坟茔 紧挨着 却再没有往来 一头老狗,吸着雪茄 托着天...
    江西黄小军阅读 283评论 1 4