主頁 > 移動端開發 > Android自定義控制元件:圖形報表的實作(折線圖、曲線圖、動態曲線圖)(View與SurfaceView分別實作圖表控制元件)

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

2020-09-16 13:40:06 移動端開發

圖形報表很常用,因為展示資料比較直觀,常見的形式有很多,如:折線圖、柱形圖、餅圖、雷達圖、股票圖、還有一些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 = https://www.cnblogs.com/qixingchao/p/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 = https://www.cnblogs.com/qixingchao/p/new ChartDataImpl();
    }

    //獲取圖表資料的業務邏輯
    public void getChartData(){
        //請求的資料數量
        int size = 50;
        //獲得圖表資料
        List 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 = https://www.cnblogs.com/qixingchao/p/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 = https://www.cnblogs.com/qixingchao/p/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 = https://www.cnblogs.com/qixingchao/p/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; imax?info.getNum():max;
        }
        return max;
    }

    //獲得最小值:用于計算、適配Y軸區間
    private int getMinData(){
        int min = showData.get(0).getNum();
        for(ChartDataInfo info : showData){
            min = info.getNum()
此類是個繪圖工具類,只是包括繪制的方法,而畫布、畫筆等引數需要外界傳入
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沒有做特別詳細的測驗,如果有問題請大家自行調整,

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/57466.html

標籤:Android

上一篇:Android原生PDF功能實作:PDF閱讀、PDF頁面跳轉、PDF手勢伸縮、PDF目錄樹、PDF預覽縮略圖

下一篇:Android 設備唯一標識(多種實作方案)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more