項(xiàng)目開發(fā)中時(shí)不時(shí)會碰到柱狀圖、折線圖、餅狀圖等效果,這些效果肯定是需要自定義控件通過繪制或者擺放來實(shí)現(xiàn),當(dāng)然了,也有一些很不錯(cuò)的第三方庫,比如MPAndroid、hellocharts等,里面就實(shí)現(xiàn)了柱狀圖、折線圖、餅狀圖等各種效果,甚至還有k線圖效果;這里自定義柱狀圖的目的是為了熟悉Android的自定義view、Canvas繪制等知識,提升自己的Android開發(fā)水平等;先來看下大致實(shí)現(xiàn)的一個(gè)效果:
通過看效果,大致需要繪制實(shí)現(xiàn)下面這些東西:
1、標(biāo)題的繪制
2、橫軸、縱軸的繪制
3、橫軸/縱軸刻度 箭頭 文字的繪制,縱軸還有測度的繪制
4、柱狀圖的繪制
而對于自定義view來說,首先繼承自view,初始化參數(shù)和自定義屬性,測量,繪制...大致就是一個(gè)這樣的流程;老規(guī)矩還是先看初始這一步;
public class HistogramView extends View {
//圖表標(biāo)題
private String graphTitle = "";
//標(biāo)題字體的大小
private int graphTitleSize = 18;
//標(biāo)題的字體顏色
private int graphTitleColor = Color.RED;
//x軸名稱
private String xAxisName = "";
//y軸名稱
private String yAxisName = "";
//坐標(biāo)軸字體顏色
private int axisTextSize = 12;
//坐標(biāo)軸字體顏色
private int axisTextColor = Color.BLACK;
//x y坐標(biāo)線條的顏色
private int axisLineColor = Color.BLACK;
//x,y坐標(biāo)線的寬度
private int axisLineWidth = 2;
private Paint mPaint;
private int screenWith, screenHeight;
//視圖的寬度
private int width;
//視圖的高度
private int height;
//起點(diǎn)x坐標(biāo)值
private int originalX;
//起點(diǎn)y坐標(biāo)值
private int originalY;
//y軸等份劃分
private int axisDivideSizeY;
//標(biāo)題距離x軸的距離
private int titleMarginXaxis = 60;
//x y軸刻度的高度
private int xAxisScaleHeight = 5;
//刻度的最大值
private Integer maxValue;
//y軸空留部分高度
private int yMarign = 30;
//柱狀圖數(shù)據(jù)
private List<Integer> columnList;
//柱狀圖顏色
private List<Integer> columnColors;
public HistogramView(Context context) {
this(context, null);
}
public HistogramView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public HistogramView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//獲取屏幕的寬高
WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics metrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(metrics);
screenWith = metrics.widthPixels;
screenHeight = metrics.heightPixels;
initAttrs(context, attrs);
initPaint();
}
/**
* //獲取自定義屬性
*/
private void initAttrs(Context context, AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.HistogramView);
graphTitle = array.getString(R.styleable.HistogramView_graphTitle);
xAxisName = array.getString(R.styleable.HistogramView_xAxisName);
yAxisName = array.getString(R.styleable.HistogramView_yAxisName);
axisTextSize = array.getDimensionPixelSize(R.styleable.HistogramView_axisTextSize, sp2px(axisTextSize));
axisTextColor = array.getColor(R.styleable.HistogramView_axisTextColor, axisTextColor);
axisLineColor = array.getColor(R.styleable.HistogramView_axisLineColor, axisLineColor);
graphTitleSize = array.getDimensionPixelSize(R.styleable.HistogramView_graphTitleSize, sp2px(graphTitleSize));
graphTitleColor = array.getColor(R.styleable.HistogramView_graphTitleColor, graphTitleColor);
axisLineWidth = (int) array.getDimension(R.styleable.HistogramView_axisLineWidth, dip2px(axisLineWidth));
array.recycle();
}
/**
* 初始化paint
*/
private void initPaint() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setDither(true);
}
private int sp2px(int sp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
}
private int dip2px(int dip) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, getResources().getDisplayMetrics());
}
}
就是一些常量、成員變量的定義和賦值,初始化自定義屬性和畫筆,接下來還是測量,那就看看onMeasure方法;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int w = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode == MeasureSpec.AT_MOST) {
w = screenWith;
}
int h = MeasureSpec.getSize(heightMeasureSpec);
if (heightMode == MeasureSpec.AT_MOST) {
h = screenHeight;
}
setMeasuredDimension(w, h);
if (width == 0 || height == 0) {
//x軸的起點(diǎn)位置
originalX = dip2px(30);
//視圖的寬度 空間的寬度減去左邊和右邊的位置
width = getMeasuredWidth() - originalX * 2;
//y軸的起點(diǎn)位置 空間高度的2/3
originalY = getMeasuredHeight() * 2 / 3;
//圖表顯示的高度為空間高度的一半
height = getMeasuredHeight() / 2;
}
}
onMeasure方法有時(shí)候會多次調(diào)用,所以當(dāng)試圖的width和height賦值后,就沒有必要再去計(jì)算一些值了,對于Android屏幕來說,它的原點(diǎn)x、y和生活的坐標(biāo)軸x,y有點(diǎn)一樣,左上角頂點(diǎn)是它的原點(diǎn)x,y;x軸往右邊走還是一樣的增大,y軸往下走就不一樣了,往上走是增大,往上走是減小的;
A點(diǎn)是屏幕的原點(diǎn),B點(diǎn)是自定義柱狀圖的原始點(diǎn),就需要對B點(diǎn)原始點(diǎn)進(jìn)行定義,再根據(jù)B點(diǎn)原始點(diǎn)來計(jì)算柱狀圖顯示的寬度和高度;originalX也就是B的x點(diǎn),往右移動了30,originalY也就是B的y為屏幕高度的2/3,這個(gè)可以根據(jù)自己的需要進(jìn)行設(shè)定,原點(diǎn)知道了,就可以計(jì)算試圖寬度width了,就是getMeasuredWidth() - originalX * 2就可以了,測量ok了,剩下就只有繪制了,先易后難,先繪制柱狀圖的標(biāo)題;
/**
* 繪制標(biāo)題
*
* @param canvas
*/
private void drawTitle(Canvas canvas) {
if (!TextUtils.isEmpty(graphTitle)) {
//繪制標(biāo)題
mPaint.setTextSize(graphTitleSize);
mPaint.setColor(graphTitleColor);
//設(shè)置文字粗體
mPaint.setFakeBoldText(true);
//獲取文字的寬度
float measureText = mPaint.measureText(graphTitle);
canvas.drawText(
graphTitle,
getWidth() / 2 - measureText / 2,
originalY + dip2px(titleMarginXaxis),
mPaint
);
}
}
標(biāo)題有才會進(jìn)行繪制,一開始也就是paint的設(shè)置,繪制文字調(diào)用drawText就可以進(jìn)行繪制了,不過要先確定文字的x,y的起始位置;
y的話就在originalY的基礎(chǔ)上往下移動一定距離就可以,看效果,標(biāo)題是屏幕居中顯示,那就用屏幕寬度/2-文字寬度/2就可以得到x的位置了;
x軸和y軸的繪制放一起進(jìn)行繪制,x軸變動的x的終點(diǎn),y軸變動的也只是y軸的終點(diǎn);
/**
* 繪制x軸
*
* @param canvas
*/
protected void drawXAxis(Canvas canvas) {
mPaint.setColor(axisLineColor);
mPaint.setStrokeWidth(axisLineWidth);
canvas.drawLine(originalX, originalY, originalX + width, originalY, mPaint);
}
/**
* 繪制y軸
*
* @param canvas
*/
protected void drawYAxis(Canvas canvas) {
mPaint.setColor(axisLineColor);
mPaint.setStrokeWidth(axisLineWidth);
canvas.drawLine(originalX, originalY, originalX, originalY - height, mPaint);
}
接下來是x刻度值,y軸刻度和刻度值的繪制;
/**
* 繪制x軸刻度值
*
* @param canvas
*/
protected void drawXAxisScaleValue(Canvas canvas) {
int xTxtMargin = dip2px(15);
mPaint.setColor(axisTextColor);
mPaint.setTextSize(axisTextSize);
mPaint.setFakeBoldText(true);
float cellWidth = width / (columnList.size() + 2);
for (int i = 0; i < columnList.size() + 1; i++) {
if (i == 0) {
continue;
}
String txt = i + "";
//測量文字的寬度
float txtWidth = mPaint.measureText(txt);
canvas.drawText(txt, cellWidth * i + originalX + (cellWidth / 2 - txtWidth / 2),
originalY + xTxtMargin,
mPaint);
}
}
首先要計(jì)算每一份顯示的寬度,第一和最后一個(gè)位置要多空置各一個(gè)寬度,就要在柱狀圖數(shù)據(jù)集合size上+2;就是width / (columnList.size() + 2),然后調(diào)用drawText進(jìn)行繪制;
/**
* 繪制y軸刻度
*
* @param canvas
*/
protected void drawYAxisScale(Canvas canvas) {
mPaint.setColor(axisLineColor);
float cellHeight = (height - dip2px(yMarign)) / axisDivideSizeY;
for (int i = 0; i < axisDivideSizeY; i++) {
canvas.drawLine(originalX,
originalY - cellHeight * (i + 1),
originalX + 10,
originalY - cellHeight * (i + 1),
mPaint);
}
}
y軸刻度的高度是根據(jù)調(diào)用是傳入的axisDivideSizeY來計(jì)算的,要看y上面顯示多少分,計(jì)算出每份的高度cellHeight后,調(diào)用drawLine進(jìn)行繪制;
/**
* 繪制y軸刻度值
*
* @param canvas
*/
protected void drawYAxisScaleValue(Canvas canvas) {
try {
mPaint.setColor(axisTextColor);
mPaint.setTextSize(axisTextSize);
int cellHeight = (height - dip2px(yMarign)) / axisDivideSizeY;
float cellValue = maxValue / (axisDivideSizeY + 0f);
//這里只處理的大于1時(shí)的繪制 小于等于1的繪制沒有處理
int ceil = (int) Math.ceil(cellValue);
// DecimalFormat df2 = new DecimalFormat("###.00");
// String format = df2.format(ceil);
// float result = Float.parseFloat(format);
for (int i = 0; i < axisDivideSizeY + 1; i++) {
if (i == 0) {
continue;
}
String s = ceil * i + "";
float v = mPaint.measureText(s);
canvas.drawText(s,
originalX - v - 10,
originalY - cellHeight * i + 10,
mPaint);
}
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
每份的高度和刻度一樣也是通過axisDivideSizeY來計(jì)算出cellHeight,每份顯示的value也就是刻度值,通過柱狀圖數(shù)據(jù)集合中的最大值/axisDivideSizeY y軸顯示的份數(shù),最大值的話是用過調(diào)用setColumnInfo方法設(shè)置參數(shù)時(shí)獲取的;
/**
* 調(diào)用該方法進(jìn)行圖表的設(shè)置
* @param columnList 柱狀圖的數(shù)據(jù)
* @param columnColors 顏色
* @param axisDivideSizeY y軸顯示的等份數(shù)
*/
public void setColumnInfo(List<Integer> columnList, List<Integer> columnColors, int axisDivideSizeY) {
this.columnList = columnList;
this.columnColors = columnColors;
this.axisDivideSizeY = axisDivideSizeY;
//獲取刻度的最大值
maxValue = Collections.max(columnList);
Log.e("TAG", "maxValue-->" + maxValue);
invalidate();
}
計(jì)算出每份的刻度值,遍歷循環(huán)就可以計(jì)算出對應(yīng)的刻度值,調(diào)用drawText就可以進(jìn)行繪制了;x、y軸,標(biāo)題,x、y軸的刻度和刻度值都繪制好了,就剩下x、y的箭頭,柱狀圖了;
/**
* 繪制x軸箭頭
*
* @param canvas
*/
private void drawXAxisArrow(Canvas canvas) {
mPaint.setColor(axisTextColor);
Path xPath = new Path();
xPath.moveTo(originalX + width + 30, originalY);
xPath.lineTo(originalX + width, originalY + 10);
xPath.lineTo(originalX + width, originalY - 10);
xPath.close();
canvas.drawPath(xPath, mPaint);
//繪制x軸名稱
if (!TextUtils.isEmpty(xAxisName)) {
canvas.drawText(xAxisName, originalX + width, originalY + 50, mPaint);
}
}
/**
* 繪制y軸箭頭
*
* @param canvas
*/
private void drawYAxisArrow(Canvas canvas) {
mPaint.setColor(axisTextColor);
Path yPath = new Path();
yPath.moveTo(originalX, originalY - height - 30);
yPath.lineTo(originalX - 10, originalY - height);
yPath.lineTo(originalX + 10, originalY - height);
yPath.close();
canvas.drawPath(yPath, mPaint);
//繪制y軸名稱
if (!TextUtils.isEmpty(yAxisName)) {
canvas.drawText(yAxisName, originalX - 50, originalY - height - 35, mPaint);
}
}
x、y軸的箭頭、文字繪制差不多,不過要繪制三角形箭頭,canvas并沒有提供繪制三角形的api,需要利用path路徑來繪制,最后看看柱狀圖的繪制;
/**
* 繪制柱狀圖
*
* @param canvas
*/
protected void drawColumn(Canvas canvas) {
if (columnList != null && columnColors != null) {
float cellWidth = width / (columnList.size() + 2);
//根據(jù)最大值和高度計(jì)算比例
float scale = (height - dip2px(yMarign)) / maxValue;
for (int i = 0; i < columnList.size(); i++) {
mPaint.setColor(columnColors.get(i));
float leftTopY = originalY - columnList.get(i) * scale;
canvas.drawRect(originalX + cellWidth * (i + 1),
leftTopY,
originalX + cellWidth * (i + 2),
originalY - axisLineWidth / 2,
mPaint);
}
}
}
x軸每份的寬度和x軸刻度值的計(jì)算一樣的,根據(jù)柱狀圖顯示的高度/maxValue,計(jì)算出每份的高度,調(diào)用drawRect繪制矩形,繪制時(shí)需要注意矩形矩形的起始x、y點(diǎn),終點(diǎn)x、y點(diǎn),x軸的話,其實(shí)上一個(gè)的終點(diǎn)就是下一個(gè)的x起始點(diǎn),因?yàn)榈谝粋€(gè)是空置的,所以x的起始點(diǎn)就是originalX + cellWidth * (i + 1) x原點(diǎn)+對應(yīng)index位置的每份寬度;y軸的話,終點(diǎn)是一致的,都是原點(diǎn)-x軸寬度/2(originalY - axisLineWidth / 2),起始點(diǎn)就是y軸原點(diǎn)-index對應(yīng)的value*scale;這樣就確定了每個(gè)矩形的起始x、y點(diǎn),終點(diǎn)x、y點(diǎn)繪制出來就ok了;使用的話通過setColumnInfo傳入對應(yīng)的參數(shù)就可以了。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.lsm.histogramview.HistogramView
android:id="@+id/histogram_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:graphTitle="柱狀圖效果"
app:xAxisName="天"
app:yAxisName="營業(yè)額"/>
</RelativeLayout>
public class MainActivity extends AppCompatActivity {
private HistogramView histogramView;
private List<Integer> values;
private List<Integer> colors;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
histogramView = findViewById(R.id.histogram_view);
values = new ArrayList<>();
colors = new ArrayList<>();
values.add(16);
values.add(25);
values.add(44);
values.add(11);
values.add(22);
values.add(17);
values.add(35);
colors.add(Color.BLUE);
colors.add(Color.BLACK);
colors.add(Color.GREEN);
colors.add(Color.GRAY);
colors.add(Color.RED);
colors.add(Color.YELLOW);
colors.add(Color.LTGRAY);
histogramView.setColumnInfo(values, colors, 7);
}
}