折线统计图,条形统计图,扇形统计图(动画+重力感应阴影)

chart 统计图

先说一下坐标轴吧
横轴纵轴是一条Path,长度和高度是按照控件宽度设定的,这样保证在不同手机上比例相同
包括箭头也是path,比较简单

    /**
     * 画坐标轴
     * @param canvas
     * @param paint
     */
    private void drawAxes(Canvas canvas, Paint paint) {
        //坐标轴
        Path path = new Path();//三角形
        path.moveTo(width/10, width/20);
        path.lineTo(width/10, width/2);
        path.lineTo(width*9/10, width/2);
        canvas.drawPath(path, paint);

        //向上箭头
        Path pathArrowTop=new Path();
        pathArrowTop.moveTo(width/10-width/60,width/20+width/60);
        pathArrowTop.lineTo(width/10, width/20);
        pathArrowTop.lineTo(width/10+width/60, width/20+width/60);
        canvas.drawPath(pathArrowTop, paint);

        //向下箭头
        Path pathArrowRight=new Path();
        pathArrowRight.moveTo(width*9/10-width/60, width/2-width/60);
        pathArrowRight.lineTo(width*9/10, width/2);
        pathArrowRight.lineTo(width*9/10-width/60, width/2+width/60);
        canvas.drawPath(pathArrowRight, paint);
    }

横纵坐标的数字是写死的几个值,还没有添加动态设置方法
纵轴数字的间距是把纵轴高度除以纵轴数字数量
起点为原点
注意要测量字体宽高

Rect rect = new Rect();
paintText.getTextBounds(textVertical[i], 0, textVertical[i].length(), rect);
int textWidth = rect.width();
int textHeight = rect.height();

X轴要减去字体宽度,Y轴要加上字体高度的一半,同时添加了一个间距textSpace,以保证字体不与坐标轴重叠,同时又处于当前数值所在纵轴位置的中心(横轴同理)

canvas.drawText(textVertical[i],width/10-textWidth-textSpace,width/2-(i+1)*width/10+textHeight/2,paintText);
    private String[] textVertical ={"100","200","300","400"};
    private String[] textHorizontal ={"0","1","2","3","4","5","6","7"};

    /**
     * 坐标轴汉字
     * @param canvas
     */
    private void drawText(Canvas canvas) {
        paintText.setTextSize(width/30);
        //横轴
        for (int i = 0; i < textHorizontal.length; i++) {
            Rect rect = new Rect();
            paintText.getTextBounds(textHorizontal[i], 0, textHorizontal[i].length(), rect);
            int textWidth = rect.width();
            int textHeight = rect.height();
            canvas.drawText(textHorizontal[i],width/10+i*width/10-textWidth/2,width/2+textHeight+textSpace,paintText);
        }

        //纵轴
        for (int i = 0; i < textVertical.length; i++) {
            Rect rect = new Rect();
            paintText.getTextBounds(textVertical[i], 0, textVertical[i].length(), rect);
            int textWidth = rect.width();
            int textHeight = rect.height();
            canvas.drawText(textVertical[i],width/10-textWidth-textSpace,width/2-(i+1)*width/10+textHeight/2,paintText);
        }
    }

核心部分,画折线数据(随机数,最大设定了400)
也是画一个path
起点为原点 (width/10, width/2)
所经各点的横坐标为 原点横坐标+第几个点横轴每个点间距 width(i+2)/10
纵坐标为 原点纵坐标 -(当前数值/100)*(width/10)
value为ValueAnimator当前值(动画)

for (int i = 0; i < randomNums.length; i++) {
          pathData.lineTo(width*(i+2)/10, width/2-value*randomNums[i]*width/1000);
      }
    /**
     * 画数据
     * @param canvas
     */
    private void drawData(Canvas canvas) {
        if(value==0) return;
        //根据重力数据画阴影
        paintData.setShadowLayer(5,gravityX*shadowWidth,-gravityY*shadowWidth,Color.LTGRAY);

        Path pathData=new Path();
        pathData.moveTo(width/10, width/2);
        for (int i = 0; i < randomNums.length; i++) {
            pathData.lineTo(width*(i+2)/10, width/2-value*randomNums[i]*width/1000);
        }
        canvas.drawPath(pathData, paintData);
    }

显示当前数值大大小,其位置是折线的拐点,同数据位置,注意宽高的加减,保证不遮挡拐点,并与拐点中心对其

    /**
     * 动态数值
     * @param canvas
     */
    private void drawNum(Canvas canvas) {
        paintNum.setTextSize(width/30);
        if(value==0) return;
        for (int i = 0; i < randomNums.length; i++) {
            Rect rect = new Rect();
            paintNum.getTextBounds((int)(value*randomNums[i])+"", 0, String.valueOf((int)(value*randomNums[i])).length(), rect);
            int textWidth = rect.width();
            int textHeight = rect.height();
            canvas.drawText((int)(value*randomNums[i])+"",width*(i+2)/10-textWidth/2, width/2-value*randomNums[i]*width/1000-textHeight/2,paintNum);
        }
    }

点击事件
ACTION_DOWN时判断点击位置与所有拐点的距离,如果距离小于限定范围则认为点击有效
记录点击位置 clickPosition=i ;
将可点击状态置为 isClick=true;
同时启动按下的动画 downAnim();

ACTION_MOVE时判断当前位置与ACTION_DOWN时的位置间距,如果距离大于限定值则取消按下动画并将可点击状态置为 isClick=false;

ACTION_UP时先判断可点击状态是否为true,如果为true则执行抬起水波纹动画

Util.getDistance(downX,downY,width*(i+2)/10,width/2-randomNums[i]*width/1000)<width/22
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                //获取屏幕上点击的坐标
                downX = event.getX();
                downY = event.getY();
                for (int i = 0; i < randomNums.length; i++) {
                    if(Util.getDistance(downX,downY,width*(i+2)/10,width/2-randomNums[i]*width/1000)<width/22){
                        clickPosition=i;
                        isClick=true;
                        downAnim();
                        return true;
                    }
                }
                break;
            case MotionEvent.ACTION_MOVE:
                float moveX=event.getX();
                float moveY=event.getY();
                if(Util.getDistance(moveX,moveY,downX,downY)>width/15){
                    if(valueAnimatorDowm.isRunning()){
                        valueAnimatorDowm.cancel();
                    }
                    downValue=2;
                    isClick=false;
                    invalidate();
                    return false;
                }
                break;
            case MotionEvent.ACTION_UP:
                if(mOnPointClickListener!=null&&isClick){
                    mOnPointClickListener.onPointClick(clickPosition,randomNums[clickPosition]);
                    rippleAmin();
                }
                return true;
        }
        return super.onTouchEvent(event);
    }

位置也为拐点坐标
设置RadialGradient用于中心渐变色
同时也设定了透明度
半径为 downValue*半径

    /**
     * 按下水波纹特效
     * @param canvas
     */
    private void drawDownRipple(Canvas canvas) {
        paintDown.setAlpha((int)(255*(1-downValue/2)));
        Shader mShader = new RadialGradient(width*(clickPosition+2)/10,width/2-randomNums[clickPosition]*width/1000,width/22,0x00878787,Color.GRAY,Shader.TileMode.CLAMP);
        paintDown.setShader(mShader);
        canvas.drawCircle(width*(clickPosition+2)/10,width/2-randomNums[clickPosition]*width/1000,downValue*width/22,paintDown);
    }

    /**
     * 抬起水波纹特效
     * @param canvas
     */
    private void drawRipple(Canvas canvas) {
        paintRipple.setAlpha((int)(255*(1-rippleValue)));
        Shader mShader = new RadialGradient(width*(clickPosition+2)/10,width/2-randomNums[clickPosition]*width/1000,width/22,0x00878787,Color.DKGRAY,Shader.TileMode.REPEAT);
        paintRipple.setShader(mShader);
        canvas.drawCircle(width*(clickPosition+2)/10,width/2-randomNums[clickPosition]*width/1000,rippleValue*width/22,paintRipple);
    }

动态阴影
不调用setGravity方法有默认静态阴影

    //方向数据
    private float gravityX=4;
    private float gravityY=-4;
    private float gravityZ=4;

    /**
     * 获取手机方向(为避免连续绘制UI,设置数据变化范围大于0.1才刷新UI)
     * @param x
     * @param y
     * @param z
     */
    public void setGravity(float x,float y,float z){
        if(Math.abs(x-gravityX)>0.1||Math.abs(y-gravityY)>0.1||Math.abs(z-gravityZ)>0.1){
            gravityX=x;
            gravityY=y;
            gravityZ=z;
            invalidate();
        }
    }

paintData.setShadowLayer(5,gravityX*shadowWidth,-gravityY*shadowWidth,Color.LTGRAY);

重力数据获取
在Activity的onCreate中

mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);

同时在其他生命周期里适时调用注册传感器监听与取消注册,避免软件后台调用监听,只在页面展示时监听即可

    @Override
    protected void onResume() {
        super.onResume();
        mSensorManager.registerListener(this,
                mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY),
                SensorManager.SENSOR_DELAY_GAME);
    }

    @Override
    protected void onStop() {
        // 程序退出时取消注册传感器监听器
        mSensorManager.unregisterListener(this);
        super.onStop();
    }

    @Override
    protected void onPause() {
        // 程序暂停时取消注册传感器监听器
        mSensorManager.unregisterListener(this);
        super.onPause();
    }

监听回调,此调用view的setGravity,让view刷新Ui

    @Override
    public void onSensorChanged(SensorEvent sensorEvent) {
        if(sensorEvent.sensor.getType() == Sensor.TYPE_GRAVITY){
            float gravityX=sensorEvent.values[0];
            float gravityY=sensorEvent.values[1];
            float gravityZ=sensorEvent.values[2];
            lineView.setGravity(gravityX,gravityY,gravityZ);
        }
    }

点击事件

    //点击
    public interface OnPointClickListener{
        void onPointClick(int position, int number);
    }

    public void setOnPointClickListener(OnPointClickListener listener){
        this.mOnPointClickListener=listener;
    }
            case MotionEvent.ACTION_UP:
                if(mOnPointClickListener!=null&&isClick){
                    mOnPointClickListener.onPointClick(clickPosition,randomNums[clickPosition]);
                    rippleAmin();
                }
                return true;

条形统计图原理与折线相同,不再描述,区别在于折线绘制了path路径,条形绘制了rect
细节区别前往GitHub查看Demo

    /**
     * 画数据
     * @param canvas
     */
    private void drawData(Canvas canvas) {
        //根据重力数据画阴影
        paintData.setShadowLayer(10,gravityX*shadowWidth,-gravityY*shadowWidth,Color.DKGRAY);
        for (int i = 0; i < randomNums.length; i++) {
            paintData.setColor(calculateColor((int)(value*randomNums[i])));
            canvas.drawRect(width*(i+2)/10-width/30,width/2-value*randomNums[i]*width/1000,width*(i+2)/10+width/30,width/2,paintData);
        }
    }

扇形统计图
重点在于每条数据的开始位置角度以及扫过的角度的计算

float startAngle=value*360*getSum(i-1)/sum;
float swipeAngle=value*360*randomNums[i]/sum;

    /**
     * 获取数据总数
     * @return
     */
    private int getSum(int position) {
        int sum=0;
        for (int i = 0; i <= position; i++) {
            sum=sum+randomNums[i];
        }
        return sum;
    }
    /**
     * 画扇形
     * @param canvas
     */

    private int sum=0;
    private RectF rectF;

    private void drawSwipe(Canvas canvas) {
        if(sum==0||value==0.0f) return;
        rectF=new RectF(width/5,width/5,width*4/5,width*4/5);
        //根据重力数据画阴影
        paint.setShadowLayer(10,gravityX*shadowWidth,-gravityY*shadowWidth,Color.DKGRAY);
        for (int i = 0; i < randomNums.length; i++) {
            paint.setColor(colors[i]);
            float startAngle=value*360*getSum(i-1)/sum;
            float swipeAngle=value*360*randomNums[i]/sum;
            canvas.drawArc(rectF,startAngle,swipeAngle,true,paint);
        }
    }

画百分比
位置在所表示扇形位置的中心与圆心连线上(三角函数计算)

float textX= (float) (width/2 + Math.sin(2*Math.PI/360*(90-360*(getSum(i-1)+randomNums[i]/2)/sum))*width*6/17-textWidth/2);
float textY= (float) (width/2 + Math.cos(2*Math.PI/360*(90-360*(getSum(i-1)+randomNums[i]/2)/sum))*width*6/17+textHeight/2);
    /**
     *百分比
     * @param canvas
     */
    private void drawText(Canvas canvas) {
        if(sum==0) return;
        paintText.setTextSize(width/30);
        paintText.setAlpha((int)(255*value));
        for (int i = 0; i < randomNums.length; i++) {
            Rect rect = new Rect();

            double f = (double)randomNums[i]/sum;
            BigDecimal b = new BigDecimal(f*100);
            double f1 = b.setScale(2,BigDecimal.ROUND_HALF_UP).doubleValue();
            String textPercent=(f1)+"%";
            paintText.getTextBounds(textPercent, 0, textPercent.length(), rect);
            int textWidth = rect.width();
            int textHeight = rect.height();
            float textX= (float) (width/2 + Math.sin(2*Math.PI/360*(90-360*(getSum(i-1)+randomNums[i]/2)/sum))*width*6/17-textWidth/2);
            float textY= (float) (width/2 + Math.cos(2*Math.PI/360*(90-360*(getSum(i-1)+randomNums[i]/2)/sum))*width*6/17+textHeight/2);
            canvas.drawText(textPercent,textX,textY,paintText);
        }
    }

GitHub地址 https://github.com/ZuoJinDong/Chart

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

推荐阅读更多精彩内容

  • 选择qi:是表达式 标签选择器 类选择器 属性选择器 继承属性: color,font,text-align,li...
    love2013阅读 2,301评论 0 11
  • 直方图主要用在数据图表,作为对比数据,用柱体高度的高低,形象直观地表达出来,往往与折线图配合使用,而折线图便于从众...
    理想是试阅读 933评论 0 0
  • 1、属性选择器:id选择器 # 通过id 来选择类名选择器 . 通过类名来选择属性选择器 ...
    Yuann阅读 1,614评论 0 7
  • 折线图是工作中使用最频繁的图表之一,大多数人对折线图的认识还是非常浅显的,接下来,来深入了解一下折线图。 一.什么...
    璞石阅读 13,510评论 0 14
  • 喜欢在雨中看世界,透过窗户,一丝微凉,看向窗外的世界,有花、有树、还有打伞的人儿。少了平时的喧嚣,有的只是雨打房子...
    良柳如烟阅读 583评论 0 2