Android自定義控制項:圖形報表的實現(折線圖、曲線圖、動態曲線圖)(View與SurfaceView分別實現圖表控制項)

来源:https://www.cnblogs.com/qixingchao/archive/2019/10/11/11652384.html
-Advertisement-
Play Games

圖形報表很常用,因為展示數據比較直觀,常見的形式有很多,如:折線圖、柱形圖、餅圖、雷達圖、股票圖、還有一些3D效果的圖表等。 Android中也有不少第三方圖表庫,但是很難相容各種各樣的需求。 如果第三方庫不能滿足我們的需要,那麼就需要自己去寫這麼一個控制項。 往往在APP需求給定後,很多開發者卻無從 ...


圖形報表很常用,因為展示數據比較直觀,常見的形式有很多,如:折線圖、柱形圖、餅圖、雷達圖、股票圖、還有一些3D效果的圖表等。
Android中也有不少第三方圖表庫,但是很難相容各種各樣的需求。
如果第三方庫不能滿足我們的需要,那麼就需要自己去寫這麼一個控制項。

往往在APP需求給定後,很多開發者卻無從下手,不知道該如何寫。
今天剛好抽出點時間,做了個小Demo,給大家講解一下。
本節,主要分享自定義圖表的基本過程,不會涉及過於複雜的知識點。
咱們還是按照:需求、分析、設計、實現、總結這種方式給大家講解吧!!!
這樣大家也更容易看得懂。
***

需求

先上效果圖:
頁面1:曲線圖.gif

頁面2:動態曲線圖.gif

需求內容:
1.數據:
-- 模擬50天的霧霾數值吧,每天的數值是一個100以內的隨機數;
-- 以當前日期為最後一天,向前取50天的數據,也就是50條;
2.業務邏輯
-- 頁面載入時,請求數據,展示在圖表上;
-- 點擊【刷新】數據,重新請求數據,展示在圖表上;
3.View
-- 圖表背景色為暗灰色:#343643;
-- 圖表背景邊框線顏色為淺藍色:#999dd2;
-- 曲線顏色為藍色:#7176ff;
-- 文字顏色為白色;
-- 圖表可設置Padding值;
-- 圖表全量顯示數據,即適配顯示;
-- 曲線上的數值文本顯示在對應的位置;
-- X坐標軸左右分別顯示 開始和結束的日期,並與左右邊框線對齊;
-- 圖表應支持兩種查看方式:整體載入(全量載入) 和 逐條載入(動態載入)


分析

1.數據比較簡單,做個隨機數即可,略;
2.業務邏輯,較簡單,略;
3.View,本節的重點,需要詳細分析一下:
3.1 這種圖表控制項如何實現?

一般做法:使用畫布、畫筆進行繪製。 
如何繪製:使用畫筆在畫布上繪製圖形
(畫布類提供了很多畫圖的方法,畫筆可以設置各種筆觸效果)。


建議:大家最好提前瞭解一下畫布和畫筆的用法。

3.2 背景色如何繪製?

canvas.drawColor(參數:顏色)即可,很簡單,即:畫布直接填充背景顏色,不用畫筆。

3.3 背景邊框線如何實現?

方案1:先定義路徑Path,記錄每一個跟邊框線的信息,再使用canvas.drawPath進行繪製;
方案2:使用canvas.drawLine分別繪製每一條橫線和縱線;


建議:多線條時,canvas.drawPath管理更簡單,繪製會更方便一些。

3.4 曲線如何繪製?

我們可以看作二維坐標系,包含X軸和Y軸;
那麼,曲線的數據如何才能在坐標系中合適的顯示呢?
其實不難,我們可以根據畫布大小(或控制項大小(如果畫布尺寸等於控制項尺寸)),
計算出曲線的每個數據在X軸和Y軸的位置信息,然後將這些位置點連成線就可以了;
X軸應顯示數據的位置:
以圖表能適配全量數據為參考(也就是能顯示全部的數據,本Demo中就是50條霧霾數據的點):
X軸的長度應與數據總條數對應,那麼每一條數據在X軸的位置,應是:
    每條數據在X軸的間隔 = X軸長度 / 數據條數;
    每條數據在X軸的位置 = 第N條數據 * 間隔;
Y軸應顯示數據的位置:
以圖表能適配全量數據為參考,
Y軸的區域應能包含所有數據大小,那麼,我們需要先獲得數據的最大最小值與之對應,
每一條數據num在Y軸的位置,應是:
    每條數據的Y軸比率 = (num - min ) / (max - min);
    每條數據在Y軸的位置 = 比率 * Y軸長度;
獲得了數據在X、Y軸的位置,我們就可以繪製曲線了,
此處仍然使用Path收集每一個數據點的位置,同時使用曲線進行連接,
即path.quadTo(x1, y1,x2,y2)(該方法後面有介紹);
然後再畫布上繪製曲線路徑:canvas.drawPath(path,paint);

3.5 如何繪製文本?

使用canvas.drawText(text, x, y, paint);
不過x,y的位置的計算,稍微麻煩一些,大家可以看一下這篇文章的相關介紹:
https://www.jianshu.com/p/3e48dd0547a0
文章 -- 繪圖基礎 -- 繪製文本  

文本繪製原理
文本繪製差異:

文本繪製時並非從文本的左上角開始繪製,而是基於Baseline開始繪製。
舉例:
如果我們想在自定義控制項左上角位置繪製文本,
可能會這麼寫canvas.drawText("MfgiA", 0, 0, paint);
但是這麼寫,等運行出來,我們發現該控制項左上角只會顯示Baseline下麵的內容,
也就只能看到字母g的下半部分,
而其他部分,因為超出了自定義控制項上邊界,所以沒有被繪製出來。

如果不明白也不要緊,我們先學習主要的知識。
如果想把文本位置控制的特別精確,請務必參考該文章。

3.6 動態圖表如何繪製?
圖表的動態效果其實就是每隔一定時間重繪一次,也就是動態了(視頻效果也是這麼個原理);
之所以做成兩種效果(非動態/動態),主要是讓大家瞭解一下View和SurfaceView的用法差異。
主要差異如下:

View    
-- 僅能在主線程中刷新。
   缺點:如果繪製內容過多或頻率過高,會影響主線程FPS,造成頁面卡頓
-- 使用了單緩衝;
緩衝可以理解成對處理的包裝,舉個簡單易懂點的例子:
   工人搬磚
   工人有10000塊磚要從A區搬到B區,他每次搬一塊,要搬10000次,
   為了不想來回跑這麼多次,工人想了個辦法,找了個筐來背磚,每筐可以背100塊,
   這樣他就來回跑100次就行了,提高了搬磚效率。那麼,這個筐呢就是一個緩衝處理。

在View的繪製上也很容易理解,例如:我們使用畫筆按序(中間可有停頓)繪製多個圖形,
但是View並沒有一個個的去繪製,而是在一次draw方法中,全部繪製了出來。
因為,View也使用了緩衝處理。

SurfaceView   
-- 可在子線程中刷新;
   如果繪製的內容少,不建議使用,因為創建線程和緩衝區,也增加了記憶體。
   反之,推薦使用,但是要註意線程的管控。   
-- 使用了雙緩衝;
   繼續以工人搬磚的例子講解。
   工人轉身忽然看到了一輛卡車(一車能裝>1萬塊),心想這不更省事了麽,
   於是他先把一框框磚搬到了車上,再把車開到B區,卸磚。
   這輛車也就相當於第二次緩衝了。

在控制項繪製時實現雙緩衝一般可以這麼做:
1.新建一個臨時圖片,並創建其臨時畫布(畫布相當於那輛卡車);
2.將我們想繪製的內容,先繪製到臨時圖片的畫布上(即圖片上)
3.在控制項需要繪製時,再把圖片繪製到控制項的真正畫布上;
  
經過上面的對比分析,我們可以得出結論:
1.全量載入的圖表(曲線圖),使用View或SurfaceView來繪製都是可以的
  因為:繪製的信息適量,沒有特別的性能要求。
2.逐條載入的圖表(動態曲線圖),我們儘量使用SurfaceView來繪製
  因為:如果在View里使用線程sleep控制逐條載入,會導致主線程阻塞
  (也就是頁面看著卡頓半天,等阻塞恢復之後,再忽然繪製出來的效果)。
  如果想不卡頓,只能在View中使用線程或Timer來處理逐條效果,然後再與主線程進行通信。
  與其這麼麻煩,我們不如使用SurfaceView,直接能在子線程中刷新View不是更好嗎。

看完上面的介紹,相信大家對View與SurfaceView的區別和用法,也應該瞭解一些了。
那麼,咱們開始下一步吧。


設計

這一個功能實現相對複雜一些,我們最好對Demo進行一個簡單的分層或模塊設計。
分析我們的Demo應有的結構,主要包含

  1. 兩種自定義圖表控制項(View和SurfaceView)、
  2. 一些簡單的業務邏輯、
  3. 數據的處理。

那麼,咱們直接用現成的框架吧,MVC、MVP都是可以的,不過MVC、MVP用哪個好呢?
我們直接使用MVP吧,解耦比MVC更好一些。
此處就不畫架構圖了,直接文本表示吧:

M(數據層):

1. IChartData.java 圖表數據介面(提供了一個方法:獲得圖表數據)
2. ChartDataImpl.java 圖表數據實現類(實現了上面的介面)
3. ChartDataInfo.java 圖表數據實體類(封裝了兩個屬性:日期和數值)
4. ChartDateUtils.java 工具類(主要是日期格式的處理)

P(Presenter中間層):

1.ChartPresenter.java 用於連接M和V層,負責業務邏輯的處理,此處也就是:獲得了數據,交給UI

V(UI層)

1. IChartUI.java UI介面,提供了顯示圖表的方法,供Presenter使用
2. MainActivity.java UI介面的實現類,用於曲線圖的展示與交互
3. SurfaceChartActivity.java UI介面的實現類,用於動態曲線圖的展示與交互
4. ChartView.java 曲線圖控制項(直接使用畫布、畫筆繪製)
5. ChartSurfaceView.java 動態曲線圖控制項(使用Timer、線程池、線程、畫布、畫筆繪製)
6. DrawChartUtils.java 繪圖工具類(繪製的代碼主要封裝在該類裡面)

代碼結構圖

功能如何實現已經設計好了,那麼,開始下一步吧。
***

實現

  1. 數據層
    數據層主要使用隨機數模擬真實數據,沒有難的技術點,咱們僅把代碼貼出來吧
    1.1 圖表數據實體類
/**
 * 類:ChartDataInfo 圖表數據實體類
 * 作者: qxc
 * 日期:2018/4/18.
 */
public class ChartDataInfo {
    private String date;
    private int num;

    public ChartDataInfo(String date, int num) {
        this.date = date;
        this.num = num;
    }

    public String getDate() {
        return date;
    }

    public void setDate(String date) {
        this.date = date;
    }

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }
}

1.2 圖表數據介面

import java.util.List;
/**
 * 類:IChartData 圖表數據介面
 * 作者: qxc
 * 日期:2018/4/18.
 */
public interface IChartData {
    /**
     * 獲得圖表數據
     * @param size 數據條數
     * @return 數據集合
     */
    List<ChartDataInfo> getChartData(int size);
}

1.3 圖表數據實現類

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * 類:ChartDataImpl 圖表數據實現類
 * 作者: qxc
 * 日期:2018/4/18.
 */
public class ChartDataImpl implements IChartData{
    private int maxNum = 100;

    /**
     * 返回隨機的圖表數據
     * @param size 數據條數
     * @return 圖表數據集合
     */
    @Override
    public List<ChartDataInfo> getChartData(int size) {
        List<ChartDataInfo> data = new ArrayList<>();
        Random random = new Random();
        random.setSeed(ChartDateUtils.getDateNow());
        //返回maxNum以內的隨機數
        for(int i = size-1; i>=0 ; i--){
            ChartDataInfo dataInfo = new ChartDataInfo(ChartDateUtils.getDate(i), random.nextInt(maxNum));
            data.add(dataInfo);
        }
        return data;
    }
}

1.4 數據層工具類

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

/**
 * 類:DateUtils 數據層工具類
 * 1.日期的處理
 * 2.
 * 作者: qxc
 * 日期:2018/4/18.
 */
public class ChartDateUtils {
    public static long getDateNow(){
        Date date = new Date();
        return date.getTime();
    }

    public static String getDate(int day){
        Calendar calendar = Calendar.getInstance();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
        calendar.add(Calendar.DATE, -day);
        String date = sdf.format(calendar.getTime());
        return date;
    }
}
  1. Presenter層
    這一層就是標準的Presenter,持有M和V的介面,對他們的業務邏輯進行處理。
    2.1 ChartPresenter
import com.iwangzhe.mvpchart.model.ChartDataImpl;
import com.iwangzhe.mvpchart.model.ChartDataInfo;
import com.iwangzhe.mvpchart.model.IChartData;
import com.iwangzhe.mvpchart.view.IChartUI;

import java.util.List;

/**
 * 類:ChartPresenter
 * 作者: qxc
 * 日期:2018/4/18.
 */
public class ChartPresenter {
    private IChartUI iChartView;
    private IChartData iChartData;

    public ChartPresenter(IChartUI iChartView) {
        this.iChartView = iChartView;
        this.iChartData = new ChartDataImpl();
    }

    //獲取圖表數據的業務邏輯
    public void getChartData(){
        //請求的數據數量
        int size = 50;
        //獲得圖表數據
        List<ChartDataInfo> data = iChartData.getChartData(size);
        //把數據設置給UI
        iChartView.showChartData(data);
    }
}
  1. UI層(View)
    繪圖的技術是本文的核心點,需要重點講解
    3.1 IChartUI 介面
package com.iwangzhe.mvpchart.view;
import com.iwangzhe.mvpchart.model.ChartDataInfo;
import java.util.List;
/**
 * 類:IChartView
 * 作者: qxc
 * 日期:2018/4/18.
 */
public interface IChartUI {
    /**
     * 顯示圖表
     * @param data 數據
     */
    void showChartData(List<ChartDataInfo> data);
}

3.2 MainActivity
佈局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#000000">
    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#343643"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="10dp"
        android:text="  刷新ChartView數據  "
        android:textColor="#ffffff"
        android:textSize="18sp"/>
    <Button
        android:id="@+id/btnSurface"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#343643"
        android:layout_toRightOf="@+id/btn"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="10dp"
        android:text="   使用SurfaceView展示圖表   "
        android:textColor="#ffffff"
        android:textSize="18sp"/>
    <com.iwangzhe.mvpchart.view.customView.ChartView
        android:id="@+id/cv"
        android:layout_below="@+id/btn"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="8dp"/>
</RelativeLayout>

代碼

package com.iwangzhe.mvpchart.view;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import com.iwangzhe.mvpchart.R;
import com.iwangzhe.mvpchart.model.ChartDataInfo;
import com.iwangzhe.mvpchart.presenter.ChartPresenter;
import com.iwangzhe.mvpchart.view.customView.ChartView;

import java.util.List;

public class MainActivity extends Activity implements IChartUI {
    ChartPresenter chartPresenter;
    ChartView cv;
    Button btn;
    Button btnSurface;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //初始化presenter
        chartPresenter = new ChartPresenter(this);
        //初始化控制項
        initView();
        //初始化數據
        initData();
        //初始化事件
        initEvent();
    }

    //初始化控制項
    private void initView() {
        cv = (ChartView) findViewById(R.id.cv);
        btn = (Button) findViewById(R.id.btn);
        btnSurface = (Button) findViewById(R.id.btnSurface);
    }

    //初始化數據
    private void initData() {
        chartPresenter.getChartData();//請求數據
    }

    //初始化事件
    private void initEvent() {
        //刷新數據
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                chartPresenter.getChartData();//重新請求數據(刷新數據)
            }
        });
        //跳轉到動態曲線頁面
        btnSurface.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, SurfaceChartActivity.class);
                startActivity(intent);
            }
        });
    }

    //P層的數據回調
    @Override
    public void showChartData(List<ChartDataInfo> data) {       
        //圖表控制項設置數據源
        cv.setDataSet(data);
    }
}

3.3 SurfaceChartActivity
佈局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#000000">
    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#343643"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="10dp"
        android:text="    刷新SurfaceView數據    "
        android:textColor="#ffffff"
        android:textSize="18sp"/>
    <com.iwangzhe.mvpchart.view.customView.ChartSurfaceView
        android:id="@+id/cv"
        android:layout_below="@+id/btn"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="8dp"/>
</RelativeLayout>

代碼

package com.iwangzhe.mvpchart.view;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import com.iwangzhe.mvpchart.R;
import com.iwangzhe.mvpchart.model.ChartDataInfo;
import com.iwangzhe.mvpchart.presenter.ChartPresenter;
import com.iwangzhe.mvpchart.view.customView.ChartSurfaceView;
import java.util.List;
/**
 * 類:SurfaceChartActivity
 * 作者: qxc
 * 日期:2018/4/19.
 */
public class SurfaceChartActivity extends Activity implements IChartUI{
    ChartPresenter chartPresenter;
    ChartSurfaceView cv;
    Button btn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_surface_chart);
        //初始化presenter
        chartPresenter = new ChartPresenter(this);
        //初始化控制項
        initView();
        //初始化數據
        initData();
        //初始化事件
        initEvent();
    }

    //初始化控制項
    private void initView() {
        cv = (ChartSurfaceView) findViewById(R.id.cv);
        btn = (Button) findViewById(R.id.btn);
    }

    //初始化數據
    private void initData() {
        chartPresenter.getChartData();//請求數據
    }

    //初始化事件
    private void initEvent() {
        //刷新數據
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                chartPresenter.getChartData();//重新請求數據(刷新數據)
            }
        });
    }

    @Override
    public void showChartData(List<ChartDataInfo> data) {
        //圖表控制項設置數據源
        cv.setDataSource(data);
    }
}

3.4 ChartView

package com.iwangzhe.mvpchart.view.customView;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import com.iwangzhe.mvpchart.model.ChartDataInfo;
import java.util.List;
/**
 * 類:ChartView
 * 作者: qxc
 * 日期:2018/4/18.
 */
public class ChartView extends View{
    int canvasWidth;//畫布寬度
    int canvasHeight;//畫布高度
    int padding = 100;//邊界間隔
    Paint paint;//畫筆

    List<ChartDataInfo> data;//數據

    public ChartView(Context context, AttributeSet attrs) {
        super(context, attrs);
        //初始化畫筆屬性
        initPaint();
    }

    //設置圖表數據
    public void setDataSet(List<ChartDataInfo> data){
        this.data = data;

        //強制重繪
        invalidate();
    }

    //初始化畫筆屬性
    private void initPaint(){
        //設置防鋸齒
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        //繪製圖形樣式
        //Paint.Style.STROKE描邊
        //Paint.Style.FILL內容
        //Paint.Style.FILL_AND_STROKE內容+描邊
        paint.setStyle(Paint.Style.STROKE);
        //設置畫筆寬度
        paint.setStrokeWidth(1);
    }

    //每一次外觀變化,都會調用該方法
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //獲得畫布寬度
        this.canvasWidth = getWidth() - padding * 2;
        //獲得畫布高度
        this.canvasHeight = getHeight() - padding * 2;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //每次重繪,繪製圖表信息
        DrawChartUtils.getInstance().drawChart(canvas, paint, canvasWidth,canvasHeight,padding,data);
    }
}
該類中,
1.在onSizeChanged中獲得了畫布的寬度和高度,作為背景邊線和曲線數據的繪製區域
2.畫布的寬度和高度減去了padding信息(兩邊都需要有padding,所以乘以了2)
3.該View創建時,初始化了一支畫筆,設置了畫筆的一些屬性
4.在onSizeChanged方法執行後,都會執行onDraw方法進行繪製,該方法中可以獲得畫布
5.每次刷新數據,調用setDataSet方法後,也會強制執行onDraw方法進行繪製,因為invalidate方法會強制重繪
6.我們統一在onDraw方法中繪製圖表信息,而圖表信息的繪製封裝在DrawChartUtils類中

3.5 ChartSurfaceView

package com.iwangzhe.mvpchart.view.customView;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import com.iwangzhe.mvpchart.model.ChartDataInfo;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 類:ChartSurfaceView
 * 作者: qxc
 * 日期:2018/4/19.
 */
public class ChartSurfaceView extends SurfaceView implements SurfaceHolder.Callback{
    SurfaceHolder holder;
    Timer timer;
    List<ChartDataInfo> data;//總數據
    List<ChartDataInfo> showData;//當前繪製的數據
    ExecutorService threadPool;//線程池

    Canvas canvas;//畫布
    Paint paint;//畫筆
    int canvasWidth;//畫布寬度
    int canvasHeight;//畫布高度
    int padding = 100;//邊界間隔

    public ChartSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
        initPaint();
    }

    private void initView(){
        holder = getHolder();
        holder.addCallback(this);
        holder.setKeepScreenOn(true);
        threadPool = Executors.newCachedThreadPool();//緩存線程池
    }

    //初始化畫筆屬性
    private void initPaint(){
        //設置防鋸齒
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        //繪製圖形樣式
        //Paint.Style.STROKE描邊
        //Paint.Style.FILL內容
        //Paint.Style.FILL_AND_STROKE內容+描邊
        paint.setStyle(Paint.Style.STROKE);
        //設置畫筆寬度
        paint.setStrokeWidth(1);
    }

    //設置圖表數據源
    public void setDataSource(List<ChartDataInfo> data){
        this.data = data;
        this.showData = new ArrayList<>();

        if(timer!=null){
            timer.cancel();
        }
        if(canvasWidth > 0){
            startTimer();
        }
    }

    @Override
    public void surfaceCreated(SurfaceHolder surfaceHolder) {
        canvasWidth = getWidth() - padding * 2;
        canvasHeight = getHeight() - padding * 2;
        startTimer();
    }

    @Override
    public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
    }

    int index;
    private void startTimer(){
        index = 0;
        timer = new Timer();
        TimerTask task=new TimerTask() {
            @Override
            public void run() {
                index += 1;
                showData.clear();
                showData.addAll(data.subList(0,index));
                //開啟子線程 繪製頁面,並使用線程池管理
                threadPool.execute(new ChartRunnable());
                if(index>=data.size()){
                    timer.cancel();
                }
            }
        };
        timer.schedule(task, 0 , 20);
    }

    //子線程
    class ChartRunnable implements Runnable{
        @Override
        public void run() {
            //獲得畫布
            canvas = holder.lockCanvas();
            //繪製曲線圖形
            DrawChartUtils.getInstance().drawChart
             (canvas,paint,canvasWidth,canvasHeight,padding,showData);
            //提交畫布
            holder.unlockCanvasAndPost(canvas);
        }
    }
}
該類主要與ChartView 的差異就是,圖形繪製是在子線程中進行的
相同的東西,此處不再贅述,主要講一下差異性的內容:
1.需要實現SurfaceHolder.Callback,重寫3個方法
  surfaceCreated 當View創建成功會觸發,指示可以做繪圖工作了
  surfaceChanged 當View發生變化會觸發,一般可以在裡面數據參數的重新賦值處理;
  surfaceDestroyed 當View銷毀時會觸發,一般做一些銷毀前的處理工作,如線程等
2.此處的逐條載入是通過Timer實現的,每一個Timer周期,集合中多增加了一條數據,
  同時創建一個線程繪製一次,當所有的數據繪製完畢,取消timer;
3.使用timer,每個周期都創建了一個線程,那麼我們需要提高效率,應使用緩存線程池管控線程;
4.SurfaceView中的畫布獲取方式與View中不一樣
  View是在onDraw方法中直接獲取
  SurfaceView是通過holder.lockCanvas()獲得,繪製完畢,必須執行提交:
  holder.unlockCanvasAndPost(canvas);
  否則,頁面卡頓不動。

3.6 DrawChartUtils

package com.iwangzhe.mvpchart.view.customView;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import com.iwangzhe.mvpchart.model.ChartDataInfo;
import java.util.List;
/**
 * 類:ChartUtils
 * 作者: qxc
 * 日期:2018/4/19.
 */
public class DrawChartUtils {
    private Canvas canvas;//畫布
    private Paint paint;//畫筆
    private int canvasWidth;//畫布寬度
    private int canvasHeight;//畫布高度
    private int padding;//View邊界間隔

    private final String color_bg = "#343643";//背景色
    private final String color_bg_line = "#999dd2";//背景色
    private final String color_line = "#7176ff";//線顏色
    private final String color_text = "#ffffff";//文本顏色

    List<ChartDataInfo> showData;//圖表數據

    private static DrawChartUtils chartUtils;
    public static DrawChartUtils getInstance(){
        if(chartUtils == null){
            synchronized (DrawChartUtils.class){
                if(chartUtils == null){
                    chartUtils = new DrawChartUtils();
                }
            }
        }
        return chartUtils;
    }

    //繪製圖表
    public void drawChart(Canvas canvas, Paint paint, int canvasWidth, int canvasHeight, int padding, List<ChartDataInfo> showData) {
        //初始化畫布、畫筆等數據
        this.canvas = canvas;
        this.paint = paint;
        this.canvasWidth = canvasWidth;
        this.canvasHeight = canvasHeight;
        this.padding = padding;
        this.showData = showData;
        if(canvas == null || paint==null || canvasWidth<=0 ||canvasHeight<=0||showData==null || showData.size() ==0){
            return;
        }

        //繪製圖表背景
        drawBg();
        //繪製圖表線
        drawLine();
    }

    //繪製圖表背景
    private void drawBg(){
        //繪製背景色
        canvas.drawColor(Color.parseColor(color_bg));

        //繪製背景坐標軸線
        drawBgAxisLine();
    }

    //繪製圖表背景坐標軸線
    private void drawBgAxisLine(){
        //5條線:表示橫縱各畫5條線
        int lineNum = 5;
        Path path = new Path();

        //x、y軸間隔
        int x_space = canvasWidth / lineNum;
        int y_space = canvasHeight / lineNum;

        //畫橫線
        for(int i=0; i<=lineNum; i++){
            path.moveTo(0 + padding, i * y_space+ padding);
            path.lineTo(canvasWidth+ padding, i * y_space+ padding);
        }

        //畫縱線
        for(int i=0; i<=lineNum; i++){
            path.moveTo(i * x_space+ padding, 0 + padding);
            path.lineTo(i * x_space+ padding, canvasHeight+ padding);
        }

        //設置畫筆寬度、樣式、顏色
        paint.setStrokeWidth(2);
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(Color.parseColor(color_bg_line));
        //畫路徑
        canvas.drawPath(path, paint);
    }

    //繪製圖表線(數據曲線)
    private void drawLine(){
        if(showData == null){
            return;
        }
        int size = showData.size();

        //畫布自適應顯示數據(即:畫布的寬度應顯示全量的圖表數據)
        //x軸間隔
        float x_space = canvasWidth / size;
        //y軸最大最小值區間對應畫布高度(即畫布的高度應顯示全量的圖表數據)
        float max = getMaxData();
        float min = getMinData();

        float pre_x = 0;
        float pre_y = 0;
        Path path = new Path();

        //從左向右畫圖
        //將數值轉化成對應的坐標值
        for(int i=0; i<size; i++){
            float num = showData.get(i).getNum();
            float x = (i*x_space) + (x_space/2)+ padding;
            float y = (num-min)/(max - min)*canvasHeight+ padding;

            if(i == 0){
                path.moveTo(x,y);
            }else {
                path.quadTo(pre_x, pre_y, x, y);
            }
            pre_x = x;
            pre_y = y;
            drawText(String.valueOf(showData.get(i).getNum()),x,y);
        }

        //設置畫筆寬度、樣式、顏色
        paint.setStrokeWidth(5);
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(Color.parseColor(color_line));
        //畫路徑
        canvas.drawPath(path, paint);

        drawAxisXText();
    }

    //畫坐標軸文本
    private void drawAxisXText(){
        String start = showData.get(0).getDate();
        String end = showData.get(showData.size()-1).getDate();

        //設置畫筆寬度、樣式、文本大小、顏色
        paint.setStrokeWidth(2);
        paint.setStyle(Paint.Style.FILL);
        paint.setTextSize(40);
        paint.setColor(Color.parseColor(color_text));

        float width_text = paint.measureText(end);

        //開始文本位置
        float x_start = padding;
        float y_start = canvasHeight + padding - paint.descent() - paint.ascent() +10;
        //繪製開始文本
        canvas.drawText(start, x_start, y_start, paint);

        //結束文本位置
        float x_end = canvasWidth + padding - width_text;
        float y_end = canvasHeight + padding-paint.descent()-paint.ascent() +10;
        canvas.drawText(end, x_end, y_end, paint);
    }

    //畫線條文本
    private void drawText(String text, float x, float y){
        //設置畫筆寬度、樣式、文本大小、顏色
        paint.setStrokeWidth(2);
        paint.setStyle(Paint.Style.FILL);
        paint.setTextSize(30);
        paint.setColor(Color.parseColor(color_text));
        canvas.drawText(text, x, y, paint);
    }

    //獲得最大值:用於計算、適配Y軸區間
    private int getMaxData(){
        int max = showData.get(0).getNum();
        for(ChartDataInfo info : showData){
            max = info.getNum()>max?info.getNum():max;
        }
        return max;
    }

    //獲得最小值:用於計算、適配Y軸區間
    private int getMinData(){
        int min = showData.get(0).getNum();
        for(ChartDataInfo info : showData){
            min = info.getNum()<min?info.getNum():min;
        }
        return min;
    }
}
此類是個繪圖工具類,只是包括繪製的方法,而畫布、畫筆等參數需要外界傳入
1.getInstance方法,獲得該類的單例(線程安全的單例)
2.drawChart方法,是對外提供的繪圖入口方法
  接收外界傳參並判斷合法性
  調用繪製圖表背景的方法
  調用繪製圖表線的方法
3.drawBg,繪製背景方法,包含兩部分:背景色、背景邊框
  背景色是直接填充的方式,不用畫筆
4.drawBgAxisLine,繪製背景邊框線
  橫線縱線各畫5+1條,每一條線,我們可認為是畫筆走過的路徑,
  那麼,我們可以把每一條路徑封裝起來,放入集合中。
  我們不需要自己定義這種集合,直接使用系統提供的Path就可以了
  Path有幾個常用的方法:  
  MoveTo(float dx, float dy) 直接移動至某個點,中間不會產生連線;
  LineTo(float dx, float dy) 使用直線連接至某個點;
  QuadTo(float dx1, float dy1, float dx2, float dy2) 使用曲線連接至某個點(貝塞爾曲線);
  CubicTo(float x1,float y1,float x2,float y2,float x3,float y3)
  使用曲線連接至某個點,參數更多而已;
5.畫筆的設置,方法比較多,此處只列咱們用到的
  paint = new Paint(Paint.ANTI_ALIAS_FLAG);抗鋸齒,如不設置,界面粗糙有鋸齒效果;
  paint.setStrokeWidth(2);設置描邊的寬度
  paint.setStyle(STROKE);
  設置樣式,主要包括實心、描邊、實心和描邊3種類型,畫線一般設置成描邊即可;
  paint.setColor(Color.parseColor(color_bg_line));//設置顏色
6.drawLine畫曲線,主要將數據(集合index和數值大小)分別對應到坐標系的坐標
  X軸按照集合的下標平分X軸長度;
  Y軸根據最大最小值定位數值的位置;
  畫線仍然使用Path,要比每根曲線單獨畫要更合適一些;
7.繪製文本
  paint.setStyle(Paint.Style.FILL);
  畫筆可調整成實心,繪製文本更美觀,當然也可其他類型,請根據喜好自行調整;
  float width_text = paint.measureText(end);
  通過設置畫筆參數和文本內容,使用畫筆的measureText方法可以精確計算出文本的實際寬度;
  文本的坐標與其他圖形有差異,繪製位置是基於文本的Baseline,
  此處曲線文本的繪製時,文本位置未做精確處理;
  而日期的繪製時,文本位置是做了精確處理的;
  float y_start = canvasHeight + padding - paint.descent() - paint.ascent() +10;
  如果想對文本位置控制的更精確,請參考文章:https://www.jianshu.com/p/3e48dd0547a0

總結

本次分享涉及的技術點較多,再給大家簡單梳理一下:
-- MVP框架的應用;
-- 自定義View實現圖表;
-- 自定義SurfaceView實現圖表;
-- View和SurfaceView的主要差異和使用場景差異;
-- 畫布、畫筆、Path等畫圖類的使用;
-- Timer、Runnable、線程池的應用;

其他種類的圖形,思路基本上是一樣的。
如果還想做圖表控制項的交互,如數據拖動、觸摸、縮放、滑動定位等特效,需要大家再去多學學事件傳遞交互機制、GestureDetector、ScaleGestureDetector等技術。
以後要是有時間,也可再詳細給大家介紹一下。

本次Demo的下載地址:https://pan.baidu.com/s/1jm8lYrYEYovoS_iYLz4DRA
因為時間關係,Demo沒有做特別詳細的測試,如果有問題請大家自行調整。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言: MySQL 5.7中引入了一個新的sys schema,sys是一個MySQL自帶的系統庫,在安裝MySQL 5.7以後的版本,使用mysqld進行初始化時,會自動創建sys庫。 sys庫裡面的表、視圖、函數、存儲過程可以使我們更方便、快捷的瞭解到MySQL的一些信息,比如哪些語句使用了臨時 ...
  • 通過日常的工作記錄MySQL常用命令,不斷的學習,不斷總結,未完待續…… ...
  • 1.實現陰影或模糊邊效果方式: 2.通過shape來實現,具體是通過layer-list 多層疊放的方式實現的 使用: 頂部縮放了:android:top=2*5=10dp ...
  • 概述 移動端所說的AI,通常是指“機器學習”。 定義:機器學習其實就是研究電腦怎樣模擬人類的學習行為,以獲取新的知識或技能,並重新組織已有的知識結構使之不斷改善自身。從實踐的意義上來說,機器學習是一類從數據中自動分析獲得規律,並利用規律對未知數據進行預測的演算法。 目前,機器學習已經有了十分廣泛的應 ...
  • 可實現多種漸變、直角or弧角、進度條、載入條 (Various gradient, right or arc angle, progress bar and loading bar can be realized) Github地址 YangsBryant/BGradualProgress (Git ...
  • 單選滾動選擇器、diy豐富、有阻尼效果、簡單美觀、觸摸or點擊模式 (Rolling Selector, Diy Rich, Damping Effect, Simple and Beautiful, Touch or Click Mode) Github地址 YangsBryant/DSelect ...
  • EditText搜索結果下拉框、自動or回調模式、可diy、使用超簡便 (EditText search results drop-down box, auto or callback mode, diy, easy to use)#支持自動展示搜索條目 #支持手動展示搜索條目(可自己記錄歷史數據, ...
  • 前言 項目開發中,多少會遇到這種需求:獲得設備唯一標識DeviceId,用於: 1.標識一個唯一的設備,做數據精準下發或者數據統計分析; 2.賬號與設備綁定; 3..... 分析 這類文章,網上有許多資料,例如:使用IMEI、MAC等作為設備標識使用。 不過,看過這些文章或者深入調研的同學應該都清楚 ...
一周排行
    -Advertisement-
    Play Games
  • Timer是什麼 Timer 是一種用於創建定期粒度行為的機制。 與標準的 .NET System.Threading.Timer 類相似,Orleans 的 Timer 允許在一段時間後執行特定的操作,或者在特定的時間間隔內重覆執行操作。 它在分散式系統中具有重要作用,特別是在處理需要周期性執行的 ...
  • 前言 相信很多做WPF開發的小伙伴都遇到過表格類的需求,雖然現有的Grid控制項也能實現,但是使用起來的體驗感並不好,比如要實現一個Excel中的表格效果,估計你能想到的第一個方法就是套Border控制項,用這種方法你需要控制每個Border的邊框,並且在一堆Bordr中找到Grid.Row,Grid. ...
  • .NET C#程式啟動閃退,目錄導致的問題 這是第2次踩這個坑了,很小的編程細節,容易忽略,所以寫個博客,分享給大家。 1.第一次坑:是windows 系統把程式運行成服務,找不到配置文件,原因是以服務運行它的工作目錄是在C:\Windows\System32 2.本次坑:WPF桌面程式通過註冊表設 ...
  • 在分散式系統中,數據的持久化是至關重要的一環。 Orleans 7 引入了強大的持久化功能,使得在分散式環境下管理數據變得更加輕鬆和可靠。 本文將介紹什麼是 Orleans 7 的持久化,如何設置它以及相應的代碼示例。 什麼是 Orleans 7 的持久化? Orleans 7 的持久化是指將 Or ...
  • 前言 .NET Feature Management 是一個用於管理應用程式功能的庫,它可以幫助開發人員在應用程式中輕鬆地添加、移除和管理功能。使用 Feature Management,開發人員可以根據不同用戶、環境或其他條件來動態地控制應用程式中的功能。這使得開發人員可以更靈活地管理應用程式的功 ...
  • 在 WPF 應用程式中,拖放操作是實現用戶交互的重要組成部分。通過拖放操作,用戶可以輕鬆地將數據從一個位置移動到另一個位置,或者將控制項從一個容器移動到另一個容器。然而,WPF 中預設的拖放操作可能並不是那麼好用。為瞭解決這個問題,我們可以自定義一個 Panel 來實現更簡單的拖拽操作。 自定義 Pa ...
  • 在實際使用中,由於涉及到不同編程語言之間互相調用,導致C++ 中的OpenCV與C#中的OpenCvSharp 圖像數據在不同編程語言之間難以有效傳遞。在本文中我們將結合OpenCvSharp源碼實現原理,探究兩種數據之間的通信方式。 ...
  • 一、前言 這是一篇搭建許可權管理系統的系列文章。 隨著網路的發展,信息安全對應任何企業來說都越發的重要,而本系列文章將和大家一起一步一步搭建一個全新的許可權管理系統。 說明:由於搭建一個全新的項目過於繁瑣,所有作者將挑選核心代碼和核心思路進行分享。 二、技術選擇 三、開始設計 1、自主搭建vue前端和. ...
  • Csharper中的表達式樹 這節課來瞭解一下表示式樹是什麼? 在C#中,表達式樹是一種數據結構,它可以表示一些代碼塊,如Lambda表達式或查詢表達式。表達式樹使你能夠查看和操作數據,就像你可以查看和操作代碼一樣。它們通常用於創建動態查詢和解析表達式。 一、認識表達式樹 為什麼要這樣說?它和委托有 ...
  • 在使用Django等框架來操作MySQL時,實際上底層還是通過Python來操作的,首先需要安裝一個驅動程式,在Python3中,驅動程式有多種選擇,比如有pymysql以及mysqlclient等。使用pip命令安裝mysqlclient失敗應如何解決? 安裝的python版本說明 機器同時安裝了 ...