自己定义View之Chart图标系列(1)——点阵图

近期要做一些图表类的需求,一開始就去github上看了看,发现开源的图表框架还是蛮多的。可是非常少有全然符合我的需求的。另外就是使用起来比較麻烦。所以就决定自己来造轮子了~~~

今天要介绍的就是Android图标系列中点阵图(姑且这么叫着吧╮(╯▽╰)╭)的画法。

效果图例如以下:

需求:

1. 给出几个点 画出坐标轴(用虚线)

2. 画出相应的点 在点的上方标出数值

3. 下方要显示个数值表示的意义

4. 重点!!

!动态计算坐标轴,多余的坐标不能显示。

前三条好理解。第四条啥意思呢~

(比方说,我们数据是{10.1,12.5, 20.2, 15.1} 那么我们的坐标轴起点就应该是从10開始画,而不是0!)例如以下图所看到的

接下来我们就来一步步实现这些需求吧~

1.定义数据模型

**
 * Created by JK on 2016/1/18.
 * 点阵折线图的数据模型
 */
public class DotChartItem {
    public int color;//颜色
    public float value;//值
    public String title;//标题

    public DotChartItem(int color, float value, String title) {
        this.color = color;
        this.value = value;
        this.title = title;

    }

}

2.自己定义View —— JKLineChart

首先是初始化部分

/**
 * Created by JK on 16/1/18.
 * 用毛的开源项目自己写控件之点阵图
 * 这是一个点阵图
 */
public class JKLineChart extends View {

    private static final String TAG = "JKLineChart";
    private Context mContext;
    private Paint mCoordPaint;//坐标线的Paint
    private Paint mIndicPaint;  //点图的Paint
    private Paint mHintTitlePaint; //提示块的Paint
    private int mWidth;
    private int mHeight;
    private int mCoordColor = Color.GRAY; //坐标线的color
    private int mPadding = 20;
    private int mGapHeight = 50;//坐标轴之间的间隔
    private float mTextX = 0f; //提示模块中。第一个方块的X坐标(和坐标上的字分离)
    private float[] mMinAndMaxPointY = new float[2]; //最低点和最高点的Y坐标(參考点 用来计算方块的坐标)
    private List<LineChartItem> mItemList; //数据集合

    private List<LineChartItem> mTypeSet;//下方提示模块的数据集合
    private float[] values;

    enum IndicatorType {  //点阵图 图形类型
        CIRCLE, //圆
        RECTANGLE //方块
    }

    private IndicatorType mIndicatorType = IndicatorType.RECTANGLE;
    private int mGap = 5; //坐标轴之间的间隔
    private int mIndicWidth = 5; //小方块的宽度 或者小圆圈的直径
    //以下2个值用来做动画
    private int mInitWidth = 0;
    private int mSpeed = 1;

    private boolean isShowHintBlock = true; //是否显示底下的提示模块
    private int mHintTitleHeight = 30; //提示模块高度
    private final double EP = 0.000000001;

    public JKLineChart(Context context) {
        this(context, null);
    }

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

    public JKLineChart(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.JKLineChart, 0, 0);
        try {
            isShowHintBlock = typedArray.getBoolean(R.styleable.JKLineChart_showHintBlock, true);
            //坐标轴之间的间隔 默觉得5
            mGap = typedArray.getInt(R.styleable.JKLineChart_coordGap, mGap);
            //点是用圆圈还是用方块来画
            int isRound = typedArray.getInt(R.styleable.JKLineChart_indicatorType, 1);
            Log.d(TAG, "ROUND :" + isRound);
            if (isRound == 0) {
                mIndicatorType = IndicatorType.CIRCLE;
            } else {
                mIndicatorType = IndicatorType.RECTANGLE;
            }

        } finally {
            typedArray.recycle();
        }

        init(context);
    }

上面的操作主要是定义一些变量。并从构造函数中获取我们的自己定义属性

 private void init(Context context) {
        this.mContext = context;
        mItemList = new ArrayList<LineChartItem>();
        //mPadding = dip2px(context,mPadding);
        mIndicWidth = dip2px(context, mIndicWidth);
        mHintTitleHeight = dip2px(context, 40);
        mGapHeight = dip2px(context,mGapHeight);
        initPaint();
    }
    //初始化画笔
    private void initPaint() {
        mIndicPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mCoordPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mHintTitlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mCoordPaint.setColor(mCoordColor);
        mCoordPaint.setTextSize(40);
    }
    @Override
    protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
        this.mWidth = width;
        this.mHeight = height;
        if (isShowHintBlock) {
            mHeight = mHeight - mHintTitleHeight;
        }
        Log.d(TAG, "onSizeChanged");

    }

mHeight 代表的是点阵图的高度(不包含下方的提示部分)。假设要显示下方的提示部分,就要减去提示部分的高度。

以下就是最重点。最基本的onDraw方法喽

private void animatedDraw(Canvas canvas) {
        if (mItemList == null || mItemList.size() == 0)
            return;

        drawCoordText(canvas);
        drawPoint(canvas);
        if (isShowHintBlock) {
            drawHintTitle(canvas);
        }

    }

分三步走,

第一 drawCoordText画坐标线

第二 drawPoint画点

第三 drawHintTitle画下方提示部分

/**
     * 画坐标线
     * @param canvas
     */
    private void drawCoordText(Canvas canvas) {
        //获取坐标上下限的值
        float[] array = getMinAndMaxCoord();
        //坐标轴总数量
        float totalCount = (array[1] -array[0])/ mGap +1;
        for (float i = array[1], count = 0; i >= array[0]; i = i - mGap, count++) {
            String text = (int) i + "";
            Rect textRect = new Rect();
            mCoordPaint.getTextBounds(text, 0, text.length(), textRect);
            float y = mPadding*3 + mGapHeight*count;
            if (count == 0) {
                mMinAndMaxPointY[1] = y; //最高点的坐标
            } else if (count == totalCount - 1) {
                mMinAndMaxPointY[0] = y; //最低点的坐标
            }
            canvas.drawText(text, mPadding, y, mCoordPaint);
            //canvas.drawLine(mPadding,y,mWidth,y,mCoordPaint);
            //画虚线坐标线
            PathEffect effects = new DashPathEffect(new float[]{5, 10}, 1);
            Path path = new Path();
            path.moveTo(mPadding * 2f + textRect.width(), y - textRect.height() / 2);
            if (mTextX >= -EP && mTextX <= EP) {
                mTextX = mPadding * 2f + textRect.width();
            }
            path.lineTo(mWidth - mPadding, y - textRect.height() / 2);
            mCoordPaint.setStyle(Paint.Style.STROKE);
            mCoordPaint.setPathEffect(effects);
            canvas.drawPath(path, mCoordPaint);

        }
    }

这里有两个辅助函数非常重要!

。!堪称画坐标线的核心!

前面提到过,我们的坐标轴是依据传入的数值动态计算的,而不是直接从0開始画的。

 private float[] getMaxAndMin(float[] array) {
        float min, max;
        min = max = array[0];
        float[] result = new float[2];
        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i] + " ");
            if (Float.compare(array[i], max) > 0)   // 推断最大值
                max = array[i];
            if (Float.compare(array[i], min) < 0)   // 推断最小值
                min = array[i];
        }
        result[0] = min;
        result[1] = max;
        return result;
    }

上面这个函数用来获取一个数组中的最大最小值,不管数组是否排序,这个比較简单。

/**
     * 动态计算最小坐标值和最大坐标值x
     * ps:{4.9f,6f,7f,8f,9.1f}  坐标最小值为  = 0; 坐标最大值 = 10;
     *
     * @return
     */
    private float[] getMinAndMaxCoord() {
        float[] array = getMaxAndMin(values);
        float[] result = new float[2];
        float min = array[0];
        float max = array[1];
        result[0] = (Math.round((min - 0.5f)) / mGap * mGap); //坐标值最小值
        result[1] = ((int) (floor((max + mGap - 0.5f)) / mGap) * mGap); //坐标值最大值
        return result;
    }

上面这个函数用来计算坐标轴的最小值和最大值,

比方数组是{4.9f,6f,7f,8f,9.1f} 坐标最小值为 = 0; 坐标最大值 = 10;

数组是{5.1f,6f,7f,8f,10.1f} 坐标最小值为 = 5; 坐标最大值 = 15;

floor是我们重写了地板除法

 /**
     * 重写floor除法
     *
     * @param value
     * @return
     */
    private float floor(float value) {
        float result = 0f;
        float tail = value % 1;
        int integer = (int) (value - tail);
        if (Float.compare(tail, 0.5f) > 0) {

            return integer + 1;
        } else if (Float.compare(tail, 0.5f) <= 0) {

            return integer;
        }
        return result;
    }

第二步: 画点

 /**
     * 画小方块
     *
     * @param canvas
     */
    private void drawPoint(Canvas canvas) {
        float totalWidth = mWidth - 2 * mPadding - mTextX;  //全部方块所能占领的总面积
        int size = values.length;
        for (int i = 0; i <size; i++) {
            float value = values[i];
            int color = mItemList.get(i).color;
            mIndicPaint.setColor(color);
            mIndicPaint.setTextSize(40);
            String text = value + "";
            Rect textRect = new Rect();
            mIndicPaint.getTextBounds(text, 0, text.length(), textRect);
            if (mIndicatorType == IndicatorType.RECTANGLE) {
                float top = getPointYByValue(value);
                float left = mPadding + mTextX + totalWidth / size * i + (totalWidth / size - mInitWidth) / 2;
                float right = left + mInitWidth;
                float bottom = top + mInitWidth;
                RectF indicRect = new RectF(left, top, right, bottom);
                canvas.drawText(text, left, top - textRect.height(), mIndicPaint);
                canvas.drawRect(indicRect, mIndicPaint);
            }
        }
        if (mInitWidth <= mIndicWidth) {
            mInitWidth += mSpeed;
            invalidate();
        }
    }

mTextX是右側方块区域的起始坐标。用来与坐标数字分离,totalWidth是全部点所能占领的宽度,依据点的数量将totalWidth等分,然后居中显示在各自的区域里。

这里我们仅仅实现了点为方块的样式。点为圆圈的样式各位看官能够自己尝试去实现以下。

getPointYByValue函数比較重要。是依据点的值来计算坐标的。



 /**
     * 依据值来计算方块的y坐标
     *
     * @param value
     * @return
     */
    private float getPointYByValue(float value) {
        float[] array = getMinAndMaxCoord();
        float diffY = mMinAndMaxPointY[0] - mMinAndMaxPointY[1]; //坐标值之间的差值
        float diffValue = array[1] - array[0];
        float y = mMinAndMaxPointY[1] + diffY/diffValue* (array[1]-value)-mIndicWidth*3/2;
        return y;
    }

最后 就是画底部的提示区域了

 public void getHintTitleList() {
        mTypeSet = new ArrayList<LineChartItem>(mItemList);
        for (int i = 0; i < mTypeSet.size() - 1; i++) {
            for (int j = mTypeSet.size() - 1; j > i; j--) {
                if (mTypeSet.get(j).title.equals(mTypeSet.get(i).title)) {
                    mTypeSet.remove(j);
                }
            }
        }
        Collections.sort(mTypeSet, new Comparator<LineChartItem>() {
            @Override
            public int compare(LineChartItem lhs, LineChartItem rhs) {
                return -(Float.compare(lhs.value, rhs.value));
            }
        });
    }

    private void drawHintTitle(Canvas canvas) {
        getHintTitleList();
        int totalWidth = mWidth - mPadding * 2;
        int width = totalWidth / mTypeSet.size();
        float startY = mMinAndMaxPointY[0] + mPadding * 3;
        for (int i = 0; i < mTypeSet.size(); i++) {
            //draw
            LineChartItem type = mTypeSet.get(i);
            String text = type.title;
            int color = type.color;
            mHintTitlePaint.setColor(color);
            mHintTitlePaint.setTextSize(40);
            Rect textRect = new Rect();
            mHintTitlePaint.getTextBounds(text, 0, text.length(), textRect);
            float x = (width - (mIndicWidth*2 + mPadding + textRect.width())) / 2 + i * width;
            RectF indicRect = new RectF(x + mPadding, startY, x + mIndicWidth*2 + mPadding, startY + mIndicWidth*2);
            canvas.drawRect(indicRect, mHintTitlePaint);
            mHintTitlePaint.setColor(mCoordColor);
            canvas.drawText(text, mPadding * 3 + x, startY + mIndicWidth*2, mHintTitlePaint);
        }

    }

getHintTitleList将我们传入的数组筛选出提示标题,并依照数值排序。

最最后~!

!。,因为我们的坐标可能有非常多。0-——无穷都有可能,所以我们自己定义View的高度不是固定的,须要动态计算出来。

public void setItemList(List<LineChartItem> list) {
        if(list.size()==0 || list == null)
            return;
        mItemList.clear();
        mItemList.addAll(list);
        int size = mItemList.size();
        values = new float[mItemList.size()];
        for (int i = 0; i < size; i++) {
            values[i] = mItemList.get(i).value;
        }
        computerHeight();
        invalidate();
    }

    private void computerHeight() {
        float[] array = getMinAndMaxCoord();
        //坐标轴总数量
        float totalCount = (array[1] -array[0])/ mGap  ;
        //上面折线图的最大Y坐标
        float y = mPadding*3 + mGapHeight*totalCount;
        //以下提示部分的起始Y坐标
        float startY = y + mPadding * 3;
        int height = (int) (startY+mIndicWidth*2+mPadding*2);
        ViewGroup.LayoutParams lp = getLayoutParams();
        lp.height = height;
        this.setLayoutParams(lp);
        Log.d(TAG,"HEIGHT:"+height);

    }

每次我们填充时间的时候,都会先调用computerHeight来动态计算视图高度

使用方式 SO EASY~!

 LineChartItem item1 = new LineChartItem(Color.parseColor("#00ff00"), 12.5f, "正常");
        LineChartItem item2 = new LineChartItem(Color.parseColor("#0000ff"), 10.1f, "偏低");
        LineChartItem item3 = new LineChartItem(Color.parseColor("#FF0000"), 20.2f, "偏高");
        LineChartItem item4 = new LineChartItem(Color.parseColor("#FF0000"), 15.1f, "偏高");
        List<LineChartItem> list = new ArrayList<LineChartItem>();
        list.add(item1);
        list.add(item2);
        list.add(item3);
        list.add(item4);
        mLineChart.setItemList(list);

你仅仅用将数据传递给LineChart 。剩下的工作就会自己主动完毕了,就是这么简单,全然没有网上那些开源控件用起来那么复杂吧~~

源代码 我的github:https://github.com/devilthrone/JKChart

欢迎fork and starO(∩_∩)O

OVER!

时间: 2024-10-27 08:24:04

自己定义View之Chart图标系列(1)——点阵图的相关文章

自定义View之Chart图标系列(1)——点阵图

最近要做一些图表类的需求,一开始就去github上看了看,发现开源的图表框架还是蛮多的,但是很少有完全符合我的需求的,另外就是使用起来比较麻烦,所以就决定自己来造轮子了~~~ 今天要介绍的就是Android图标系列中点阵图(姑且这么叫着吧╮(╯▽╰)╭)的画法. 效果图如下: 需求: 1. 给出几个点 画出坐标轴(用虚线) 2. 画出对应的点 在点的上方标出数值 3. 下方要显示个数值表示的意义 4. 重点!!!!动态计算坐标轴,多余的坐标不能显示. 前三条好理解,第四条啥意思呢~ (比如说,我

Path类的最全面具体解释 - 自己定义View应用系列

前言 自己定义View是Android开发人员必须了解的基础:而Path类的使用在自己定义View绘制中发挥着很关键的数据 网上有大量关于自己定义View中Path类的文章.但存在一些问题:内容不全.思路不清晰.简单问题复杂化等等 今天.我将全面总结自己定义View中Path类的使用,我能保证这是市面上的最全面.最清晰.最易懂的 文章较长,建议收藏等充足时间再进行阅读 阅读本文前请先阅读自己定义View基础 - 最易懂的自己定义View原理系列 文件夹 1. 简单介绍 定义:路径.即无数个点连起

Android画图系列(二)——自己定义View绘制基本图形

这个系列主要是介绍下Android自己定义View和Android画图机制.自己能力有限.假设在介绍过程中有什么错误.欢迎指正 前言 在上一篇Android画图系列(一)--自己定义View基础中我们了解自己定义View相关的基本知识.只是,这些东西依然还是理论,接下来我们就实际绘制一些东西 在本篇文章中,我们先了解下面Canvas,而且画一些主要的图形 Canvas简单介绍 Canvas我们能够称之为画布.能够在上面绘制各种东西.是安卓平台2D图形绘制的基础.非常强大. 一般来说,比較基础的东

安卓自己定义View进阶-Canvas之绘制基本形状

Canvas之绘制基本形状 作者微博: @GcsSloop [本系列相关文章] 在上一篇自己定义View分类与流程中我们了解自己定义View相关的基本知识,只是,这些东西依然还是理论,并不能拿来(zhuang)用(B), 这一次我们就了解一些能(zhaung)用(B)的东西. 在本篇文章中,我们先了解Canvas的基本用法,最后用一个小演示样例来结束本次教程. 一.Canvas简单介绍 Canvas我们能够称之为画布,能够在上面绘制各种东西,是安卓平台2D图形绘制的基础,非常强大. **一般来说

自己定义 View 基础和原理

课程背景: 在 Android 提供的系统控件不能满足需求的情况下,往往须要自己开发自己定义 View 来满足需求,可是该怎样下手呢.本课程将带你进入自己定义 View 的开发过程,来了解它的一些原理. 核心内容: 1.编写自己的自己定义 View 2.增加逻辑线程 3.提取和封装自己定义 View 4.利用 xml 中定义样式来影响显示效果 编写自己的自己定义 View(上) 本课时主要解说最简单的自己定义 View,然后增加绘制元素(文字.图形等),而且能够像使用系统控件一样在布局中使用.

Android 它们的定义View它BounceProgressBar

转载请注明出处:http://blog.csdn.net/bbld_/article/details/41246247 [Rocko's blog] 之前几天下载了非常久没用了的桌面版酷狗来用用的时候,发现当中载入歌曲的等待进度条的效果不错(个人感觉).例如以下: 然后趁着这周末两天天气较冷,窝在宿舍放下成堆的操作系统作业(目測要抄一节多课的一堆堆文字了啊...啊..)毅然决定把它鼓捣出来,终于的效果例如以下(总感觉有点不和谐啊·): 对照能看出来的就是多了形状的选择还有使用图片了.那么接下来就

【Android】自己定义View

翻译自:http://developer.android.com/training/custom-views/index.html 一)创建view类 一个设计良好的自己定义view与其它的类一样.它使用接口来封装一系列的功能.有效的使用CPU和内存等.除了这些,定制view还应该满足例如以下条件: 符合Android标准 与Android XML 布局文件配合,提供符合style风格的定制属性 发送易接近性事件(accessibility events,针对听力或视觉有缺陷用户提供方便的事件)

手把手带你画一个 时尚仪表盘 Android 自己定义View

拿到美工效果图.咱们程序猿就得画得一模一样. 为了不被老板喷,仅仅能多练啊. 听说你认为前面几篇都so easy,那今天就带你做个相对照较复杂的. 转载请注明出处:http://blog.csdn.net/wingichoy/article/details/50468674 注意:每一篇博客都是建立在之前博客的基础知识上的,假设你刚接触自己定义view.能够来说说自己定义view简单学习的方式这里看我曾经的文章.记录了我学习自己定义view的过程,并且前几篇博客或多或少犯了一些错误(反复绘制,o

【Android】自己定义View、画布Canvas与画笔Paint

安卓自己定义View事实上非常easy. 这个View能够像<[Android]利用Java代码布局,button加入点击事件>(点击打开链接)一样.利用Java代码生成一系列的组件. 也能够配合画布Canvas与画笔Paint来使用. 以下用一个样例来说明.例如以下图,有一个自己定义布局View.里面摆放着,利用画布Canvas与画笔Paint绘制出来的蓝色正方形与红色文字. 在res\layout\activity_main.xml中.直接像摆放安卓固有组件一样,能够直接使用这个我定义组件