上一篇主要介紹了繪製經過每個點的光滑曲線的原理,本文會重點介紹一下在Android中如何從零開始使用貝塞爾方法編寫一個光滑曲線圖控件。程序的設計圖如下:
一、樣式控制類ChartStyle
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | /** 網格線顏色 */ private int gridColor; /** 座標軸分隔線寬度 */ private int axisLineWidth; /** 橫座標文本大小 */ private float horizontalLabelTextSize; /** 橫座標文本顏色 */ private int horizontalLabelTextColor; /** 橫座標標題文本大小 */ private float horizontalTitleTextSize; /** 橫座標標題文本顏色 */ private int horizontalTitleTextColor; /** 橫座標標題文本左間距 */ private int horizontalTitlePaddingLeft; /** 橫座標標題文本右間距 */ private int horizontalTitlePaddingRight; /** 縱座標文本大小 */ private float verticalLabelTextSize; /** 縱座標文本上下間距 */ private int verticalLabelTextPadding; /** 縱座標文本左右間距相對文本的比例 */ private float verticalLabelTextPaddingRate; /** 縱座標文本顏色 */ private int verticalLabelTextColor; |
二、基礎數據集合ChartData
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
private
Marker
marker;
private
List<Series>
seriesList;
private
List<Label>
xLabels;
private
List<Label>
yLabels;
private
List<Title>
titles;
private
int
maxValueY;
private
int
minValueY;
private
int
maxPointsCount;
private
LabelTransform
labelTransform;
/**
縱座標顯示文本的數量 */
private
int
yLabelCount;
/**
使用哪一個series的橫座標來顯示橫座標文本 */
private
int
xLabelUsageSeries;
public
interface
LabelTransform
{
/**
縱座標顯示的文本 */
String
verticalTransform(int
valueY);
/**
橫座標顯示的文本 */
String
horizontalTransform(int
valueX);
/**
是否顯示指定位置的橫座標文本 */
boolean
labelDrawing(int
valueX);
}
|
2.1、座標軸標籤Label
1 2 3 4 5 6 7 8 9 10 | /**文本對應的座標X*/ public float x; /**文本對應的座標Y*/ public float y; /** 文本對應的繪製座標Y */ public float drawingY; /**文本對應的實際數值*/ public int value; /**文本*/ public String text; |
2.2、時間序列Series
1
2
3
4
5
6
7
8
|
/**
序列曲線的標題 */
private
Title
title;
/**
序列曲線的顏色 */
private
int
color;
/**
序列點集合 */
private
List<Point>
points;
/**
貝塞爾曲線點 */
private
List<Point>
besselPoints;
|
2.3、橫向標題Title
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | /**文本對應的座標X*/ public float textX; /**文本對應的座標Y*/ public float textY; /**文本*/ public String text; /**圓點對應的座標X*/ public float circleX; /**圓點對應的座標Y*/ public float circleY; /**顏色*/ public int color; /**圓點的半徑*/ public int radius; /**圖形標註與文本的間距*/ public int circleTextPadding; /**文本區域*/ public Rect textRect=new Rect(); |
2.4、數據結點Point
1
2
3
4
5
6
7
8
9
10
|
/**是否在圖形中繪製出此結點*/
public
boolean
willDrawing;
/**
在canvas中的X座標 */
public
float
x;
/**
在canvas中的Y座標 */
public
float
y;
/**
實際的X數值 */
public
int
valueX;
/**
實際的Y數值 */
public
int
valueY;
|
三、光滑曲線圖BesselChartView
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | /** 通用畫筆 */ private Paint paint; /** 曲線的路徑,用於繪製曲線 */ private Path curvePath; /** 曲線圖繪製的計算信息 */ private BesselCalculator calculator; /** 曲線圖的樣式 */ private ChartStyle style; /** 曲線圖的數據 */ private ChartData data; /** 手勢解析 */ private GestureDetector detector; /** 是否繪製全部貝塞爾結點 */ private boolean drawBesselPoint; /** 滾動計算器 */ private Scroller scroller; @Override protected void onDraw(Canvas canvas) { if (data.getSeriesList().size() == 0) return; calculator.ensureTranslation(); canvas.translate(calculator.getTranslateX(), 0); drawGrid(canvas); drawCurveAndPoints(canvas); drawMarker(canvas); drawHorLabels(canvas); } |
四、核心類BesselCalculator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
/**
縱座標文本矩形 */
public
Rect
verticalTextRect;
/**
橫座標文本矩形 */
public
Rect
horizontalTextRect;
/**
橫座標標題文本矩形 */
public
Rect
horizontalTitleRect;
/**
圖形的高度 */
public
int
height;
/**
圖形的寬度 */
public
int
width;
/**
縱軸的寬度 */
public
int
yAxisWidth;
/**
縱軸的高度 */
public
int
yAxisHeight;
/**
橫軸的高度 */
public
int
xAxisHeight;
/**
橫軸的標題的高度 */
public
int
xTitleHeight;
/**
橫軸的長度 */
public
int
xAxisWidth;
/**
灰色豎線頂點 */
public
Point[]
gridPoints;
/**
畫布X軸的平移,用於實現曲線圖的滾動效果 */
private
float
translateX;
/**
用於測量文本區域長寬的畫筆 */
private
Paint
paint;
private
ChartStyle
style;
private
ChartData
data;
/**
光滑因子 */
private
float
smoothness;
/**
* 計算圖形繪製的參數信息
*
* @param width 曲線圖區域的寬度
*/
public
void
compute(int
width)
{
this.width
=
width;
this.translateX
=
0;
computeVertcalAxisInfo();//
計算縱軸參數
computeHorizontalAxisInfo();//
計算橫軸參數
computeTitlesInfo();//
計算標題參數
computeSeriesCoordinate();//
計算縱軸參數
computeBesselPoints();//
計算貝塞爾結點
computeGridPoints();//
計算網格頂點
}
|
五、核心代碼:
5.1 計算光滑曲線的貝塞爾控制點
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | /** 計算貝塞爾結點 */ private void computeBesselPoints() { for (Series series : data.getSeriesList()) { List<Point> besselPoints = series.getBesselPoints(); List<Point> points = new ArrayList<Point>(); for (Point point : series.getPoints()) { if (point.valueY > 0) points.add(point); } int count = points.size(); if (count < 2) continue; besselPoints.clear(); for (int i = 0; i < count; i++) { if (i == 0 || i == count - 1) { computeUnMonotonePoints(i, points, besselPoints); } else { Point p0 = points.get(i - 1); Point p1 = points.get(i); Point p2 = points.get(i + 1); if ((p1.y - p0.y) * (p1.y - p2.y) >= 0) {// 極值點 computeUnMonotonePoints(i, points, besselPoints); } else { computeMonotonePoints(i, points, besselPoints); } } } } } /** 計算非單調情況的貝塞爾結點 */ private void computeUnMonotonePoints(int i, List<Point> points, List<Point> besselPoints) { if (i == 0) { Point p1 = points.get(0); Point p2 = points.get(1); besselPoints.add(p1); besselPoints.add(new Point(p1.x + (p2.x - p1.x) * smoothness, p1.y)); } else if (i == points.size() - 1) { Point p0 = points.get(i - 1); Point p1 = points.get(i); besselPoints.add(new Point(p1.x - (p1.x - p0.x) * smoothness, p1.y)); besselPoints.add(p1); } else { Point p0 = points.get(i - 1); Point p1 = points.get(i); Point p2 = points.get(i + 1); besselPoints.add(new Point(p1.x - (p1.x - p0.x) * smoothness, p1.y)); besselPoints.add(p1); besselPoints.add(new Point(p1.x + (p2.x - p1.x) * smoothness, p1.y)); } } /** * 計算單調情況的貝塞爾結點 * * @param i * @param points * @param besselPoints */ private void computeMonotonePoints(int i, List<Point> points, List<Point> besselPoints) { Point p0 = points.get(i - 1); Point p1 = points.get(i); Point p2 = points.get(i + 1); float k = (p2.y - p0.y) / (p2.x - p0.x); float b = p1.y - k * p1.x; Point p01 = new Point(); p01.x = p1.x - (p1.x - (p0.y - b) / k) * smoothness; p01.y = k * p01.x + b; besselPoints.add(p01); besselPoints.add(p1); Point p11 = new Point(); p11.x = p1.x + (p2.x - p1.x) * smoothness; p11.y = k * p11.x + b; besselPoints.add(p11); } |
5.2、座標變換。由於手機屏幕的座標是朝右下方的,而我們實際顯示的時候是朝左上方的,所以需要進行座標變換,代碼:
1
2
|
float
ratio
=
(point.valueY
-
data.getMinValueY())
/
(float)
(data.getMaxValueY()
-
data.getMinValueY());
point.y
=
maxCoordinateY
-
(maxCoordinateY
-
minCoordinateY)
*
ratio;
|
5.3、實現拖動
1 2 3 4 | @Override public boolean onTouchEvent(MotionEvent event) { return detector.onTouchEvent(event); } |
實現OnGestureListener的OnScroll方法
1
2
3
4
5
6
7
8
9
10
|
@Override
public
boolean
onScroll(MotionEvent
e1,
MotionEvent
e2,
float
distanceX,
float
distanceY)
{
if
(Math.abs(distanceX
/
distanceY)
>
1)
{
getParent().requestDisallowInterceptTouchEvent(true);
BesselChartView.this.calculator.move(distanceX);
invalidate();
return
true;
}
return
false;
}
|
在BesselChartView的onDraw方法中調用如下代碼來平移畫布實現拖動
1 | canvas.translate(calculator.getTranslateX(), 0); |
5.4、實現滑動
只實現拖動會讓人有一種不流暢的感覺,所以還需要實現滑動,考慮到應用要支持api level 8,可以使用Scroller來實現(api level 9以後google推薦使用OverScroller來實現,OverScroller允許滾動超出邊界,可以實現回彈效果), OnGestureListener的onFling和onDown方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@Override
public
boolean
onFling(MotionEvent
e1,
MotionEvent
e2,
float
velocityX,
float
velocityY)
{
scroller.fling((int)
BesselChartView.this.calculator.getTranslateX(),
0,
(int)
velocityX,
0,
-getWidth(),
0,
0,
0);
ViewCompat.postInvalidateOnAnimation(BesselChartView.this);
return
true;
}
@Override
public
boolean
onDown(MotionEvent
e)
{
scroller.forceFinished(true);
ViewCompat.postInvalidateOnAnimation(BesselChartView.this);
return
true;
}
|
獲取scroller計算的偏移,同時刷新UI,computeScroll()會在View的onDraw方法之前執行
1 2 3 4 5 6 7 | @Override public void computeScroll() { if (scroller.computeScrollOffset()) { calculator.moveTo(scroller.getCurrX()); ViewCompat.postInvalidateOnAnimation(this); } } |
5.5、實現滾動動畫
1
|
scroller.startScroll(0,
0,
-calculator.xAxisWidth
/
2,
0,
7000);
|
六、使用到的繪圖相關的api
6.1 Canvas 畫布
1 2 3 4 5 6 | translate(float dx, float dy) drawLine(float startX, float startY, float stopX, float stopY, Paint paint) drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint) drawCircle(float cx, float cy, float radius, Paint paint) drawPath(Path path, Paint paint) drawText(String text, float x, float y, Paint paint) |
6.2 Paint 畫筆
1
2
3
4
5
6
|
setStyle(Style
style)
setStrokeWidth(float
width)
setColor(int
color)
setTextSize(float
textSize)
setTextAlign(Align
align)
setAlpha(int
a)
|
6.3 Path 路徑
1
2
|
moveTo(float
x,
float
y)
cubicTo(float
x1,
float
y1,
float
x2,
float
y2,
float
x3,
float
y3)
|
七、源代碼地址:https://github.com/TomkeyZhang/BesselChart
八、在安居客android app中的效果圖