新項目需求裡面,需要根據不同的資料,繪制一條圓滑曲線,最開始想到的是用二階貝塞爾曲線來繪制。兩個資料點,一個控制點,三個資料,從第0個開始,依次推進。實踐的時候 發現不對,繪制出來的資料跟UI要求的或者跟預期中的差距太遠了。
項目需求的曲線
在度娘哪裡得到的資訊,無外乎兩種:
一種就是教你如何繪制三個點下的二階貝塞爾,或者說4個點下的三階貝塞爾。笑話,單單幾個點獨立成線并且将資料點和控制點都告訴你的繪制,我也會。問題是,我現在隻告訴你兩個資料點,沒有控制點,你如何繪制出完美的貝塞爾曲線?
第二種就是裝13的貨,列出一大堆高深莫測的貝塞爾推導公式,能徹底了解這些公式的數學大牛有幾個?話說我都這麼牛逼的數學大牛了,還這裡給你瞎BBB?一點也不實際。
另外一個共同點就是,無論第一種情況還是第二種情況,原創的都太少了,都是你抄過去,我抄過來,對于臉盲的我來說,一臉懵逼。
度娘沒有,沒大神指導,就自己研究呗。
後面研究發現,繪制這樣的曲線其實需要更高階的三階貝塞爾曲線來完成。并且在兩個資料點和兩個控制點的坐标關系上,有如下規律。有了這個規律,就能大大減輕計算複雜程度了。具體規律是:
三階貝塞爾曲線的繪制方法:linePath.moveTo()、linePth.cubicTo();
繪制三階貝塞爾曲線,需要4個點:兩端的兩個資料點(startPoint,endPoint),中間兩個控制點(controlAPoint、controlBPoint)。
這4個點的坐标值的确定方法(假如給定資料List<Integer> list):
一、startPoint:
1、當繪制第一個點的時候:start.x為曲線起始點到View左邊界的距離,可以簡單的了解為paddingLeft,如下圖中紅框框1的寬度;start.y為list.get(0);
2、當繪制第1+N個點的時候,startPoint = endPoint;endPoint後面會介紹。
二、controlAPoint:
controlA.x = start.x + L/2; controlA.y =start.y; L為如上圖中紅框框2的寬度。
三、controlBPoint:
controlB.x = controlA.x; controlB.y = end.y;
四、endPoint:
end.x = start.x + L;同上,L為如上圖中紅框框2的寬度。
End.y = list.get(i)(注意,這裡的i>0,從1開始);
這是繪制三階貝塞爾曲線的4個坐标點的關系計算。
具體繪制如下:
public class MyBezierChhar extends View {
/**曲線的畫筆*/
private Paint linePaint;
/**錨點的畫筆*/
private Paint pointPaint;
/**軸線文本,坐标文本的畫筆*/
private Paint textPaint;
/**警示框文本的畫筆*/
private Paint warnPaint;
/**軸線的畫筆*/
private Paint shaftPaint;
private Paint warnTextPaint;
private Path warnPath;
private Path linePath;
private Path shaftPath;
private int viewWidth;
private int viewHight;
private float textPaintSize = 24f;
private float lineWidth = 4f;
private float pointWidth = 8f;
float maxValue = 0;
/**Y軸坐标偏移量*/
private int offSetShaftY = dpTopx(5);
/**X軸坐标偏移量*/
private int offSetShaftX = dpTopx(5);
/**在onDraw方法裡面執行個體化*/
private PointF[] points;
private List<Float> list;
private List<String> dateList;
private void initNativeParams(){
linePaint = new Paint();
linePaint.setStrokeWidth(lineWidth);
linePaint.setAntiAlias(true);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setStrokeCap(Paint.Cap.ROUND);
linePaint.setColor(Color.parseColor("#0072ff"));
pointPaint = new Paint();
pointPaint.setStrokeWidth(pointWidth);
pointPaint.setAntiAlias(true);
pointPaint.setStyle(Paint.Style.FILL);
pointPaint.setStrokeCap(Paint.Cap.ROUND);
pointPaint.setColor(Color.parseColor("#0072ff"));
textPaint = new Paint();
textPaint.setAntiAlias(true);
textPaint.setStyle(Paint.Style.FILL);
textPaint.setColor(Color.parseColor("#0072ff"));
textPaint.setTextSize(textPaintSize);
textPaint.setTextAlign(Paint.Align.CENTER);
warnPaint = new Paint();
warnPaint.setAntiAlias(true);
warnPaint.setStyle(Paint.Style.FILL);
warnPaint.setColor(Color.parseColor("#ffa200"));
float warnTextPaintSize = 24f;
warnTextPaint = new Paint();
warnTextPaint.setTextSize(warnTextPaintSize);
warnTextPaint.setAntiAlias(true);
warnTextPaint.setStyle(Paint.Style.FILL);
/**左對齊*/
warnTextPaint.setTextAlign(Paint.Align.LEFT);
warnTextPaint.setColor(Color.parseColor("#ffffff"));
shaftPaint = new Paint();
shaftPaint.setStrokeWidth(0.5f);
shaftPaint.setAntiAlias(true);
shaftPaint.setStyle(Paint.Style.STROKE);
shaftPaint.setColor(Color.parseColor("#0072ff"));
linePath = new Path();
shaftPath = new Path();
warnPath = new Path();
list = new ArrayList<>();
}
public MyBezierChhar(Context context) {
super(context);
initNativeParams();
}
public MyBezierChhar(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initNativeParams();
}
public MyBezierChhar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initNativeParams();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
viewWidth = w;
viewHight = h;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
/**繪制背景色*/
canvas.drawColor(Color.parseColor("#ffffffff"));
/**offSetShaftY:Y軸坐标偏移量,分正負和方向*/
canvas.translate(0, offSetShaftY);
/**offSetShaftX:X軸坐标偏移量,分正負和方向*/
canvas.drawLine(offSetShaftX,0, viewWidth, 0, shaftPaint);
/**areaNumber個資料,areaNumber-1根線*/
int areaNumber = getList().size();
/**Y軸便宜後,水準方向還可以用的寬度*/
int shaftLineWidth = viewWidth - offSetShaftY;
/**平均每個區間的寬度*/
float averageWidth = shaftLineWidth / areaNumber;
/**areaNumber個資料, areaNumber-1根線,第0根線(也可以看做Y坐标),不畫,作用從1開始*/
for (int i = 1; i < areaNumber; i++) {
float coordinates = offSetShaftY + averageWidth * i;
canvas.drawLine(coordinates, 0, coordinates, viewHight, shaftPaint);
}
/**Y軸方向偏移5dp的距離,繪制坐标文本*/
if (dateList.size() != areaNumber) {
return;
}
for (int i = 0; i < dateList.size(); i++) {
float locationX = averageWidth / 2 + averageWidth * i;
canvas.drawText(dateList.get(i), locationX, (offSetShaftY + textPaintSize), textPaint);
}
float drawAreaY = (viewHight - offSetShaftY - textPaintSize) * 0.75f;
/**繪制曲線*/
/**曲線繪制區域:
* X方向:offSetShaftX開始的整個水準方向
* Y方向:(viewWidth(View的整體高度) - offSetShaftY(垂直方向的偏移量) - textPaintSize(坐标值得尺寸))*0.75的區域内*/
/**最大的值*/
for (int i = 0; i < list.size(); i++) {
if(list.get(i) > maxValue){
maxValue = list.get(i);
}
}
/**所有點都位于自己區域内水準中心線上,是以所有點的X坐标公式:
* float locationX = averageWidth / 2 + averageWidth*i;
**/
/**起始點的坐标*/
PointF start = new PointF();
start.x = (averageWidth / 2) + offSetShaftX;
/**(list.get(0) / maxValue):算出給定的資料占總高度的百分比, 乘drawAreaY就得出新的高度*/
start.y = viewHight - ((list.get(0) / maxValue) * drawAreaY + offSetShaftY + textPaintSize + dpTopx(12));
points[0] = start;
linePath.moveTo(start.x, start.y);
for (int i = 1; i < list.size(); i++) {
PointF end = new PointF();
end.x = averageWidth / 2 + offSetShaftX + averageWidth * i;
end.y = viewHight - ((list.get(i) / maxValue) * drawAreaY + offSetShaftY + textPaintSize + dpTopx(12));
float controlPointA = start.x + averageWidth / 2;
/**控制點1*/
PointF controlA = new PointF();
controlA.set(controlPointA, start.y);
/**控制點2*/
PointF controlB = new PointF();
controlB.set(controlPointA, end.y);
linePath.cubicTo(controlA.x, controlA.y, controlB.x, controlB.y, end.x, end.y);
start = end;
points[i] = end;
}
canvas.drawPath(linePath, linePaint);
for (int i = 0; i < points.length; i++) {
PointF pointF = points[i];
if (pointF == null) {
return;
}
/**繪制錨點*/
canvas.drawCircle(pointF.x, pointF.y, pointWidth, pointPaint);
/**繪制坐标值*/
canvas.drawText(String.valueOf(list.get(i)), pointF.x + textPaintSize, pointF.y - textPaintSize, textPaint);
/**不合格資料辨別*/
if (list.get(i) < 50) {
/**圓角矩形*/
float startX = pointF.x + (textPaintSize/2);
float startY = pointF.y + (textPaintSize/2);
float endX = pointF.x + textPaintSize + 80;
float endY = pointF.y + textPaintSize + 30;
RectF rectF = new RectF(startX, startY, endX, endY);
warnPath.addRoundRect(rectF, 10, 10, Path.Direction.CCW);
canvas.drawPath(warnPath, warnPaint);
/**不合格警示文本*/
String warnText = "未達标";
canvas.drawText(warnText, startX + 9, startY + 29, warnTextPaint);
}
}
}
public List<Float> getList() {
if (list == null) {
return new ArrayList<>();
}
return list;
}
public void setList(List<Float> list) {
this.list = list;
postInvalidate();
}
public List<String> getDateList() {
if (dateList == null) {
return new ArrayList<>();
}
return dateList;
}
public void setDateList(List<String> dateList) {
this.dateList = dateList;
if (dateList == null || dateList.size() < 1) {
return;
}
points = new PointF[dateList.size()];
postInvalidate();
}
private int dpTopx(float dipValue) {
final float scale = getResources().getDisplayMetrics().density;
return (int) (dipValue * scale + 0.5f);
}
private int spTopx(float spValue) {
final float fontScale = getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale + 0.5f);
}
}
Activity裡面用法如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/back_ground"
tools:context="com.haoyue.demolist.bezier.MyBeizierActivity">
<FrameLayout
android:id="@+id/flAddNewView"
android:layout_width="match_parent"
android:layout_height="300dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_margin="10dp"
android:background="@drawable/view_click"
android:text="加載曲線圖"
android:gravity="center"
android:onClick="loadBeizier"/>
</LinearLayout>
Java代碼如下:
FrameLayout flAddNewView;
MyBezierChhar bezierChhar;
List<Float> list = new ArrayList<>();
List<String> dateList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my_beizier);
flAddNewView = findViewById(R.id.flAddNewView);
bezierChhar = new MyBezierChhar(this);
}
public void loadBeizier(View view){
int viewNumber = flAddNewView.getChildCount();
if (viewNumber > 0) {
bezierChhar.postInvalidate();
return;
}
for (int i = 1; i < 11; i++) {
dateList.add(i + "日");
}
list.add(200f);
list.add(100f);
list.add(300f);
list.add(20f);
list.add(120f);
list.add(60f);
list.add(160f);
list.add(300f);
list.add(50f);
list.add(150f);
bezierChhar.setList(list);
bezierChhar.setDateList(dateList);
flAddNewView.addView(bezierChhar);
}
實際運作效果圖:
完全滿足了UI設計得需求。