安卓自定义饼状图
来源:互联网 发布:mac设置自动关机 编辑:程序博客网 时间:2024/06/11 01:00
闲来想自己写个饼状图,于是就动手开始画了。
主要的逻辑:1.根据比例依次旋转角度画出扇形;2.在扇形区域内设置内容数据;3.当点击某个扇形的时候,就让当前扇形脱离整体,空出一部分
看起来这个逻辑比较复杂,但是真正写下来之后就会发现其实饼状图也简单,主要就是围绕着安卓简单的自定义控件画扇形。效果如下:
接下来就开始代码实现:
首先初始化2个画笔,一个是画扇形的,一个是画扇形的边框的,还有初始化集合数据:
private void initPaint() { mPaint = new Paint(); mPaint.setStyle(Paint.Style.FILL); mPaint.setAntiAlias(true); mPaintBorder = new Paint(Paint.ANTI_ALIAS_FLAG); mPaintBorder.setStyle(Paint.Style.STROKE); mPaintBorder.setAntiAlias(true); mPaintBorder.setColor(Color.BLACK); mPaintBorder.setTextSize(35); mDatas.add(new PieItemBean("测试1", 9, Color.rgb(155, 187, 90))); mDatas.add(new PieItemBean("测试2", 3, Color.rgb(191, 79, 75))); mDatas.add(new PieItemBean("测试3", 76f, Color.rgb(242, 167, 69))); mDatas.add(new PieItemBean("测试4", 6, Color.rgb(60, 173, 213))); mDatas.add(new PieItemBean("测试5", 6, Color.rgb(90, 79, 88)));}
然后先复写onMeasure方法,以便设置控件的大小:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); setMeasuredDimension(measureValue(widthMeasureSpec), measureValue(heightMeasureSpec)); } private int measureValue(int measureSpec) { int result = 100;//设置最小值 int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); if (specMode == MeasureSpec.EXACTLY) { //fill_parent或者设置了具体的宽高 result = specSize; } else if (specMode == MeasureSpec.AT_MOST) { //wrap_content result = Math.min(result, specSize); } return result; }
接下来就是最重要的onDraw方法了.
在onDraw方法中,首先需要获得padding值和宽高,从而设置圆的半径:
int width = getMeasuredWidth(); int height = getMeasuredHeight(); int rWidth = width - getPaddingLeft() - getPaddingRight(); int rHeight = height - getPaddingTop() - getPaddingBottom(); mRadious = Math.min(rWidth, rHeight) / 2; //圆心坐标 cenX = mRadious + getPaddingLeft(); cenY = mRadious + getPaddingTop();
然后就是根据每个条目所占比例来绘制扇形图了:
//用于存放当前百分比的圆心角度 float currentAngle = 0.0f; float offsetAngle = 0f;//角度偏移量 for (int i = 0; i < mDatas.size(); i++) { PieItemBean bean = mDatas.get(i); currentAngle = per2Radious(totalAngle, bean.value);//得到当前角度 mPaint.setColor(bean.color);//给画笔设置颜色 if (mRectF == null) {//设置圆所需的范围 mRectF = new RectF(getPaddingLeft(), getPaddingTop(), width - getPaddingRight(), width - getPaddingRight()); } //在饼图中显示所占比例 canvas.drawArc(mRectF, offsetAngle , currentAngle, true, mPaint); //边框 canvas.drawArc(mRectF, offsetAngle , currentAngle, true, mPaintBorder); //下次的起始角度 offsetAngle += currentAngle; }
其中用到了2个工具方法:
/** * 将百分比转换为图心角角度 */ public float per2Radious(float totalAngle, float percentage) { float angle = 0.0f; if (percentage >= 101f || percentage < 0.0f) { //Log.e(TAG,"输入的百分比不合规范.须在0~100之间."); } else { float v = percentage / 100;//先获取百分比 float itemPer = totalAngle * v;//获取对应角度的百分比 angle = round(itemPer, 2);//精确到小数点后面2位 } return angle; } /** * 四舍五入到小数点后scale位 */ public float round(float v, int scale) { if (scale < 0) throw new IllegalArgumentException("The scale must be a positive integer or zero"); BigDecimal bgNum1 = new BigDecimal(v); BigDecimal bgNum2 = new BigDecimal("1"); return bgNum1.divide(bgNum2, scale, BigDecimal.ROUND_HALF_UP).floatValue(); }
然后我们就可以先运行起来了,运行后结果如下:
可以看到整个轮廓已经出来了,接下来就是第二步,在扇形区域内部显示文字.在显示文字的的时候主要是要计算文字描绘的起点位置,因为对于三角函数只对90°内的熟悉,所以计算的时候都转换为90°之内的值来计算:
//先将度数定位到当前所在条目的一半位置 float degree = offsetAngle + currentAngle / 2; //根据角度所在不同象限来计算出文字的起始点坐标 float dx = 0, dy = 0; if (degree > 0 && degree <= 90f) {//在第四象限 dx = (float) (cenX + mRadious * 2.3 / 3 * Math.cos(2 * PI / 360 * degree));//注意Math.sin(x)中x为弧度值,并非数学中的角度,所以需要将角度转换为弧度 dy = (float) (cenY + mRadious * 2.7 / 3 * Math.sin(2 * PI / 360 * degree)); } else if (degree > 90f && degree <= 180f) {//在第三象限 dx = (float) (cenX - mRadious * 2.3 / 3 * Math.cos(2 * PI / 360 * (180f - degree))); dy = (float) (cenY + mRadious * 2.7 / 3 * Math.sin(2 * PI / 360 * (180f - degree))); } else if (degree > 180f && degree <= 270f) {//在第二象限 dx = (float) (cenX - mRadious * 2.3 / 3 * Math.cos(2 * PI / 360 * (270f - degree))); dy = (float) (cenY - mRadious * 2.7 / 3 * Math.sin(2 * PI / 360 * (270f - degree))); } else { dx = (float) (cenX + mRadious * 2.3 / 3 * Math.cos(2 * PI / 360 * (360f - degree))); dy = (float) (cenY - mRadious * 2.7 / 3 * Math.sin(2 * PI / 360 * (360f - degree))); } //文字的基本线坐标设置为半径的2.3/3位置处,起点y坐标设置为半径的2.7/3位置处 canvas.drawText(bean.value + "%", dx, dy, mPaintBorder); //下次的起始角度 offsetAngle += currentAngle;
现在数据已经都绘制好了,剩下的就是扇形区的点击事件了
要判断点击的位置位于哪个条目,需要以下几点判断:
1.首先根据点击点的坐标来计算到圆心的距离,来判断点击位置是否在圆内部
2.如果在圆的内部,再计算出点击处与x轴所成的角度,然后根据该角度来判断在哪个数据合集内,之前的offsetAngle属性最后记录的都是当前元素在坐标系内横跨的角度,所以建立一个数组存起来,方便对比:
//下次的起始角度 offsetAngle+= currentAngle; degrees[i] = offsetAngle;
然后就是复写onTouchEvent来计算了。在这里补充一下onTouchEvent事件的返回值说明:如果返回true,则是当前触摸事件(整个事件包括down,move,up)不处理,留着交给父控件处理或者在别的动作的时候消耗,所以在down,move时都返回true,以便将触摸事件能够交给up时处理,up处理完,当前的点击事件已经处理完成了,所以可以返回false,示意该控件已经处理完了这个事件,不需要父控件在处理了。
private long startTime;//点击的起始时间 @Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: startTime = System.currentTimeMillis(); break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: long currTime = System.currentTimeMillis(); float dx = x - cenX; float dy = y - cenY; if (currTime - startTime <= 500) {//按下和抬起的时间在500毫秒内认为是单击 float degree;//被点击选中的角度 //先判断是否在圆内部 if (isInCircle(dx, dy, mRadious)) {//根据不同的象限来获取到x轴的角度 if (dx > 0 && dy > 0) {//第四象限 degree = (float) (90f - 180 * Math.atan2(dx, dy) / PI); } else if (dx < 0 && dy > 0) {//第三象限 degree = (float) (180 * Math.atan2(-dx, dy) / PI + 90f); } else if (dx < 0 && dy < 0) {//第二象限 degree = (float) (180 * Math.atan2(dy, dx) / PI + 360f); } else {//第一象限 degree = (float) (360f - 180 * Math.atan2(-dy, dx) / PI); } //然后判断该角度在哪个数据集内 selectedPos = judgeDegree(degree); invalidate();//请求重绘 } } return false; } return true; } /** * 根据坐标计算是否在圆内部 */ private boolean isInCircle(float lx, float ly, float radius) { double v = Math.pow(Math.abs(lx), 2) + Math.pow(Math.abs(ly), 2); double dis = Math.sqrt(v); if (dis > radius) { return false; } return true; } /** * 判断当前点击的在哪个数据集合里面 */ private int judgeDegree(float degree) { int selectedPos = 0; for (int i = 0; i < degrees.length; i++) { if (degree <= degrees[i]) { selectedPos = i; break; } } return selectedPos; }
至此,onTouchEvent事件已经处理完了,接下来就是在onDraw方法中将选中的条目剥离出来,单独处理,从而完成点击效果
if (selectedPos == i) {//选中的偏离一点儿 canvas.save(); canvas.rotate(offsetAngle + currentAngle / 2);//先将画布x轴旋转到当前角度的一半位置, canvas.translate(50, 0);//然后在平移50个单位,就将被选中的模块独立出来了 canvas.drawArc(mRectF, currentAngle / 2, -currentAngle, true, mPaint); //边框 canvas.drawArc(mRectF, currentAngle / 2, -currentAngle, true, mPaintBorder); canvas.restore(); } else { //在饼图中显示所占比例 canvas.drawArc(mRectF, offsetAngle , currentAngle, true, mPaint); //边框 canvas.drawArc(mRectF, offsetAngle , currentAngle, true, mPaintBorder); }
最后,再加入一个初始化时候的旋转动画
利用handler和message:
private static final int MSG_INFO = 0x24; private int num = 0; private float perDegree = 360f / 10;//单位扩大角度 private Handler mHander = new Handler() { @Override public void handleMessage(Message msg) { super.handleMessage(msg); if (msg.what == MSG_INFO) { if (num > 10) { return; } totalAngle = num * perDegree; invalidate(); num++; } } };
在onDraw中加入推送消息
mHander.sendEmptyMessageDelayed(MSG_INFO,20);
附上源码下载地址:
demo地址
- 安卓自定义饼状图
- 自定义安卓控件
- 安卓----自定义控件
- 安卓自定义进度条
- 自定义安卓属性
- 安卓自定义控件
- 安卓自定义进度条
- 安卓自定义标题
- 安卓自定义控件
- 安卓自定义interpolator
- 安卓 自定义颜色
- 安卓自定义按钮
- 安卓自定义控件
- 安卓自定义控件
- 安卓自定义View
- 自定义安卓照相机
- 安卓自定义view
- 安卓自定义广播
- 探索skynet(二):skynet如何启动一个服务
- Spring Security(07)——缓存UserDetails
- Unity Shader:三向贴图(Tri-planar mapping)---解决地形拉伸贴图变形以及贴图边缘的缝隙问题
- git删除远程分支文件
- 在ARM实验板LCD上显示汉字
- 安卓自定义饼状图
- [LeetCode] 104. Maximum Depth of Binary Tree
- Spring Security(08)——intercept-url配置
- Python并发速查表
- 初入Python
- 关于linux文件权限的介绍
- Struts2知识点
- Leetcode 514. Freedom Trail
- POj 2976