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