圖形報表很常用浪耘,因為展示數(shù)據(jù)比較直觀乱灵,常見的形式有很多,如:折線圖点待、柱形圖阔蛉、餅圖、雷達圖癞埠、股票圖状原、還有一些3D效果的圖表等。
Android中也有不少第三方圖表庫苗踪,但是很難兼容各種各樣的需求颠区。
如果第三方庫不能滿足我們的需要,那么就需要自己去寫這么一個控件通铲。
往往在APP需求給定后毕莱,很多開發(fā)者卻無從下手,不知道該如何寫颅夺。
今天剛好抽出點時間朋截,做了個小Demo,給大家講解一下吧黄。
本節(jié)部服,主要分享自定義圖表的基本過程,不會涉及過于復雜的知識點拗慨。
咱們還是按照:需求廓八、分析、設計赵抢、實現(xiàn)剧蹂、總結(jié)這種方式給大家講解吧!7橙础宠叼!
這樣大家也更容易看得懂。
需求
先上效果圖:
需求內(nèi)容:
1.數(shù)據(jù):
-- 模擬50天的霧霾數(shù)值吧短绸,每天的數(shù)值是一個100以內(nèi)的隨機數(shù)车吹;
-- 以當前日期為最后一天筹裕,向前取50天的數(shù)據(jù)醋闭,也就是50條窄驹;
2.業(yè)務邏輯
-- 頁面加載時,請求數(shù)據(jù)证逻,展示在圖表上乐埠;
-- 點擊【刷新】數(shù)據(jù),重新請求數(shù)據(jù)囚企,展示在圖表上丈咐;
3.View
-- 圖表背景色為暗灰色:#343643;
-- 圖表背景邊框線顏色為淺藍色:#999dd2龙宏;
-- 曲線顏色為藍色:#7176ff棵逊;
-- 文字顏色為白色;
-- 圖表可設置Padding值;
-- 圖表全量顯示數(shù)據(jù)银酗,即適配顯示辆影;
-- 曲線上的數(shù)值文本顯示在對應的位置;
-- X坐標軸左右分別顯示 開始和結(jié)束的日期黍特,并與左右邊框線對齊蛙讥;
-- 圖表應支持兩種查看方式:整體加載(全量加載) 和 逐條加載(動態(tài)加載)
分析
1.數(shù)據(jù)比較簡單,做個隨機數(shù)即可灭衷,略次慢;
2.業(yè)務邏輯,較簡單翔曲,略迫像;
3.View,本節(jié)的重點瞳遍,需要詳細分析一下:
3.1 這種圖表控件如何實現(xiàn)闻妓?
一般做法:使用畫布、畫筆進行繪制傅蹂。
如何繪制:使用畫筆在畫布上繪制圖形
(畫布類提供了很多畫圖的方法纷闺,畫筆可以設置各種筆觸效果)。
建議:大家最好提前了解一下畫布和畫筆的用法份蝴。
3.2 背景色如何繪制犁功?
canvas.drawColor(參數(shù):顏色)即可,很簡單婚夫,即:畫布直接填充背景顏色浸卦,不用畫筆。
3.3 背景邊框線如何實現(xiàn)案糙?
方案1:先定義路徑Path限嫌,記錄每一個跟邊框線的信息靴庆,再使用canvas.drawPath進行繪制;
方案2:使用canvas.drawLine分別繪制每一條橫線和縱線怒医;
建議:多線條時炉抒,canvas.drawPath管理更簡單,繪制會更方便一些稚叹。
3.4 曲線如何繪制?
我們可以看作二維坐標系焰薄,包含X軸和Y軸;
那么扒袖,曲線的數(shù)據(jù)如何才能在坐標系中合適的顯示呢塞茅?
其實不難,我們可以根據(jù)畫布大屑韭省(或控件大幸笆荨(如果畫布尺寸等于控件尺寸)),
計算出曲線的每個數(shù)據(jù)在X軸和Y軸的位置信息飒泻,然后將這些位置點連成線就可以了鞭光;
X軸應顯示數(shù)據(jù)的位置:
以圖表能適配全量數(shù)據(jù)為參考(也就是能顯示全部的數(shù)據(jù),本Demo中就是50條霧霾數(shù)據(jù)的點):
X軸的長度應與數(shù)據(jù)總條數(shù)對應蠢络,那么每一條數(shù)據(jù)在X軸的位置衰猛,應是:
每條數(shù)據(jù)在X軸的間隔 = X軸長度 / 數(shù)據(jù)條數(shù);
每條數(shù)據(jù)在X軸的位置 = 第N條數(shù)據(jù) * 間隔刹孔;
Y軸應顯示數(shù)據(jù)的位置:
以圖表能適配全量數(shù)據(jù)為參考啡省,
Y軸的區(qū)域應能包含所有數(shù)據(jù)大小,那么髓霞,我們需要先獲得數(shù)據(jù)的最大最小值與之對應卦睹,
每一條數(shù)據(jù)num在Y軸的位置,應是:
每條數(shù)據(jù)的Y軸比率 = (num - min ) / (max - min);
每條數(shù)據(jù)在Y軸的位置 = 比率 * Y軸長度方库;
獲得了數(shù)據(jù)在X结序、Y軸的位置,我們就可以繪制曲線了纵潦,
此處仍然使用Path收集每一個數(shù)據(jù)點的位置徐鹤,同時使用曲線進行連接,
即path.quadTo(x1, y1,x2,y2)(該方法后面有介紹);
然后再畫布上繪制曲線路徑:canvas.drawPath(path,paint);
3.5 如何繪制文本邀层?
使用canvas.drawText(text, x, y, paint);
不過x,y的位置的計算返敬,稍微麻煩一些,大家可以看一下這篇文章的相關(guān)介紹:
http://www.reibang.com/p/3e48dd0547a0
文章 -- 繪圖基礎 -- 繪制文本
文本繪制差異:
文本繪制時并非從文本的左上角開始繪制寥院,而是基于Baseline開始繪制劲赠。
舉例:
如果我們想在自定義控件左上角位置繪制文本,
可能會這么寫canvas.drawText("MfgiA", 0, 0, paint);
但是這么寫,等運行出來凛澎,我們發(fā)現(xiàn)該控件左上角只會顯示Baseline下面的內(nèi)容霹肝,
也就只能看到字母g的下半部分,
而其他部分塑煎,因為超出了自定義控件上邊界沫换,所以沒有被繪制出來。
如果不明白也不要緊轧叽,我們先學習主要的知識苗沧。
如果想把文本位置控制的特別精確刊棕,請務必參考該文章炭晒。
3.6 動態(tài)圖表如何繪制?
圖表的動態(tài)效果其實就是每隔一定時間重繪一次甥角,也就是動態(tài)了(視頻效果也是這么個原理)网严;
之所以做成兩種效果(非動態(tài)/動態(tài)),主要是讓大家了解一下View和SurfaceView的用法差異嗤无。
主要差異如下:
View
-- 僅能在主線程中刷新震束。
缺點:如果繪制內(nèi)容過多或頻率過高,會影響主線程FPS当犯,造成頁面卡頓
-- 使用了單緩沖垢村;
緩沖可以理解成對處理的包裝,舉個簡單易懂點的例子:
工人搬磚
工人有10000塊磚要從A區(qū)搬到B區(qū)嚎卫,他每次搬一塊嘉栓,要搬10000次,
為了不想來回跑這么多次拓诸,工人想了個辦法侵佃,找了個筐來背磚,每筐可以背100塊奠支,
這樣他就來回跑100次就行了馋辈,提高了搬磚效率。那么倍谜,這個筐呢就是一個緩沖處理迈螟。
在View的繪制上也很容易理解,例如:我們使用畫筆按序(中間可有停頓)繪制多個圖形尔崔,
但是View并沒有一個個的去繪制答毫,而是在一次draw方法中,全部繪制了出來您旁。
因為烙常,View也使用了緩沖處理。
SurfaceView
-- 可在子線程中刷新;
如果繪制的內(nèi)容少蚕脏,不建議使用侦副,因為創(chuàng)建線程和緩沖區(qū),也增加了內(nèi)存驼鞭。
反之秦驯,推薦使用,但是要注意線程的管控挣棕。
-- 使用了雙緩沖译隘;
繼續(xù)以工人搬磚的例子講解。
工人轉(zhuǎn)身忽然看到了一輛卡車(一車能裝>1萬塊)洛心,心想這不更省事了么固耘,
于是他先把一框框磚搬到了車上,再把車開到B區(qū)词身,卸磚厅目。
這輛車也就相當于第二次緩沖了。
在控件繪制時實現(xiàn)雙緩沖一般可以這么做:
1.新建一個臨時圖片法严,并創(chuàng)建其臨時畫布(畫布相當于那輛卡車)损敷;
2.將我們想繪制的內(nèi)容,先繪制到臨時圖片的畫布上(即圖片上)
3.在控件需要繪制時深啤,再把圖片繪制到控件的真正畫布上拗馒;
經(jīng)過上面的對比分析,我們可以得出結(jié)論:
1.全量加載的圖表(曲線圖)溯街,使用View或SurfaceView來繪制都是可以的
因為:繪制的信息適量诱桂,沒有特別的性能要求。
2.逐條加載的圖表(動態(tài)曲線圖)苫幢,我們盡量使用SurfaceView來繪制
因為:如果在View里使用線程sleep控制逐條加載访诱,會導致主線程阻塞
(也就是頁面看著卡頓半天,等阻塞恢復之后韩肝,再忽然繪制出來的效果)触菜。
如果想不卡頓,只能在View中使用線程或Timer來處理逐條效果哀峻,然后再與主線程進行通信涡相。
與其這么麻煩,我們不如使用SurfaceView剩蟀,直接能在子線程中刷新View不是更好嗎催蝗。
看完上面的介紹,相信大家對View與SurfaceView的區(qū)別和用法育特,也應該了解一些了丙号。
那么先朦,咱們開始下一步吧。
設計
這一個功能實現(xiàn)相對復雜一些犬缨,我們最好對Demo進行一個簡單的分層或模塊設計喳魏。
分析我們的Demo應有的結(jié)構(gòu),主要包含
- 兩種自定義圖表控件(View和SurfaceView)怀薛、
- 一些簡單的業(yè)務邏輯刺彩、
- 數(shù)據(jù)的處理。
那么枝恋,咱們直接用現(xiàn)成的框架吧创倔,MVC、MVP都是可以的焚碌,不過MVC畦攘、MVP用哪個好呢?
我們直接使用MVP吧呐能,解耦比MVC更好一些念搬。
此處就不畫架構(gòu)圖了,直接文本表示吧:
M(數(shù)據(jù)層):
1. IChartData.java 圖表數(shù)據(jù)接口(提供了一個方法:獲得圖表數(shù)據(jù))
2. ChartDataImpl.java 圖表數(shù)據(jù)實現(xiàn)類(實現(xiàn)了上面的接口)
3. ChartDataInfo.java 圖表數(shù)據(jù)實體類(封裝了兩個屬性:日期和數(shù)值)
4. ChartDateUtils.java 工具類(主要是日期格式的處理)
P(Presenter中間層):
1.ChartPresenter.java 用于連接M和V層摆出,負責業(yè)務邏輯的處理,此處也就是:獲得了數(shù)據(jù)首妖,交給UI
V(UI層)
1. IChartUI.java UI接口偎漫,提供了顯示圖表的方法,供Presenter使用
2. MainActivity.java UI接口的實現(xiàn)類有缆,用于曲線圖的展示與交互
3. SurfaceChartActivity.java UI接口的實現(xiàn)類象踊,用于動態(tài)曲線圖的展示與交互
4. ChartView.java 曲線圖控件(直接使用畫布、畫筆繪制)
5. ChartSurfaceView.java 動態(tài)曲線圖控件(使用Timer棚壁、線程池杯矩、線程、畫布袖外、畫筆繪制)
6. DrawChartUtils.java 繪圖工具類(繪制的代碼主要封裝在該類里面)
功能如何實現(xiàn)已經(jīng)設計好了史隆,那么,開始下一步吧曼验。
實現(xiàn)
- 數(shù)據(jù)層
數(shù)據(jù)層主要使用隨機數(shù)模擬真實數(shù)據(jù)泌射,沒有難的技術(shù)點,咱們僅把代碼貼出來吧
1.1 圖表數(shù)據(jù)實體類
/**
* 類:ChartDataInfo 圖表數(shù)據(jù)實體類
* 作者: 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ù)實現(xiàn)類
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* 類:ChartDataImpl 圖表數(shù)據(jù)實現(xiàn)類
* 作者: qxc
* 日期:2018/4/18.
*/
public class ChartDataImpl implements IChartData{
private int maxNum = 100;
/**
* 返回隨機的圖表數(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)的隨機數(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;
}
}
- Presenter層
這一層就是標準的Presenter鬓照,持有M和V的接口熔酷,對他們的業(yè)務邏輯進行處理。
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è)務邏輯
public void getChartData(){
//請求的數(shù)據(jù)數(shù)量
int size = 50;
//獲得圖表數(shù)據(jù)
List<ChartDataInfo> data = iChartData.getChartData(size);
//把數(shù)據(jù)設置給UI
iChartView.showChartData(data);
}
}
- UI層(View)
繪圖的技術(shù)是本文的核心點豺裆,需要重點講解
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();//請求數(shù)據(jù)
}
//初始化事件
private void initEvent() {
//刷新數(shù)據(jù)
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
chartPresenter.getChartData();//重新請求數(shù)據(jù)(刷新數(shù)據(jù))
}
});
//跳轉(zhuǎn)到動態(tài)曲線頁面
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ù)據(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();//請求數(shù)據(jù)
}
//初始化事件
private void initEvent() {
//刷新數(shù)據(jù)
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
chartPresenter.getChartData();//重新請求數(shù)據(jù)(刷新數(shù)據(jù))
}
});
}
@Override
public void showChartData(List<ChartDataInfo> data) {
//圖表控件設置數(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;//畫布寬度
int canvasHeight;//畫布高度
int padding = 100;//邊界間隔
Paint paint;//畫筆
List<ChartDataInfo> data;//數(shù)據(jù)
public ChartView(Context context, AttributeSet attrs) {
super(context, attrs);
//初始化畫筆屬性
initPaint();
}
//設置圖表數(shù)據(jù)
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內(nèi)容
//Paint.Style.FILL_AND_STROKE內(nèi)容+描邊
paint.setStyle(Paint.Style.STROKE);
//設置畫筆寬度
paint.setStrokeWidth(1);
}
//每一次外觀變化拒秘,都會調(diào)用該方法
@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中獲得了畫布的寬度和高度躺酒,作為背景邊線和曲線數(shù)據(jù)的繪制區(qū)域
2.畫布的寬度和高度減去了padding信息(兩邊都需要有padding咙轩,所以乘以了2)
3.該View創(chuàng)建時,初始化了一支畫筆阴颖,設置了畫筆的一些屬性
4.在onSizeChanged方法執(zhí)行后活喊,都會執(zhí)行onDraw方法進行繪制,該方法中可以獲得畫布
5.每次刷新數(shù)據(jù)量愧,調(diào)用setDataSet方法后钾菊,也會強制執(zhí)行onDraw方法進行繪制,因為invalidate方法會強制重繪
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;//當前繪制的數(shù)據(jù)
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內(nèi)容
//Paint.Style.FILL_AND_STROKE內(nèi)容+描邊
paint.setStyle(Paint.Style.STROKE);
//設置畫筆寬度
paint.setStrokeWidth(1);
}
//設置圖表數(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));
//開啟子線程 繪制頁面煞烫,并使用線程池管理
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 的差異就是,圖形繪制是在子線程中進行的
相同的東西,此處不再贅述允跑,主要講一下差異性的內(nèi)容:
1.需要實現(xiàn)SurfaceHolder.Callback澄惊,重寫3個方法
surfaceCreated 當View創(chuàng)建成功會觸發(fā),指示可以做繪圖工作了
surfaceChanged 當View發(fā)生變化會觸發(fā)料饥,一般可以在里面數(shù)據(jù)參數(shù)的重新賦值處理;
surfaceDestroyed 當View銷毀時會觸發(fā)朱监,一般做一些銷毀前的處理工作岸啡,如線程等
2.此處的逐條加載是通過Timer實現(xiàn)的,每一個Timer周期赫编,集合中多增加了一條數(shù)據(jù)巡蘸,
同時創(chuàng)建一個線程繪制一次,當所有的數(shù)據(jù)繪制完畢擂送,取消timer;
3.使用timer悦荒,每個周期都創(chuàng)建了一個線程,那么我們需要提高效率嘹吨,應使用緩存線程池管控線程搬味;
4.SurfaceView中的畫布獲取方式與View中不一樣
View是在onDraw方法中直接獲取
SurfaceView是通過holder.lockCanvas()獲得,繪制完畢躺苦,必須執(zhí)行提交:
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;//圖表數(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) {
//初始化畫布匹厘、畫筆等數(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));
//繪制背景坐標軸線
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);
}
//繪制圖表線(數(shù)據(jù)曲線)
private void drawLine(){
if(showData == null){
return;
}
int size = showData.size();
//畫布自適應顯示數(shù)據(jù)(即:畫布的寬度應顯示全量的圖表數(shù)據(jù))
//x軸間隔
float x_space = canvasWidth / size;
//y軸最大最小值區(qū)間對應畫布高度(即畫布的高度應顯示全量的圖表數(shù)據(jù))
float max = getMaxData();
float min = getMinData();
float pre_x = 0;
float pre_y = 0;
Path path = new Path();
//從左向右畫圖
//將數(shù)值轉(zhuǎn)化成對應的坐標值
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);
//結(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);
}
//畫線條文本
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軸區(qū)間
private int getMaxData(){
int max = showData.get(0).getNum();
for(ChartDataInfo info : showData){
max = info.getNum()>max?info.getNum():max;
}
return max;
}
//獲得最小值:用于計算衰琐、適配Y軸區(qū)間
private int getMinData(){
int min = showData.get(0).getNum();
for(ChartDataInfo info : showData){
min = info.getNum()<min?info.getNum():min;
}
return min;
}
}
此類是個繪圖工具類也糊,只是包括繪制的方法,而畫布羡宙、畫筆等參數(shù)需要外界傳入
1.getInstance方法狸剃,獲得該類的單例(線程安全的單例)
2.drawChart方法,是對外提供的繪圖入口方法
接收外界傳參并判斷合法性
調(diào)用繪制圖表背景的方法
調(diào)用繪制圖表線的方法
3.drawBg狗热,繪制背景方法钞馁,包含兩部分:背景色、背景邊框
背景色是直接填充的方式匿刮,不用畫筆
4.drawBgAxisLine僧凰,繪制背景邊框線
橫線縱線各畫5+1條,每一條線僻焚,我們可認為是畫筆走過的路徑允悦,
那么,我們可以把每一條路徑封裝起來虑啤,放入集合中。
我們不需要自己定義這種集合架馋,直接使用系統(tǒng)提供的Path就可以了
Path有幾個常用的方法:
MoveTo(float dx, float dy) 直接移動至某個點狞山,中間不會產(chǎn)生連線;
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)
使用曲線連接至某個點,參數(shù)更多而已屏鳍;
5.畫筆的設置勘纯,方法比較多,此處只列咱們用到的
paint = new Paint(Paint.ANTI_ALIAS_FLAG);抗鋸齒钓瞭,如不設置驳遵,界面粗糙有鋸齒效果;
paint.setStrokeWidth(2);設置描邊的寬度
paint.setStyle(STROKE);
設置樣式山涡,主要包括實心堤结、描邊唆迁、實心和描邊3種類型,畫線一般設置成描邊即可竞穷;
paint.setColor(Color.parseColor(color_bg_line));//設置顏色
6.drawLine畫曲線唐责,主要將數(shù)據(jù)(集合index和數(shù)值大小)分別對應到坐標系的坐標
X軸按照集合的下標平分X軸長度瘾带;
Y軸根據(jù)最大最小值定位數(shù)值的位置鼠哥;
畫線仍然使用Path,要比每根曲線單獨畫要更合適一些看政;
7.繪制文本
paint.setStyle(Paint.Style.FILL);
畫筆可調(diào)整成實心朴恳,繪制文本更美觀,當然也可其他類型帽衙,請根據(jù)喜好自行調(diào)整菜皂;
float width_text = paint.measureText(end);
通過設置畫筆參數(shù)和文本內(nèi)容,使用畫筆的measureText方法可以精確計算出文本的實際寬度厉萝;
文本的坐標與其他圖形有差異恍飘,繪制位置是基于文本的Baseline,
此處曲線文本的繪制時谴垫,文本位置未做精確處理章母;
而日期的繪制時,文本位置是做了精確處理的翩剪;
float y_start = canvasHeight + padding - paint.descent() - paint.ascent() +10;
如果想對文本位置控制的更精確乳怎,請參考文章:http://www.reibang.com/p/3e48dd0547a0
總結(jié)
本次分享涉及的技術(shù)點較多,再給大家簡單梳理一下:
-- MVP框架的應用前弯;
-- 自定義View實現(xiàn)圖表蚪缀;
-- 自定義SurfaceView實現(xiàn)圖表;
-- View和SurfaceView的主要差異和使用場景差異恕出;
-- 畫布询枚、畫筆、Path等畫圖類的使用浙巫;
-- Timer金蜀、Runnable、線程池的應用的畴;
其他種類的圖形渊抄,思路基本上是一樣的。
如果還想做圖表控件的交互丧裁,如數(shù)據(jù)拖動护桦、觸摸、縮放渣慕、滑動定位等特效嘶炭,需要大家再去多學學事件傳遞交互機制抱慌、GestureDetector、ScaleGestureDetector等技術(shù)眨猎。
以后要是有時間抑进,也可再詳細給大家介紹一下。
本次Demo的下載地址:https://pan.baidu.com/s/1jm8lYrYEYovoS_iYLz4DRA
因為時間關(guān)系睡陪,Demo沒有做特別詳細的測試寺渗,如果有問題請大家自行調(diào)整。