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

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)
    @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/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

你可能感兴趣的:(折线统计图,条形统计图,扇形统计图(动画+重力感应阴影))