好长时间没有更新博客了,终于可以抽出时间写点东西了,写点什么呢?最近在qq群里边有人问,下边的这个控件怎么画?如下图所示:图可以左右拖动,直到显示完全为止。刚开始看到这个效果图,我也想了一下总共分为以下几个步骤:
(1)坐标轴的绘画,并绘画坐标轴上的坐标值
(2)绘画坐标上的点,并将其串联起来
(3)最后进行封闭图形的填充
(4)事件的拖动重绘
1、首先定义自定义属性文件,并确定坐标轴的颜色,宽度,坐标文字的大小,线的颜色等
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="LineChart">
<attr name="xylinecolor" format="color" ></attr>
<attr name="xylinewidth" format="dimension"></attr>
<attr name="xytextcolor" format="color"></attr>
<attr name="xytextsize" format="dimension"></attr>
<attr name="linecolor" format="color"></attr>
<attr name="interval" format="dimension"></attr>
<attr name="bgcolor" format="color"></attr>
</declare-styleable>
</resources>
2、主布局文件
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:ypm = "http://schemas.android.com/apk/res/com.ypm.linechartdemo"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<com.ypm.linechartdemo.LineChart
android:id="@+id/id_linechart"
android:layout_width="match_parent"
android:layout_height="match_parent"
ypm:xylinecolor="@color/xylinecolor"
ypm:xylinewidth="@dimen/xylinewidth"
ypm:xytextsize = "@dimen/xytextsize"
ypm:linecolor="@color/linecolor"
>
</com.ypm.linechartdemo.LineChart>
</RelativeLayout>
3、接下来就是自定义LineChart控件,首先定义一些列的变量值如下:
/**
* 坐标轴的颜色
*/
private int xyColor;
/**
* 坐标轴的宽度
*/
private int xyWidth;
/**
* 坐标轴文字的颜色
*/
private int xyTextColor;
/**
* 坐标轴文字的大小
*/
private int xyTextSize;
/**
* 坐标轴的之间的间距
*/
private int interval;
/**
* 折线的颜色
*/
private int lineColor;
/**
* 背景颜色
*/
private int bgColor;
/**
* 原点坐标最大x
*/
private int ori_x;
/**
* 第一个点的坐标
*/
private int first_x;
/**
* 第一个点的坐标最小x,和最大x坐标
*/
private int ori_min_x,ori_max_x;
/**
* 原点坐标y
*/
private int ori_y;
/**
* x的刻度值长度 默认值40
*/
private int xScale = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 80, getResources()
.getDisplayMetrics());
/**
* y的刻度值长度
*/
private int yScale = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 55, getResources()
.getDisplayMetrics());
/**
* x刻度
*/
private String[] xLabels;
/**
* y刻度
*/
private String[] yLabels;
/**
* x坐标轴中最远的坐标值
*/
private int maxX_X, maxX_Y;
/**
* y坐标轴的最远坐标值
*/
private int minY_X, minY_Y;
/**
* x轴最远的坐标轴
*/
private int x_last_x, x_last_y;
/**
* y轴最远的坐标值
*/
private int y_last_x, y_last_y;
private double[] dataValues;
/**
* 滑动时候,上次手指的x坐标
*/
private float startX;
4、读取属性文件上的值
public LineChart (Context context , AttributeSet attrs , int defStyle)
{
super(context, attrs, defStyle);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.LineChart);
int count = array.getIndexCount();
for (int i = 0; i < count; i++)
{
int attr = array.getIndex(i);
switch (attr)
{
case R.styleable.LineChart_xylinecolor:
xyColor = array.getColor(attr, Color.GRAY);
break;
case R.styleable.LineChart_xylinewidth:
xyWidth = (int) array.getDimension(attr, 5);
break;
case R.styleable.LineChart_xytextcolor:
xyTextColor = array.getColor(attr, Color.BLACK);
break;
case R.styleable.LineChart_xytextsize:
xyTextSize = (int) array.getDimension(attr, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
12, getResources().getDisplayMetrics()));
break;
case R.styleable.LineChart_linecolor:
lineColor = array.getColor(attr, Color.GRAY);
break;
case R.styleable.LineChart_bgcolor:
bgColor = array.getColor(attr, Color.WHITE);
break;
case R.styleable.LineChart_interval:
interval = (int) array.getDimension(attr, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
100, getResources().getDisplayMetrics()));
break;
default:
break;
}
}
array.recycle();
}
5、初始化相应的坐标值,在onMeasure中
主要进行原点,第一个点坐标,以及x最大值的相关的计算
1 int width = getWidth();
2 int height = getHeight();
3
4 ori_x = 40;
5 ori_y = height - 40;
6
7 maxX_X = width - 50;
8 minY_Y = 50;
9
10
11 ori_min_x = width - 50 -40 - dataValues.length * xScale;
12 first_x = ori_x;
13 ori_max_x = first_x;
6、绘画坐标轴
1 /**
2 *
3 * 功能描述:绘画坐标轴
4 *
5 * @param canvas
6 * @版本 1.0
7 * @创建者 ypm
8 * @创建时间 2015-8-24 上午10:39:59
9 * @版权所有
10 * @修改者 ypm
11 * @修改时间 2015-8-24 上午10:39:59 修改描述
12 */
13 private void drawXYLine(Canvas canvas)
14 {
15 Paint paint = new Paint();
16 paint.setColor(xyColor);
17 paint.setAntiAlias(true);
18 paint.setStrokeWidth(xyWidth);
19 paint.setTextSize(xyTextSize);
20 // 绘画x轴
21 int max = first_x + (xLabels.length-1) * xScale + 50;
22 if (max > maxX_X)
23 {
24 max = getMeasuredWidth();
25 }
26
27 x_last_x = max;
28 x_last_y = ori_y;
29 canvas.drawLine(first_x, ori_y, max, ori_y, paint);
30 // 绘画y轴
31 int min = ori_y - (yLabels.length - 1) * yScale - 50;
32 if (min < minY_Y)
33 {
34 min = minY_Y;
35 }
36 y_last_x = first_x;
37 y_last_y = min;
38 canvas.drawLine(first_x, ori_y, first_x, min, paint);
39
40 // 绘画x轴的刻度
41 drawXLablePoints(canvas, paint);
42 // 绘画y轴的刻度
43 drawYLablePoints(canvas, paint);
44
45 }
7、绘画折线图
这里运用到了多边形的绘画,通过Path进行绘画,并使用了多边形的填充,以及xfermode的相关知识,可以查相关的api进行了解
1 private void drawDataLine(Canvas canvas)
2 {
3 Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
4 // paint.setStyle(Paint.Style.FILL);
5 paint.setColor(xyColor);
6 Path path = new Path();
7 for (int i = 0; i < dataValues.length; i++)
8 {
9 int x = first_x + xScale * i;
10 if (i == 0)
11 {
12 path.moveTo(x, getYValue(dataValues[i]));
13 }
14 else
15 {
16 path.lineTo(x, getYValue(dataValues[i]));
17 }
18 canvas.drawCircle(x, getYValue(dataValues[i]), xyWidth, paint);
19 }
20 path.lineTo(first_x + xScale * (dataValues.length - 1), ori_y);
21 path.lineTo(first_x, ori_y);
22 path.close();
23 paint.setStrokeWidth(5);
24 // paint.setColor(Color.parseColor("#D7FFEE"));
25 paint.setColor(Color.parseColor("#A23400"));
26 paint.setAlpha(100);
27 // 画折线
28 canvas.drawPath(path, paint);
29 paint.setStyle(Paint.Style.FILL);
30 paint.setColor(Color.RED);
31 canvas.clipPath(path);
32
33 // 将折线超出x轴坐标的部分截取掉
34 paint.setStyle(Paint.Style.FILL);
35 paint.setColor(bgColor);
36 paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER));
37 RectF rectF = new RectF(0, 0, x_last_x, ori_y);
38 canvas.drawRect(rectF, paint);
39 }
40
41 private float getYValue(double value)
42 {
43
44 return (float) (ori_y - value / 50 * yScale);
45 }
8、事件的拖动,重写onTouchEvent方法,
这里主要的逻辑就是:
(1)当手机的宽度小于坐标值的最大值的时候,就禁止拖动
(2)如果超过手机的宽度的时候,就通过裁剪功能将渲染的图像就行裁剪,拖动的时候,将没有显示的部分进行显示
主要分为3块:
第一块:第一个点的坐标+拖动的距离和第一点坐标的最大值进行比较
第二块:第一个点的坐标+拖动的距离和第一点坐标的最小值进行比较
第三块:是在第一,二块之间的
1 @Override
2 public boolean onTouchEvent(MotionEvent event)
3 {
4 if ((dataValues.length * xScale + 50 + ori_x) < maxX_X- ori_x)
5 {
6 return false;
7 }
8 switch (event.getAction())
9 {
10 case MotionEvent.ACTION_DOWN:
11
12 startX = event.getX();
13 break;
14 case MotionEvent.ACTION_MOVE:
15 float distance = event.getX() - startX;
16 // Log.v("tagtag", "startX="+startX+",distance="+distance);
17 startX = event.getX();
18 if(first_x+distance > ori_max_x)
19 {
20 Log.v("tagtag", "111");
21 first_x = ori_max_x;
22 }
23 else if(first_x+distance<ori_min_x)
24 {
25 Log.v("tagtag", "222");
26 first_x = ori_min_x;
27 }
28 else
29 {
30 Log.v("tagtag", "333");
31 first_x = (int)(first_x + distance);
32 }
33 invalidate();
34 break;
35 }
36 return true;
37 }
9、最终效果图,如下
总结:
自定义控件的编写步骤可以分为以下几个步骤:
(1)编写attr.xml文件
(2)在layout布局文件中引用,同时引用命名空间
(3)在自定义控件中进行读取(构造方法拿到attr.xml文件值)
(4)覆写onMeasure()方法
(5)覆写onLayout(),onDraw()方法
具体用到哪几个方法,具体情况具体分析,关键点还是在算法上边。