Android自定義View之年度賬單曲線圖

前言

一說到折線圖曲線圖,我馬上就想到了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移動到某個座標但是移動過程沒有痕跡。

動手

  1. 先定義常量
 /**
     * 折線畫筆
     */
    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;
  1. 重寫View的構造方法,使其最終指向帶有3個參數的構造方法
this(context,null);
this(context, attrs,0);
super(context, attrs, defStyleAttr);
  1. 構造方法內初始化數據,此步代碼略
  2. 重寫onMeasure方法,確定寬高,因爲高度一開始就是固定的,有需求的小夥伴可以在方法內確定高度。
  3. 重寫onDraw方法
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //畫漸變藍色背景
        drawBackBlue(canvas);
        //畫座標
        drawAxis(canvas);
        //畫曲線
        drawCurve(canvas);
        //畫小圓點和虛線
        drawPointsAndLine(canvas);
        //畫水錶詳情框
        drawWaterDetailsText(canvas);
    }
  1. 寫一個公開方法,用於設置元數據
 /**
     * 公開方法,用於設置元數據
     */
    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

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