博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android轻量的线性和百分比图表实现
阅读量:6414 次
发布时间:2019-06-23

本文共 11340 字,大约阅读时间需要 37 分钟。

一.写这篇文章的起因

目前github上有多个关于图表的框架,比如MPAndroidChart很好,但是很大,没必要因为一个小的图标让工程项目扩大很多,另外有些轻量级的框架,但是个人感觉都很难满足自己的需求,再者就算很好的框架,那也是别人的,只有自己动手写起来,了解前前后后的坑,自己才能成长,而且在写的过程,我们能发现更多的细节,比如绘制的时候内存分配的问题,Canvas直接绘制和通过Bitmap绘制等等,所以这篇文章的目的:

  • 1.是给大家提供自定义view绘制的思路
  • 2.滑动自定义view的部分区域怎么实现
  • 3.path动画绘制的实现
  • 4.熟悉canvas的api,总之能直接动手了,那就自定义view就通关了,所以就写这篇文章主要是鼓励大家多去实现。

二.实现出来的效果图

线性图标

三.线性图表实现的思路:

由于屏幕的宽度有限,所以我们一屏经过计算,最好显示的7个点,所以我们首先需要对我们的view宽度进行计算,首先拿到屏幕的宽度,然后再进行/7,得到每个间隔的宽度,然后乘以我们x的坐标点的个数,其中的onMeasure的方法:

int widthParentMeasureMode = MeasureSpec.getMode(widthMeasureSpec);        int widthParentMeasureSize = MeasureSpec.getSize(widthMeasureSpec);        int heightParentMeasureMode = MeasureSpec.getMode(heightMeasureSpec);        int heightParentMeasureSize = MeasureSpec.getSize(heightMeasureSpec);        int resultWidthSize = 0;        int resultHeightSize = 0;        int resultWidthMode = MeasureSpec.EXACTLY;//用来对childView进行计算的        int resultHeightMode = MeasureSpec.EXACTLY;        int paddingWidth = getPaddingLeft() + getPaddingRight();        int paddingHeight = getPaddingTop() + getPaddingBottom();        ViewGroup.LayoutParams thisLp = getLayoutParams();        switch (widthParentMeasureMode) {            //父类不加限制给子类            case MeasureSpec.UNSPECIFIED:                //这个代表在布局写死了宽度                if (thisLp.width > 0) {                    resultWidthSize = thisLp.width;                    resultWidthMode = MeasureSpec.EXACTLY;                } else {                    resultWidthSize = (int) (getYMaxTextWidth() + mXinterval * mXdots.length);                    resultWidthMode = MeasureSpec.UNSPECIFIED;                }                break;            case MeasureSpec.AT_MOST:                //这个代表在布局写死了宽度                if (thisLp.width > 0) {                    resultWidthSize = thisLp.width;                    resultWidthMode = MeasureSpec.EXACTLY;                } else if (thisLp.width == ViewGroup.LayoutParams.MATCH_PARENT) {                    resultWidthSize = Math.max(0, widthParentMeasureSize - paddingWidth);                    resultWidthMode = MeasureSpec.AT_MOST;                } else if (thisLp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {                    resultWidthSize = (int) (getYMaxTextWidth() + mXinterval * mXdots.length);                    resultWidthMode = MeasureSpec.AT_MOST;                }                break;            case MeasureSpec.EXACTLY:                //这个代表在布局写死了宽度                if (thisLp.width > 0) {                    resultWidthSize = Math.min(widthParentMeasureSize, thisLp.width);                    resultWidthMode = MeasureSpec.EXACTLY;                } else if (thisLp.width == ViewGroup.LayoutParams.MATCH_PARENT) {                    resultWidthSize = widthParentMeasureSize;                    resultWidthMode = MeasureSpec.EXACTLY;                } else if (thisLp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {                    resultWidthSize = (int) (getYMaxTextWidth() + mXinterval * mXdots.length);                    resultWidthMode = MeasureSpec.AT_MOST;                }                break;        }        switch (heightParentMeasureMode) {            //父view不加限制            case MeasureSpec.UNSPECIFIED:                //这个代表在布局写死了宽度                if (thisLp.height > 0) {                    resultHeightSize = thisLp.height;                    resultHeightMode = MeasureSpec.EXACTLY;                } else {                    resultHeightSize = (int) (getYMaxTextHeight() + mYvisibleNum * mYinterval + getXMaxTextHeight());                    resultHeightMode = MeasureSpec.UNSPECIFIED;                }                break;            case MeasureSpec.AT_MOST:                if (thisLp.height > 0) {                    resultHeightSize = heightParentMeasureSize;                    resultHeightMode = MeasureSpec.EXACTLY;                } else if (thisLp.height == ViewGroup.LayoutParams.MATCH_PARENT) {                    resultHeightSize = Math.max(0, heightParentMeasureSize - paddingHeight);                    resultHeightMode = MeasureSpec.AT_MOST;                } else if (thisLp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {                    resultHeightSize = (int) (getYMaxTextHeight() + mYvisibleNum * mYinterval + getXMaxTextHeight());                    resultHeightMode = MeasureSpec.UNSPECIFIED;                }                break;            case MeasureSpec.EXACTLY:                //这个代表在布局写死了宽度                if (thisLp.height > 0) {                    resultHeightSize = Math.min(heightParentMeasureSize, getMeasuredWidth());                    resultHeightMode = MeasureSpec.EXACTLY;                } else if (thisLp.width == ViewGroup.LayoutParams.MATCH_PARENT) {                    resultHeightSize = heightParentMeasureSize;                    resultHeightMode = MeasureSpec.EXACTLY;                } else if (thisLp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {                    resultHeightSize = (int) (getYMaxTextHeight() + mYvisibleNum * mYinterval + getXMaxTextHeight());                    resultHeightMode = MeasureSpec.AT_MOST;                }                break;        }        setMeasuredDimension(MeasureSpec.makeMeasureSpec(resultWidthSize, resultWidthMode),                MeasureSpec.makeMeasureSpec(resultHeightSize, resultHeightMode));复制代码

设置好了尺寸,我们就可以绘制界面,这里我们onDraw的时候,就依次绘制横线和竖线,在绘制横线的时候,将Y坐标的数字一起绘制上去,同理绘制竖线的时候,把x坐标的数字绘制上去,折线的画根据数字计算出坐标点,然后创建一个path,首先moveTo(firstX,firstY),然后lineTo下面的点就可以了,最后绘制上path,然而这样的话,我们在滑动的时候,会发现这个view都会跟着一起滚动了,那么我们怎样才能实现view的部分pinned呢?在这个时候,我们就需要先创建一个bitmap,将需要滑动的部分绘制到这个bitmap上去,然后bitmap在绘制到这个canvas上的时候,保持固定的位置就行了,好了再说就懵逼了,还是上代码吧:

float tempTableLeftPadding = getYMaxTextWidth();        if (mBitmap == null || mYNumCanvas == null) {            mBitmap = Bitmap.createBitmap((int) (getMeasuredWidth() - getYMaxTextWidth()), getMeasuredHeight(), Bitmap.Config.ARGB_8888);            mYNumCanvas = new Canvas(mBitmap);        }        mYNumCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);        mYNumCanvas.translate(mScrollPosX,0);//这段代码就是来实现滑动的操作        //绘制横线        for (int y = 0, size = mYdots.length; y < size; y++) {            String tempText = String.valueOf(mYdots[mYdots.length - 1 - y]);            mYNumCanvas.drawLine(0, (float) (mYinterval * y), (float) (mXdots.length * mXinterval), (float) (mYinterval * y), mXlinePaint);            canvas.drawText(tempText, getYMaxTextWidth() - mYNumPaint.measureText(tempText), getYMaxTextHeight() + (float) (mYinterval * y), mYNumPaint);        }        //绘制竖线        for (int x = 0, size = mXdots.length; x <= size; x++) {            mYNumCanvas.drawLine((float) (mXinterval * x), 0, (float) (mXinterval * x), (float) (mYinterval * mYvisibleNum), mXlinePaint);            if (x >= 1) {                String tempText = mXdots[x - 1];                mYNumCanvas.drawText(tempText, (float) (mXinterval * x) - mYNumPaint.measureText(tempText) / 2, (float) (mYvisibleNum * mYinterval + getYMaxTextHeight()), mYNumPaint);            }        }        if (isAnimationOpen)//是否需要开启动画绘制,这个后面会解释实现方式            mYNumCanvas.drawPath(mLineDrawPath, mLinePaint);        else            mYNumCanvas.drawPath(mLinePath, mLinePaint);        canvas.drawBitmap(mBitmap, tempTableLeftPadding, getYMaxTextHeight() / 2, null);复制代码

上面的mScrollPosX是根据手势监听类GestureDetector来获取的:

@Override    public boolean onTouchEvent(MotionEvent event) {        if (!isAnimationOpen || isDrawOver)            return mGestureDetector.onTouchEvent(event);        return super.onTouchEvent(event);    }复制代码

然而绘制了,我们感觉还缺少了什么,嗯,没错就是动画效果,这里我们用到通过的path绘制实现动画的方案,就是先通过PathMeasure得到path的长度,然后根据动画时间,通过ValueAnimator计算它在某个时刻的坐标,然后重新进行绘制path路径:

private void startPathAnim(long duration) {        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, mLineLength);        valueAnimator.setDuration(duration);        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {            @Override            public void onAnimationUpdate(ValueAnimator animation) {                float value = (Float) animation.getAnimatedValue();                // 获取当前点坐标封装到mCurrentPosition                mPathMeasure.getPosTan(value, mCurrentPosition, null);                mLineDrawPath.lineTo(mCurrentPosition[0], mCurrentPosition[1]);                invalidate();            }        });        valueAnimator.start();    }复制代码

四.百分比圆形图表实现

百分比图标
其实这个的实现,相比上一个少了很多,大多是集中在onDraw方法里面,关键点是在百分比的数字,怎么横向显示在扇形区域,这里我就主要这个计算规则提出来:
private void drawText(Canvas canvas, float sweepAngle, float startAngle, ArcVo temp) {        float middleAngle;        middleAngle = startAngle + sweepAngle / 2;        float startX;        float startY;        float endX;        float endY;        String drawText = temp.getPercentInCircle() * 100 + "%";        if (middleAngle <= 90) {            //在第四象限            double angle = middleAngle;            angle = Math.toRadians(angle);            startY = endY = (float) (Math.sin(angle) * mRaduis + mRaduis);            endX = (float) (mRaduis + Math.cos(angle) * mRaduis);            startX = endX - UiUtils.getTextWidth(drawText, mTextPaint);        } else if (middleAngle <= 180) {            //在第三象限            double angle = 180 - middleAngle;            angle = Math.toRadians(angle);            startY = endY = (float) (Math.sin(angle) * mRaduis + mRaduis);            startX = (float) (mRaduis - Math.cos(angle) * mRaduis);            endX = startX + UiUtils.getTextWidth(drawText, mTextPaint);        } else if (middleAngle <= 270) {            //在第二象限            double angle = 270 - middleAngle;            angle = Math.toRadians(angle);            startY = endY = (float) (mRaduis - Math.cos(angle) * mRaduis);            startX = (float) (mRaduis - Math.sin(angle) * mRaduis);            endX = startX + UiUtils.getTextWidth(drawText, mTextPaint);        } else {            //在第一象限            double angle = 360 - middleAngle;            angle = Math.toRadians(angle);            startY = endY = (float) (mRaduis - Math.sin(angle) * mRaduis);            endX = (float) (mRaduis + Math.cos(angle) * mRaduis);            startX = endX - UiUtils.getTextWidth(drawText, mTextPaint);        }        mTextPath.reset();        mTextPath.moveTo(startX, startY);        mTextPath.lineTo(endX, endY);        if (middleAngle > 180) {            canvas.drawTextOnPath(drawText, mTextPath, 0, UiUtils.getTextHeight(drawText, mTextPaint), mTextPaint);        } else {            canvas.drawTextOnPath(drawText, mTextPath, 0, -UiUtils.getTextHeight(drawText, mTextPaint), mTextPaint);        }    }     @Override    protected void onDraw(Canvas canvas) {        if (!canDraw()) return;        float sweepAngle;        float startAngle = 0;        for (int i = 0, size = mDisArcList.size(); i < size; i++) {            ArcVo temp = mDisArcList.get(i);            mArcPaint.setColor(temp.getScanColor());            sweepAngle = temp.getPercentInCircle() * 360;            canvas.drawArc(mDrawCircleRect, startAngle, sweepAngle, true, mArcPaint);            drawText(canvas, sweepAngle, startAngle, temp);            startAngle = startAngle + sweepAngle;        }    }复制代码

五.使用方式:

如果你觉得你们的项目正好要用到类似的图标,在项目的gradle文件中,增加compile 'wellijohn.org.simplelinechart:linechart:0.0.2'具体的方法,欢迎移步到github上去看,已经封装成库上传至jcenter,上面有具体的使用方法(),目前暴露的方法不多,可以留言增加

github地址:)
如果觉得项目对你们的自定义view有一定的启发的话,麻烦帮忙star一下,如果有更好的实现方案,欢迎留言交流!!

转载地址:http://wgdra.baihongyu.com/

你可能感兴趣的文章
Linux下文件 ~/.bashrc 和 ~/.bash_profile 和 /etc/bashrc 和 /etc/profile 的区别 | 用户登录后加载配置文件的顺序...
查看>>
关于在swiper轮播组件中使用echarts的'click'事件无效
查看>>
Android开源项目README规范
查看>>
asp.net core 教程(五)-配置
查看>>
Spring Bean Scope (作用域)
查看>>
Redis命令操作详解
查看>>
java.lang.ClassNotFoundException: org.apache.axis2.transport.http.AxisAdminServlet
查看>>
SSL协议详解
查看>>
Android Studio自带的抓图和录像功能
查看>>
教妹学 Java:动态伴侣 Groovy
查看>>
第三周作业
查看>>
对象.原型链,函数.原型对象
查看>>
动态 K th
查看>>
MVC 中引入Jquery文件的几种方法
查看>>
servlet容器开发要点
查看>>
[转载]使用Cufon技术实现Web自定义字体
查看>>
dede全功能手册V5.3
查看>>
架构师入门ing
查看>>
[UOJ218]火车管理
查看>>
Android -- 自定义View(一)
查看>>