Android自定義控件-折線圖

好長時間沒有更新博客了,終於可以抽出時間寫點東西了,寫點什麼呢?最近在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()方法

具體用到哪幾個方法,具體情況具體分析,關鍵點還是在算法上邊。

參考文章:

http://blog.csdn.net/yifei1989/article/details/29891211

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章