Android 曲線圖的繪制示例代碼
本文介紹了Android 曲線圖的繪制示例代碼,分享給大家,具體如下:
效果展示

效果展示.gif
使用方式
// 初始化數(shù)據(jù)表格相關(guān)
with(mTableView) {
// 配置坐標(biāo)系
setupCoordinator("日", "人", /*這里是橫坐標(biāo)的值*/0f, 5f, 10f, 15f, 20f, 25f, 30f)
// 添加曲線, 確保縱坐標(biāo)的數(shù)值位數(shù)相等
addWave(ContextCompat.getColor(this@MainActivity, R.color.colorYellow), false,
0f, 10f, 30f, 54f, 30f, 100f, 10f)
addWave(ContextCompat.getColor(this@MainActivity, R.color.colorGreen), false,
0f, 30f, 20f, 20f, 46f, 25f, 5f)
addWave(ContextCompat.getColor(this@MainActivity, R.color.colorPink), false,
0f, 30f, 20f, 50f, 46f, 30f, 30f)
addWave(Color.parseColor("#8596dee9"), true,
0f, 15f, 10f, 10f, 40f, 20f, 5f)
}
實(shí)現(xiàn)思路
- 橫坐標(biāo)是固定的, 縱坐標(biāo)需要跟隨曲線傳入的數(shù)值去動(dòng)態(tài)的調(diào)整
- 繪制坐標(biāo)軸: 縱橫交錯(cuò)的網(wǎng)格
- 根據(jù)用戶傳入坐標(biāo)數(shù)值去繪制坐標(biāo)軸上的數(shù)值
- 給X軸和Y軸添加單位信息
- 根據(jù)用戶傳入的具體的數(shù)值繪制曲線(這里不采用Bezier, 不容易精確的控制頂點(diǎn)的位置)
- 繪制填充效果
- 添加屬性動(dòng)畫
代碼實(shí)現(xiàn)
/**
* Created by FrankChoo on 2017/12/29.
* Email: frankchoochina@gmail.com
* Version: 1.0
* Description: 表格自定義View
*/
public class TableView extends View {
private List<WaveConfigData> mWaves;// 數(shù)值集合
// 坐標(biāo)軸的數(shù)值
private int mCoordinateYCount = 8;
private float[] mCoordinateXValues;// 外界傳入
private float[] mCoordinateYValues;// 動(dòng)態(tài)計(jì)算
// 坐標(biāo)的單位
private String mXUnit;
private String mYUnit;
// 所有曲線中所有數(shù)據(jù)中的最大值
private float mGlobalMaxValue;// 用于確認(rèn)是否需要調(diào)整坐標(biāo)系
private Paint mCoordinatorPaint;
private Paint mTextPaint;
private Paint mWrapPaint;
// 坐標(biāo)軸上描述性文字的空間大小
private int mTopUnitHeight;// 頂部Y軸單位高度
private int mBottomTextHeight;
private int mLeftTextWidth;
// 網(wǎng)格尺寸
private int mGridWidth, mGridHeight;
private float mAnimProgress;
public TableView(Context context) {
this(context, null);
}
public TableView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public TableView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
post(new Runnable() {
@Override
public void run() {
showAnimator();
}
});
}
private void init() {
// 初始化數(shù)據(jù)集合的容器
mWaves = new ArrayList<>();
// 坐標(biāo)系的單位
mBottomTextHeight = dp2px(40);// X軸底部字體的高度
mLeftTextWidth = mBottomTextHeight;// Y軸左邊字體的寬度
mTopUnitHeight = dp2px(30);// 頂部Y軸的單位
// 初始化坐標(biāo)軸Paint
mCoordinatorPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mCoordinatorPaint.setColor(Color.LTGRAY);
// 初始化文本Paint
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mTextPaint.setColor(Color.GRAY);
mTextPaint.setTextSize(sp2px(12));
// 初始化曲線Paint
mWrapPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mWrapPaint.setPathEffect(new CornerPathEffect(200f));
}
/**
* 配置坐標(biāo)軸信息
*
* @param xUnit X 軸的單位
* @param yUnit Y 軸的單位
* @param coordinateXValues X 坐標(biāo)軸上的數(shù)值
*/
public void setupCoordinator(String xUnit, String yUnit, float... coordinateXValues) {
mXUnit = xUnit;
mYUnit = yUnit;
mCoordinateXValues = coordinateXValues;
}
/**
* 添加一條曲線, 確保與橫坐標(biāo)的數(shù)值對(duì)應(yīng)
*
* @param color
* @param isCoverRegion
* @param values
*/
public void addWave(int color, boolean isCoverRegion, float... values) {
mWaves.add(new WaveConfigData(color, isCoverRegion, values));
// 根據(jù)value的值去計(jì)算縱坐標(biāo)的數(shù)值
float maxValue = 0;
for (float value : values) {
maxValue = Math.max(maxValue, value);
}
if (maxValue < mGlobalMaxValue) return;
mGlobalMaxValue = maxValue;
// 保證網(wǎng)格的數(shù)值都為 5 的倍數(shù)
float gridValue = mGlobalMaxValue / (mCoordinateYCount - 1);
if (gridValue % 5 != 0) {
gridValue += 5 - (gridValue % 5);
}
// 給縱坐標(biāo)的數(shù)值賦值
mCoordinateYValues = new float[mCoordinateYCount];
for (int i = 0; i < mCoordinateYCount; i++) {
mCoordinateYValues[i] = i * gridValue;
}
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
drawCoordinate(canvas);
drawWrap(canvas);
}
public void showAnimator() {
ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f).setDuration(1000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mAnimProgress = (float) animation.getAnimatedValue();
invalidate();
}
});
animator.start();
}
/**
* 繪制坐標(biāo)系
*/
private void drawCoordinate(Canvas canvas) {
Point start = new Point();
Point stop = new Point();
// 1. 繪制橫軸線和縱坐標(biāo)單位
int xLineCount = mCoordinateYValues.length;
mGridHeight = (getHeight() - getPaddingTop() - getPaddingBottom() - mBottomTextHeight - mTopUnitHeight) / (xLineCount - 1);
for (int i = 0; i < xLineCount; i++) {
start.x = getPaddingLeft() + mLeftTextWidth;
start.y = getHeight() - getPaddingBottom() - mBottomTextHeight - mGridHeight * i;
stop.x = getRight() - getPaddingRight();
stop.y = start.y;
// 繪制橫軸線
canvas.drawLine(start.x, start.y, stop.x, stop.y, mCoordinatorPaint);
// 繪制縱坐標(biāo)單位
if (i == 0) continue;
String drawText = String.valueOf((int) mCoordinateYValues[i]);
Paint.FontMetricsInt fontMetrics = mTextPaint.getFontMetricsInt();
float offsetY = ((fontMetrics.bottom - fontMetrics.top) / 2 + fontMetrics.bottom) / 2;
float baseLine = start.y + offsetY;
float left = getPaddingLeft() + mLeftTextWidth / 2 - mTextPaint.measureText(drawText) / 2;
canvas.drawText(drawText, left, baseLine, mTextPaint);
// 繪制Y軸單位
if (i == xLineCount - 1) {
drawText = mYUnit;
baseLine = getPaddingTop() + mTopUnitHeight / 2;
canvas.drawText(drawText, left, baseLine, mTextPaint);
}
}
// 2. 繪制縱軸線和橫坐標(biāo)單位
int yLineCount = mCoordinateXValues.length;
mGridWidth = (getWidth() - getPaddingLeft() - getPaddingRight() - mLeftTextWidth) / (yLineCount - 1);
for (int i = 0; i < yLineCount; i++) {
start.x = getPaddingTop() + mLeftTextWidth + mGridWidth * i;
start.y = getPaddingTop() + mTopUnitHeight;
stop.x = start.x;
stop.y = getHeight() - mBottomTextHeight - getPaddingBottom();
// 繪制縱軸線
canvas.drawLine(start.x, start.y, stop.x, stop.y, mCoordinatorPaint);
// 繪制橫坐標(biāo)單位
String drawText = String.valueOf((int) mCoordinateXValues[i]);
Paint.FontMetricsInt fontMetrics = mTextPaint.getFontMetricsInt();
float offsetY = ((fontMetrics.bottom - fontMetrics.top) / 2 + fontMetrics.bottom) / 2;
float baseLine = getHeight() - getPaddingBottom() - mBottomTextHeight / 2 + offsetY;
float left = start.x - mTextPaint.measureText(drawText) / 2;
// 繪制X軸單位
if (i == 0) {
drawText = mXUnit;
left = getPaddingLeft() + mLeftTextWidth / 2 - mTextPaint.measureText(drawText) / 2;
}
canvas.drawText(drawText, left, baseLine, mTextPaint);
}
}
/**
* 繪制曲線
*/
private void drawWrap(Canvas canvas) {
canvas.clipRect(new RectF(
mLeftTextWidth,
getPaddingTop() + mTopUnitHeight,
(getRight() - getPaddingRight()) * mAnimProgress,
getHeight() - getPaddingBottom() - mBottomTextHeight)
);
float yHeight = mGridHeight * (mCoordinateYCount - 1);
for (WaveConfigData wave : mWaves) {
Path path = new Path();
path.moveTo(0, getHeight());
float maxY = mCoordinateYValues[mCoordinateYCount - 1];// Y軸坐標(biāo)的最大值
for (int index = 1; index < wave.values.length; index++) {
path.lineTo(
mLeftTextWidth + mGridWidth * index,
getHeight() - getPaddingBottom() - mBottomTextHeight
- yHeight * (wave.values[index] / maxY)
);
}
if (wave.isCoverRegion) {
mWrapPaint.setStyle(Paint.Style.FILL);
path.lineTo(getRight() - getPaddingRight(), getHeight());
path.close();
} else {
mWrapPaint.setStyle(Paint.Style.STROKE);
mWrapPaint.setStrokeWidth(10);
}
mWrapPaint.setColor(wave.color);
canvas.drawPath(path, mWrapPaint);
}
}
private int dp2px(float dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
dp, getResources().getDisplayMetrics());
}
private int sp2px(float sp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
sp, getResources().getDisplayMetrics());
}
public static class WaveConfigData {
int color;
boolean isCoverRegion;
float values[];
public WaveConfigData(int color, boolean isCoverRegion, float[] values) {
this.color = color;
this.isCoverRegion = isCoverRegion;
this.values = values;
}
}
}
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android RecycleView和線型布局制作聊天布局
大家好,本篇文章主要講的是Android RecycleView和線型布局制作聊天布局,感興趣的同學(xué)趕緊來(lái)看一看吧,對(duì)你有幫助的話記得收藏一下2022-01-01
基于DownloadManager的簡(jiǎn)單下載器編寫小結(jié)
Android自帶的DownloadManager是一個(gè)很好的下載文件的工具。該類在API level 9之后出現(xiàn),它已經(jīng)幫我們處理了下載失敗、重新下載等功能,整個(gè)下載過(guò)程全部交給系統(tǒng)負(fù)責(zé),不需要我們過(guò)多的處理,非常的nice。關(guān)鍵的是用起來(lái)也很簡(jiǎn)單,稍微封裝一下就可以幾句話搞定下載2017-12-12
Android開(kāi)發(fā)手冊(cè)TextInputLayout樣式使用示例
這篇文章主要為大家介紹了Android開(kāi)發(fā)手冊(cè)TextInputLayout樣式使用示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06
Android開(kāi)發(fā)系列二之窗口Activity的生命周期
這篇文章主要介紹了Android學(xué)習(xí)系列二之窗口Activity的生命周期的相關(guān)資料,需要的朋友可以參考下2016-05-05
Android實(shí)現(xiàn)顯示系統(tǒng)實(shí)時(shí)時(shí)間
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)顯示系統(tǒng)實(shí)時(shí)時(shí)間,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-05-05
基于Retrofit2+RxJava2實(shí)現(xiàn)Android App自動(dòng)更新
這篇文章主要為大家詳細(xì)介紹了基于Retrofit2+RxJava2實(shí)現(xiàn)Android App自動(dòng)更新,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-05-05
Android PickerView滾動(dòng)選擇器的使用方法
這篇文章主要為大家詳細(xì)介紹了Android PickerView滾動(dòng)選擇器的使用方法,感興趣的小伙伴們可以參考一下2016-03-03
Android ListView列表優(yōu)化的方法詳解
列表 ListView 是應(yīng)用中最為常見(jiàn)的組件,而列表往往也會(huì)承載很多元素,這時(shí)就需要對(duì)其進(jìn)行優(yōu)化。本文介紹了 Flutter ListView 的4個(gè)優(yōu)化要點(diǎn),非常實(shí)用,需要的可以參考一下2022-05-05
Android中搜索圖標(biāo)和文字居中的EditText實(shí)例
本篇文章主要介紹了Android中搜索圖標(biāo)和文字居中的EditText實(shí)例,具有一定的參考價(jià)值,有興趣的可以了解一下2017-06-06

