前言
一說到折線圖曲線圖,我馬上就想到了GitHub上的MPAndroidChart,擴展功能強大,本來想用,不過我轉念一想,就一個年度賬單,自己寫不是幾百行代碼就搞定了?於是我開始了。先看一下設計圖
思考
- 先考慮需要幾種畫筆Paint:曲線、字體、網格、背景色、詳情背景色
- 再考慮幾個重要的變量:折線單位高度差、橫向和縱向Item的高寬度、當年的最高用量、當年的最低用量
- 需要確定的數據源:當前年份的水錶數據集合
- 需要計算每個月點的座標位置集合List<PointF>
- 橫向可滑動、選中的點pointFSelected以及點擊範圍的判定,在每個月峯值某個範圍內允許點擊。
必須先了解的知識點
- Canvas座標系與繪圖座標系:簡單點說Canvas座標系唯一不變,平常我們使用繪圖座標系(例如canvas的drawLine)。
- Canvas的save和restore方法:瞭解了繪圖座標系,應該很好了解,比如我先sava一下(保存當前矩陣),然後調用canvas的旋轉,然後使用Paint去繪製,繪製完了,調用restore回到之前沒有旋轉的畫板繼續畫。
- VelocityTracker:速度追蹤器,主要用跟蹤觸摸屏事件(flinging事件和其他gestures手勢事件)的速率。
- ViewConfiguration:包含用於UI的標準常量的方法,用於超時,大小和距離。用來判斷是否是拋動。
- Paint使用PathEffect畫出虛線:
//兩個值分別爲循環的實線長度、空白長度
float[] f = {dp2pxF(5f,context), dp2pxF(2f,context)};
PathEffect pathEffect = new DashPathEffect(f, 0);
paint.setPathEffect(pathEffect);
/**
* dp轉pxF
*/
public static float dp2pxF(float dpValue,Context context) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, context.getResources().getDisplayMetrics());
}
- 畫漸變背景:通過設置畫筆來實現。
/**
* 漸變背景畫筆
*/
private Paint backGroundPaint;
backGroundPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
//設置抗鋸齒
backGroundPaint.setAntiAlias(true);
//爲Paint設置漸變
LinearGradient linearGradient=new LinearGradient(pointFMaxCurve.x,pointFMaxCurve.y,pointFList.get(0).x,viewHeight-itemWidth,new int[]{
0xFFE1F1FF,0xFFEFF7FF,0xFFFAFCFF},
null, Shader.TileMode.CLAMP);
backGroundPaint.setShader(linearGradient);
- text寬度的自適應:因爲數據變化大,所以詳情框座標確認就需要獲取text的寬度。思路是這樣子:數據都是空字符串時背景框的寬度加上獲取text的寬度就是上圖用量讀數彈框的寬度了。
/**
* 精確計算文字寬度
*
* @param paint
* @param str
* @return 文字長度,像素
*/
public static int getTextWidth(Paint paint, String str) {
int iRet = 0;
if (str != null && str.length() > 0) {
int len = str.length();
float[] widths = new float[len];
paint.getTextWidths(str, widths);
for (int j = 0; j < len; j++) {
iRet += (int) Math.ceil(widths[j]);
}
}
return iRet;
}
- Path常用Api:如cubicTo畫三次貝塞爾曲線、reset清除當前路徑、moveTo移動到某個座標但是移動過程沒有痕跡。
動手
- 先定義常量
/**
* 折線畫筆
*/
private Paint brokenLinePaint;
/**
* 座標畫筆
*/
private Paint coordinatePaint;
/**
* 圓點畫筆
*/
private Paint circlePaint;
/**
* 文字畫筆(x、y軸)
*/
private Paint textPaint;
/**
* 文字畫筆(水錶詳情)
*/
private Paint textPaintDetail;
/**
* 漸變背景畫筆
*/
private Paint backGroundPaint;
/**
* 詳情背景畫筆
*/
private Paint backGroundDetailPaint;
/**
* 背景顏色
*/
private int backGroundColor= Color.WHITE;
/**
* 文字畫筆顏色
* 默認:黑色
*/
private int colorTextPaint= Color.BLACK;
/**
* 折現畫筆顏色
* 默認:藍色 0xFF1281FD
*/
private int colorBrokenLinePaint=0xFF1281FD;
/**
* 座標畫筆顏色
* 默認 灰色 0xFFEEEEEE
*/
private int colorCoordinatePaint=0xFFEEEEEE;
/**
* 詳情文字背景顏色
*/
private int colorDetailTextBg=0x66000000;
/**
* 文字大小
* 默認11sp
*/
private float textSize;
/**
* 水錶詳情文字大小
* 默認10sp
*/
private float textSizeDetail;
/**
* 單位高度差
* 默認:itemWidth/unitYItem
*/
private float unitVerticalGap;
/**
* itemWidth對應的用水量(噸)
*/
private int unitYItem;
/**
* Y方向有數據的Item個數
* 默認4個
*/
private static final int itemYSize=4;
/**
* 折線圖左和下的間距,同橫縱單位間隔
* 默認:42dp
*/
private int defaultPadding;
private int itemWidth;
/**
* 控件期望高度
* 默認爲8個itemWidth
*/
private int expectViewHeight;
/**
* 折線點的半徑(默認2.5dp的像素)
*/
private float pointRadius;
private int viewWidth;
private int viewHeight;
private int screenWidth;
private int screenHeight;
/**
* 曲線路徑
*/
private Path curvePath;
/**
* 水錶詳情背景路徑
*/
private Path WaterDetailBgPath;
/**
* 水錶詳情背景圖範圍
*/
private RectF rectF;
/**
* 最多用量
*/
private float maxDosage;
/**
* 最少用量
*/
private float minDosage;
/**
* 數據列表data
*/
private List<WaterAndElectricMeterDetail> list=new ArrayList<>();
/**
* 每個月的座標點集
*/
private List<PointF> pointFList=new ArrayList<>();
/**
* 選中的那個點
*/
private PointF pointFSelected=null;
/**
* 速度追蹤器
*/
private VelocityTracker velocityTracker;
/**
* 關於UI的標準常量
*/
private ViewConfiguration viewConfiguration;
/**
* Scroller
*/
private Scroller scroller;
- 重寫View的構造方法,使其最終指向帶有3個參數的構造方法
this(context,null);
this(context, attrs,0);
super(context, attrs, defStyleAttr);
- 構造方法內初始化數據,此步代碼略
- 重寫onMeasure方法,確定寬高,因爲高度一開始就是固定的,有需求的小夥伴可以在方法內確定高度。
- 重寫onDraw方法
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//畫漸變藍色背景
drawBackBlue(canvas);
//畫座標
drawAxis(canvas);
//畫曲線
drawCurve(canvas);
//畫小圓點和虛線
drawPointsAndLine(canvas);
//畫水錶詳情框
drawWaterDetailsText(canvas);
}
- 寫一個公開方法,用於設置元數據
/**
* 公開方法,用於設置元數據
*/
public void setData(List<WaterAndElectricMeterDetail> data) {
if (data == null) {
return;
}
//數據清理
list.clear();
pointFList.clear();
pointFSelected=null;
this.list = data;
//計算單位高度差
calculateGap();
//獲取數據點集
initPointFData();
invalidate();
}
附上GitHub上Demo地址:https://github.com/PengHaiZhuo/MyMiUiWeatherDemo
後話
因需求簡單,所以很多變量值都寫死了,有興趣的朋友可以自行修改或者通過定義自定義屬性來優化,主要是學習,加油。
非常感謝這個博主分享,提供了思路,鏈接:https://blog.csdn.net/ccy0122/article/details/76464825