Android自定義控件:圖形報(bào)表的實(shí)現(xiàn)(折線圖、曲線圖、動(dòng)態(tài)曲線圖)(View與SurfaceView分別實(shí)現(xiàn)圖表控件)

圖形報(bào)表很常用,因?yàn)檎故緮?shù)據(jù)比較直觀,常見(jiàn)的形式有很多,如:折線圖、柱形圖、餅圖、雷達(dá)圖、股票圖、還有一些3D效果的圖表等。
Android中也有不少第三方圖表庫(kù),但是很難兼容各種各樣的需求。
如果第三方庫(kù)不能滿足我們的需要,那么就需要自己去寫(xiě)這么一個(gè)控件。

往往在APP需求給定后,很多開(kāi)發(fā)者卻無(wú)從下手,不知道該如何寫(xiě)。
今天剛好抽出點(diǎn)時(shí)間,做了個(gè)小Demo,給大家講解一下。
本節(jié),主要分享自定義圖表的基本過(guò)程,不會(huì)涉及過(guò)于復(fù)雜的知識(shí)點(diǎn)。
咱們還是按照:需求、分析、設(shè)計(jì)、實(shí)現(xiàn)、總結(jié)這種方式給大家講解吧?。?!
這樣大家也更容易看得懂。


需求

先上效果圖:


頁(yè)面1:曲線圖.gif
頁(yè)面2:動(dòng)態(tài)曲線圖.gif

需求內(nèi)容:
1.數(shù)據(jù):
-- 模擬50天的霧霾數(shù)值吧,每天的數(shù)值是一個(gè)100以內(nèi)的隨機(jī)數(shù);
-- 以當(dāng)前日期為最后一天,向前取50天的數(shù)據(jù),也就是50條;
2.業(yè)務(wù)邏輯
-- 頁(yè)面加載時(shí),請(qǐng)求數(shù)據(jù),展示在圖表上;
-- 點(diǎn)擊【刷新】數(shù)據(jù),重新請(qǐng)求數(shù)據(jù),展示在圖表上;
3.View
-- 圖表背景色為暗灰色:#343643;
-- 圖表背景邊框線顏色為淺藍(lán)色:#999dd2;
-- 曲線顏色為藍(lán)色:#7176ff;
-- 文字顏色為白色;
-- 圖表可設(shè)置Padding值;
-- 圖表全量顯示數(shù)據(jù),即適配顯示;
-- 曲線上的數(shù)值文本顯示在對(duì)應(yīng)的位置;
-- X坐標(biāo)軸左右分別顯示 開(kāi)始和結(jié)束的日期,并與左右邊框線對(duì)齊;
-- 圖表應(yīng)支持兩種查看方式:整體加載(全量加載) 和 逐條加載(動(dòng)態(tài)加載)


分析

1.數(shù)據(jù)比較簡(jiǎn)單,做個(gè)隨機(jī)數(shù)即可,略;
2.業(yè)務(wù)邏輯,較簡(jiǎn)單,略;
3.View,本節(jié)的重點(diǎn),需要詳細(xì)分析一下:
3.1 這種圖表控件如何實(shí)現(xiàn)?

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


建議:大家最好提前了解一下畫(huà)布和畫(huà)筆的用法。

3.2 背景色如何繪制?

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

3.3 背景邊框線如何實(shí)現(xiàn)?

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


建議:多線條時(shí),canvas.drawPath管理更簡(jiǎn)單,繪制會(huì)更方便一些。

3.4 曲線如何繪制?

我們可以看作二維坐標(biāo)系,包含X軸和Y軸;
那么,曲線的數(shù)據(jù)如何才能在坐標(biāo)系中合適的顯示呢?
其實(shí)不難,我們可以根據(jù)畫(huà)布大?。ɑ蚩丶笮。ㄈ绻?huà)布尺寸等于控件尺寸)),
計(jì)算出曲線的每個(gè)數(shù)據(jù)在X軸和Y軸的位置信息,然后將這些位置點(diǎn)連成線就可以了;
X軸應(yīng)顯示數(shù)據(jù)的位置:
以圖表能適配全量數(shù)據(jù)為參考(也就是能顯示全部的數(shù)據(jù),本Demo中就是50條霧霾數(shù)據(jù)的點(diǎn)):
X軸的長(zhǎng)度應(yīng)與數(shù)據(jù)總條數(shù)對(duì)應(yīng),那么每一條數(shù)據(jù)在X軸的位置,應(yīng)是:
    每條數(shù)據(jù)在X軸的間隔 = X軸長(zhǎng)度 / 數(shù)據(jù)條數(shù);
    每條數(shù)據(jù)在X軸的位置 = 第N條數(shù)據(jù) * 間隔;
Y軸應(yīng)顯示數(shù)據(jù)的位置:
以圖表能適配全量數(shù)據(jù)為參考,
Y軸的區(qū)域應(yīng)能包含所有數(shù)據(jù)大小,那么,我們需要先獲得數(shù)據(jù)的最大最小值與之對(duì)應(yīng),
每一條數(shù)據(jù)num在Y軸的位置,應(yīng)是:
    每條數(shù)據(jù)的Y軸比率 = (num - min ) / (max - min);
    每條數(shù)據(jù)在Y軸的位置 = 比率 * Y軸長(zhǎng)度;
獲得了數(shù)據(jù)在X、Y軸的位置,我們就可以繪制曲線了,
此處仍然使用Path收集每一個(gè)數(shù)據(jù)點(diǎn)的位置,同時(shí)使用曲線進(jìn)行連接,
即path.quadTo(x1, y1,x2,y2)(該方法后面有介紹);
然后再畫(huà)布上繪制曲線路徑:canvas.drawPath(path,paint);

3.5 如何繪制文本?

使用canvas.drawText(text, x, y, paint);
不過(guò)x,y的位置的計(jì)算,稍微麻煩一些,大家可以看一下這篇文章的相關(guān)介紹:
http://www.itdecent.cn/p/3e48dd0547a0
文章 -- 繪圖基礎(chǔ) -- 繪制文本  
文本繪制原理

文本繪制差異:

文本繪制時(shí)并非從文本的左上角開(kāi)始繪制,而是基于Baseline開(kāi)始繪制。
舉例:
如果我們想在自定義控件左上角位置繪制文本,
可能會(huì)這么寫(xiě)canvas.drawText("MfgiA", 0, 0, paint);
但是這么寫(xiě),等運(yùn)行出來(lái),我們發(fā)現(xiàn)該控件左上角只會(huì)顯示Baseline下面的內(nèi)容,
也就只能看到字母g的下半部分,
而其他部分,因?yàn)槌隽俗远x控件上邊界,所以沒(méi)有被繪制出來(lái)。

如果不明白也不要緊,我們先學(xué)習(xí)主要的知識(shí)。
如果想把文本位置控制的特別精確,請(qǐng)務(wù)必參考該文章。

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

View    
-- 僅能在主線程中刷新。
   缺點(diǎn):如果繪制內(nèi)容過(guò)多或頻率過(guò)高,會(huì)影響主線程FPS,造成頁(yè)面卡頓
-- 使用了單緩沖;
緩沖可以理解成對(duì)處理的包裝,舉個(gè)簡(jiǎn)單易懂點(diǎn)的例子:
   工人搬磚
   工人有10000塊磚要從A區(qū)搬到B區(qū),他每次搬一塊,要搬10000次,
   為了不想來(lái)回跑這么多次,工人想了個(gè)辦法,找了個(gè)筐來(lái)背磚,每筐可以背100塊,
   這樣他就來(lái)回跑100次就行了,提高了搬磚效率。那么,這個(gè)筐呢就是一個(gè)緩沖處理。

在View的繪制上也很容易理解,例如:我們使用畫(huà)筆按序(中間可有停頓)繪制多個(gè)圖形,
但是View并沒(méi)有一個(gè)個(gè)的去繪制,而是在一次draw方法中,全部繪制了出來(lái)。
因?yàn)椋琕iew也使用了緩沖處理。

SurfaceView   
-- 可在子線程中刷新;
   如果繪制的內(nèi)容少,不建議使用,因?yàn)閯?chuàng)建線程和緩沖區(qū),也增加了內(nèi)存。
   反之,推薦使用,但是要注意線程的管控。   
-- 使用了雙緩沖;
   繼續(xù)以工人搬磚的例子講解。
   工人轉(zhuǎn)身忽然看到了一輛卡車(一車能裝>1萬(wàn)塊),心想這不更省事了么,
   于是他先把一框框磚搬到了車上,再把車開(kāi)到B區(qū),卸磚。
   這輛車也就相當(dāng)于第二次緩沖了。

在控件繪制時(shí)實(shí)現(xiàn)雙緩沖一般可以這么做:
1.新建一個(gè)臨時(shí)圖片,并創(chuàng)建其臨時(shí)畫(huà)布(畫(huà)布相當(dāng)于那輛卡車);
2.將我們想繪制的內(nèi)容,先繪制到臨時(shí)圖片的畫(huà)布上(即圖片上)
3.在控件需要繪制時(shí),再把圖片繪制到控件的真正畫(huà)布上;
  
經(jīng)過(guò)上面的對(duì)比分析,我們可以得出結(jié)論:
1.全量加載的圖表(曲線圖),使用View或SurfaceView來(lái)繪制都是可以的
  因?yàn)椋豪L制的信息適量,沒(méi)有特別的性能要求。
2.逐條加載的圖表(動(dòng)態(tài)曲線圖),我們盡量使用SurfaceView來(lái)繪制
  因?yàn)椋喝绻赩iew里使用線程sleep控制逐條加載,會(huì)導(dǎo)致主線程阻塞
  (也就是頁(yè)面看著卡頓半天,等阻塞恢復(fù)之后,再忽然繪制出來(lái)的效果)。
  如果想不卡頓,只能在View中使用線程或Timer來(lái)處理逐條效果,然后再與主線程進(jìn)行通信。
  與其這么麻煩,我們不如使用SurfaceView,直接能在子線程中刷新View不是更好嗎。

看完上面的介紹,相信大家對(duì)View與SurfaceView的區(qū)別和用法,也應(yīng)該了解一些了。
那么,咱們開(kāi)始下一步吧。


設(shè)計(jì)

這一個(gè)功能實(shí)現(xiàn)相對(duì)復(fù)雜一些,我們最好對(duì)Demo進(jìn)行一個(gè)簡(jiǎn)單的分層或模塊設(shè)計(jì)。
分析我們的Demo應(yīng)有的結(jié)構(gòu),主要包含

  1. 兩種自定義圖表控件(View和SurfaceView)、
  2. 一些簡(jiǎn)單的業(yè)務(wù)邏輯、
  3. 數(shù)據(jù)的處理。

那么,咱們直接用現(xiàn)成的框架吧,MVC、MVP都是可以的,不過(guò)MVC、MVP用哪個(gè)好呢?
我們直接使用MVP吧,解耦比MVC更好一些。
此處就不畫(huà)架構(gòu)圖了,直接文本表示吧:

M(數(shù)據(jù)層):

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

P(Presenter中間層):

1.ChartPresenter.java 用于連接M和V層,負(fù)責(zé)業(yè)務(wù)邏輯的處理,此處也就是:獲得了數(shù)據(jù),交給UI

V(UI層)

1. IChartUI.java UI接口,提供了顯示圖表的方法,供Presenter使用
2. MainActivity.java UI接口的實(shí)現(xiàn)類,用于曲線圖的展示與交互
3. SurfaceChartActivity.java UI接口的實(shí)現(xiàn)類,用于動(dòng)態(tài)曲線圖的展示與交互
4. ChartView.java 曲線圖控件(直接使用畫(huà)布、畫(huà)筆繪制)
5. ChartSurfaceView.java 動(dòng)態(tài)曲線圖控件(使用Timer、線程池、線程、畫(huà)布、畫(huà)筆繪制)
6. DrawChartUtils.java 繪圖工具類(繪制的代碼主要封裝在該類里面)
代碼結(jié)構(gòu)圖

功能如何實(shí)現(xiàn)已經(jīng)設(shè)計(jì)好了,那么,開(kāi)始下一步吧。


實(shí)現(xiàn)

  1. 數(shù)據(jù)層
    數(shù)據(jù)層主要使用隨機(jī)數(shù)模擬真實(shí)數(shù)據(jù),沒(méi)有難的技術(shù)點(diǎn),咱們僅把代碼貼出來(lái)吧
    1.1 圖表數(shù)據(jù)實(shí)體類
/**
 * 類:ChartDataInfo 圖表數(shù)據(jù)實(shí)體類
 * 作者: 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 圖表數(shù)據(jù)接口

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

1.3 圖表數(shù)據(jù)實(shí)現(xiàn)類

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

/**
 * 類:ChartDataImpl 圖表數(shù)據(jù)實(shí)現(xiàn)類
 * 作者: qxc
 * 日期:2018/4/18.
 */
public class ChartDataImpl implements IChartData{
    private int maxNum = 100;

    /**
     * 返回隨機(jī)的圖表數(shù)據(jù)
     * @param size 數(shù)據(jù)條數(shù)
     * @return 圖表數(shù)據(jù)集合
     */
    @Override
    public List<ChartDataInfo> getChartData(int size) {
        List<ChartDataInfo> data = new ArrayList<>();
        Random random = new Random();
        random.setSeed(ChartDateUtils.getDateNow());
        //返回maxNum以內(nèi)的隨機(jī)數(shù)
        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 數(shù)據(jù)層工具類

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

/**
 * 類:DateUtils 數(shù)據(jù)層工具類
 * 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層
    這一層就是標(biāo)準(zhǔn)的Presenter,持有M和V的接口,對(duì)他們的業(yè)務(wù)邏輯進(jìn)行處理。
    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();
    }

    //獲取圖表數(shù)據(jù)的業(yè)務(wù)邏輯
    public void getChartData(){
        //請(qǐng)求的數(shù)據(jù)數(shù)量
        int size = 50;
        //獲得圖表數(shù)據(jù)
        List<ChartDataInfo> data = iChartData.getChartData(size);
        //把數(shù)據(jù)設(shè)置給UI
        iChartView.showChartData(data);
    }
}
  1. UI層(View)
    繪圖的技術(shù)是本文的核心點(diǎn),需要重點(diǎn)講解
    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 數(shù)據(jù)
     */
    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數(shù)據(jù)  "
        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();
        //初始化數(shù)據(jù)
        initData();
        //初始化事件
        initEvent();
    }

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

    //初始化數(shù)據(jù)
    private void initData() {
        chartPresenter.getChartData();//請(qǐng)求數(shù)據(jù)
    }

    //初始化事件
    private void initEvent() {
        //刷新數(shù)據(jù)
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                chartPresenter.getChartData();//重新請(qǐng)求數(shù)據(jù)(刷新數(shù)據(jù))
            }
        });
        //跳轉(zhuǎn)到動(dòng)態(tài)曲線頁(yè)面
        btnSurface.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, SurfaceChartActivity.class);
                startActivity(intent);
            }
        });
    }

    //P層的數(shù)據(jù)回調(diào)
    @Override
    public void showChartData(List<ChartDataInfo> data) {       
        //圖表控件設(shè)置數(shù)據(jù)源
        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數(shù)據(jù)    "
        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();
        //初始化數(shù)據(jù)
        initData();
        //初始化事件
        initEvent();
    }

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

    //初始化數(shù)據(jù)
    private void initData() {
        chartPresenter.getChartData();//請(qǐng)求數(shù)據(jù)
    }

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

    @Override
    public void showChartData(List<ChartDataInfo> data) {
        //圖表控件設(shè)置數(shù)據(jù)源
        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;//畫(huà)布寬度
    int canvasHeight;//畫(huà)布高度
    int padding = 100;//邊界間隔
    Paint paint;//畫(huà)筆

    List<ChartDataInfo> data;//數(shù)據(jù)

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

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

        //強(qiáng)制重繪
        invalidate();
    }

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

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

    @Override
    protected void onDraw(Canvas canvas) {
        //每次重繪,繪制圖表信息
        DrawChartUtils.getInstance().drawChart(canvas, paint, canvasWidth,canvasHeight,padding,data);
    }
}
該類中,
1.在onSizeChanged中獲得了畫(huà)布的寬度和高度,作為背景邊線和曲線數(shù)據(jù)的繪制區(qū)域
2.畫(huà)布的寬度和高度減去了padding信息(兩邊都需要有padding,所以乘以了2)
3.該View創(chuàng)建時(shí),初始化了一支畫(huà)筆,設(shè)置了畫(huà)筆的一些屬性
4.在onSizeChanged方法執(zhí)行后,都會(huì)執(zhí)行onDraw方法進(jìn)行繪制,該方法中可以獲得畫(huà)布
5.每次刷新數(shù)據(jù),調(diào)用setDataSet方法后,也會(huì)強(qiáng)制執(zhí)行onDraw方法進(jìn)行繪制,因?yàn)閕nvalidate方法會(huì)強(qiáng)制重繪
6.我們統(tǒng)一在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;//總數(shù)據(jù)
    List<ChartDataInfo> showData;//當(dāng)前繪制的數(shù)據(jù)
    ExecutorService threadPool;//線程池

    Canvas canvas;//畫(huà)布
    Paint paint;//畫(huà)筆
    int canvasWidth;//畫(huà)布寬度
    int canvasHeight;//畫(huà)布高度
    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();//緩存線程池
    }

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

    //設(shè)置圖表數(shù)據(jù)源
    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));
                //開(kāi)啟子線程 繪制頁(yè)面,并使用線程池管理
                threadPool.execute(new ChartRunnable());
                if(index>=data.size()){
                    timer.cancel();
                }
            }
        };
        timer.schedule(task, 0 , 20);
    }

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

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;//畫(huà)布
    private Paint paint;//畫(huà)筆
    private int canvasWidth;//畫(huà)布寬度
    private int canvasHeight;//畫(huà)布高度
    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;//圖表數(shù)據(jù)

    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) {
        //初始化畫(huà)布、畫(huà)筆等數(shù)據(jù)
        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));

        //繪制背景坐標(biāo)軸線
        drawBgAxisLine();
    }

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

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

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

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

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

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

        //畫(huà)布自適應(yīng)顯示數(shù)據(jù)(即:畫(huà)布的寬度應(yīng)顯示全量的圖表數(shù)據(jù))
        //x軸間隔
        float x_space = canvasWidth / size;
        //y軸最大最小值區(qū)間對(duì)應(yīng)畫(huà)布高度(即畫(huà)布的高度應(yīng)顯示全量的圖表數(shù)據(jù))
        float max = getMaxData();
        float min = getMinData();

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

        //從左向右畫(huà)圖
        //將數(shù)值轉(zhuǎn)化成對(duì)應(yīng)的坐標(biāo)值
        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);
        }

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

        drawAxisXText();
    }

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

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

        float width_text = paint.measureText(end);

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

        //結(jié)束文本位置
        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);
    }

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

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

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

總結(jié)

本次分享涉及的技術(shù)點(diǎn)較多,再給大家簡(jiǎn)單梳理一下:
-- MVP框架的應(yīng)用;
-- 自定義View實(shí)現(xiàn)圖表;
-- 自定義SurfaceView實(shí)現(xiàn)圖表;
-- View和SurfaceView的主要差異和使用場(chǎng)景差異;
-- 畫(huà)布、畫(huà)筆、Path等畫(huà)圖類的使用;
-- Timer、Runnable、線程池的應(yīng)用;

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

本次Demo的下載地址:https://pan.baidu.com/s/1jm8lYrYEYovoS_iYLz4DRA
因?yàn)闀r(shí)間關(guān)系,Demo沒(méi)有做特別詳細(xì)的測(cè)試,如果有問(wèn)題請(qǐng)大家自行調(diào)整。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,881評(píng)論 25 709
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,161評(píng)論 4 61
  • 這里是一個(gè)普通人的世界, 孤要將自我表達(dá)在這虛無(wú)之地, 首先,要相信什么都是可以選擇的, 做自己的選擇并對(duì)選擇負(fù)責(zé)...
    十之島文閱讀 124評(píng)論 0 0
  • 今晚,看了一部電影《尋夢(mèng)環(huán)游記》,非常好看,好像給我打開(kāi)了另外一個(gè)世界。 本片講述了小男孩米格爾一...
    曾婭閱讀 990評(píng)論 0 6
  • 幫你讀書(shū)系列:《瓦爾登湖》 《瓦爾登湖》是美國(guó)作家梭羅獨(dú)居瓦爾登湖畔的記錄,全書(shū)事無(wú)巨細(xì)的描繪了他兩年多時(shí)間里的所...
    fe3654babd41閱讀 1,479評(píng)論 0 5

友情鏈接更多精彩內(nèi)容