在某些应用场景中,比如运动类或者金融类App需要绘制图表展示数据,为了理解图表绘制原理,我们自己来实现以下,先看效果:
开始分析:
1.准备数据:
private static final String[] HORIZONTAL_AXIS = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"}; private static final float[] DATA = {12, 24, 45, 56, 89, 70, 49, 22, 23, 10, 12, 3};
//柱状图的数据列表 private float[] mDataList; //最大值 private float mMax; //水平方向X轴坐标 private String[] mHorizontalAxis;
/** * 设置X轴坐标值 * @param horizontalAxis */ public void setHorizontalAxis(String[] horizontalAxis) { mHorizontalAxis = horizontalAxis; }
/** * 设置柱状图数据 * * @param dataList * @param max */ public void setDataList(float[] dataList, int max) { mDataList = dataList; mMax = max; }
2.绘制数据初始化
public class Bar { private int left; private int top; private int right; private int bottom; //柱状图原始数据的大小 private float value; //柱状图原始数据装换成对应的像素大小 private float transformedValue; //柱状图动画中用到,表示柱状图动画过程中顶部位置的变量,[0,top] private int currentTop; public int getCurrentTop() { return currentTop; } public void setCurrentTop(int currentTop) { this.currentTop = currentTop; } public int getLeft() { return left; } public void setLeft(int left) { this.left = left; } public int getTop() { return top; } public void setTop(int top) { this.top = top; } public int getRight() { return right; } public void setRight(int right) { this.right = right; } public int getBottom() { return bottom; } public void setBottom(int bottom) { this.bottom = bottom; } public float getValue() { return value; } public void setValue(float value) { this.value = value; } public float getTransformedValue() { return transformedValue; } public void setTransformedValue(float transformedValue) { this.transformedValue = transformedValue; } public boolean isInside(float x,float y){ return x>left&&x<right&&y>top&&y<bottom; } }
我们在onSizeChange完成Bar对象的初始化
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { //清空柱状图Bar的集合 mBars.clear(); //去除padding计算宽和高 int width = w - getPaddingLeft() - getPaddingRight(); int height = h - getPaddingBottom() - getPaddingTop(); //按照集合的大小平分宽度 int step = width / mDataList.length; //mBarWidth为柱状宽度变量,mRadius为柱状宽度的一半 mRadius = (int) (mBarWidth / 2); //计算第一条柱状左边位置 int barLeft = getPaddingLeft() + step / 2 - mRadius; //通过坐标文本画笔计算绘制x轴第一个坐标文本占据的矩形边界,这里主要获取其高度,为了计算maxBarHeight提供数据。 mAxisPaint.getTextBounds(mHorizontalAxis[0], 0, mHorizontalAxis[0].length(), mTextRect); //计算柱状图高度的最大像素大小,mTextRect.height为底部x轴坐标文本的高度,mGap为坐标文本与柱状条之间间隔大小的变量 int maxBarHeight = height - mTextRect.height() - mGap; //计算柱状高度的大小与最大数值的比值 float heightRatio = maxBarHeight / mMax; //遍历数据集合,初始化所有柱状条的对象 for (float data : mDataList) { Bar bar = new Bar(); //设置数据 bar.setValue(data); //计算原始数据对应的像素高度大小 bar.setTransformedValue(bar.getValue() * heightRatio); //绘制柱状图的四个位置 bar.setLeft(barLeft); bar.setTop((int) (getPaddingTop() + maxBarHeight - bar.getTransformedValue())); bar.setRight((int) (bar.getLeft() + mBarWidth)); bar.setBottom(getPaddingBottom() + maxBarHeight); //初始化绘制柱状条时当前top值,用作动画 bar.setCurrentTop(bar.getBottom()); //初始化好添加到集合中 mBars.add(bar); //更新左边柱状条的位置,为下一次做准备 barLeft += step; } }
3.绘制柱状图不带动画效果
/** * 绘制柱状条没有动画效果 * * @param canvas */ private void drawBar(Canvas canvas) { //遍历所有Bar对象,一个个绘制 for (int i = 0; i < mBars.size(); i++) { Bar bar = mBars.get(i); //绘制x轴坐标文本 String axis = mHorizontalAxis[i]; //计算绘制文本的起始位置坐标(textX,textY),textX为柱状条中心位置,由于我们对画笔mAxisPaint //设置了Paint.AlignCENTER,所以绘制出来的文本中线和柱状图的中线是重合的 float textX = bar.getLeft() + mRadius; float textY = getHeight() - getPaddingBottom(); //绘制坐标文本 canvas.drawText(axis, textX, textY, mAxisPaint); //选中当前的柱状条文字变色 if (i == mSelectedIndex) { mBarPaint.setColor(Color.RED); float x = bar.getLeft() + mRadius; float y = bar.getTop() - mGap; canvas.drawText(String.valueOf(bar.getValue()), x, y, mAxisPaint); } else { mBarPaint.setColor(Color.BLUE); } //设置柱状条矩形 mTemp.set(bar.getLeft(), bar.getTop(), bar.getRight(), bar.getBottom()); //绘制圆角矩形 canvas.drawRoundRect(mTemp, mRadius, mRadius, mBarPaint); } }
4.绘制柱状图,带增长动画
/** * 带增长动画 * * @param canvas */ private void drawBarWithAnimation(Canvas canvas) { //遍历所有的Bar for (int i = 0; i < mDataList.length; i++) { Bar bar = mBars.get(i); //绘制坐标文本 String axis = mHorizontalAxis[i]; float textX = bar.getLeft() + mRadius; float textY = getHeight() - getPaddingBottom(); canvas.drawText(axis, textX, textY, mAxisPaint); //更新当前柱状条底部位置变量,BAR_GROWTH_STEP为柱状条增长的步长,既让柱状条增长的变化量 bar.setCurrentTop(bar.getCurrentTop() - BAR_GROWTH_STEP); //当增长的值超过顶部说明越界了 if (bar.getCurrentTop() <= bar.getTop()) { //将currentTop重新设为本来top bar.setCurrentTop(bar.getTop()); //高度最高的柱状条顶部位置为paddingTop,如果cuerrentTop==paddingTop,说明高度的进度条也到达 //最高点,需要停止动画了 if (bar.getCurrentTop() == getPaddingTop()) { enableGrowthAnination = false; } } //绘制圆角矩形 mTemp.set(bar.getLeft(), bar.getCurrentTop(), bar.getRight(), bar.getBottom()); canvas.drawRoundRect(mTemp, mRadius, mRadius, mBarPaint); } //延迟重新出发 if (enableGrowthAnination) { postInvalidateDelayed(DELAY); } }
5.处理触摸事件
@Override public boolean onTouchEvent(MotionEvent event) { if (enableGrowthAnination) { return false; } switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: for (int i = 0; i < mBars.size(); i++) { if (mBars.get(i).isInside(event.getX(), event.getY())) { enableGrowthAnination = false; mSelectedIndex = i; invalidate(); break; } } break; case MotionEvent.ACTION_UP: mSelectedIndex = -1; enableGrowthAnination = false; invalidate(); break; } return true; }
代码如下:
package com.cmj.barchart; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; import android.support.annotation.Nullable; import android.util.AttributeSet; import android.util.TypedValue; import android.view.MotionEvent; import android.view.View; import java.util.ArrayList; import java.util.List; public class BarChart extends View { private Paint mBarPaint; private Paint mAxisPaint; private Rect mTextRect; private RectF mTemp; private float mBarWidth; private int mGap; //柱状图的数据列表 private float[] mDataList; private float mMax; //水平方向X轴坐标 private String[] mHorizontalAxis; private int mRadius; private int mSelectedIndex = -1; //柱状图增长动画 private boolean enableGrowthAnination = true; public static final int DELAY = 10; public static final int BAR_GROWTH_STEP = 15; private List<Bar> mBars = new ArrayList<>(); public BarChart(Context context) { this(context, null); } public BarChart(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public BarChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initView(); } private void initView() { mAxisPaint = new Paint(); mAxisPaint.setAntiAlias(true); mAxisPaint.setTextSize(26); mAxisPaint.setTextAlign(Paint.Align.CENTER); mBarPaint = new Paint(); mBarPaint.setColor(Color.BLUE); mBarPaint.setAntiAlias(true); mTextRect = new Rect(); mTemp = new RectF(); //柱状图宽度,10dp mBarWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, getResources().getDisplayMetrics()); mGap = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, getResources().getDisplayMetrics()); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { //清空柱状图Bar的集合 mBars.clear(); //去除padding计算宽和高 int width = w - getPaddingLeft() - getPaddingRight(); int height = h - getPaddingBottom() - getPaddingTop(); //按照集合的大小平分宽度 int step = width / mDataList.length; //mBarWidth为柱状宽度变量,mRadius为柱状宽度的一半 mRadius = (int) (mBarWidth / 2); //计算第一条柱状左边位置 int barLeft = getPaddingLeft() + step / 2 - mRadius; //通过坐标文本画笔计算绘制x轴第一个坐标文本占据的矩形边界,这里主要获取其高度,为了计算maxBarHeight提供数据。 mAxisPaint.getTextBounds(mHorizontalAxis[0], 0, mHorizontalAxis[0].length(), mTextRect); //计算柱状图高度的最大像素大小,mTextRect.height为底部x轴坐标文本的高度,mGap为坐标文本与柱状条之间间隔大小的变量 int maxBarHeight = height - mTextRect.height() - mGap; //计算柱状高度的大小与最大数值的比值 float heightRatio = maxBarHeight / mMax; //遍历数据集合,初始化所有柱状条的对象 for (float data : mDataList) { Bar bar = new Bar(); //设置数据 bar.setValue(data); //计算原始数据对应的像素高度大小 bar.setTransformedValue(bar.getValue() * heightRatio); //绘制柱状图的四个位置 bar.setLeft(barLeft); bar.setTop((int) (getPaddingTop() + maxBarHeight - bar.getTransformedValue())); bar.setRight((int) (bar.getLeft() + mBarWidth)); bar.setBottom(getPaddingBottom() + maxBarHeight); //初始化绘制柱状条时当前top值,用作动画 bar.setCurrentTop(bar.getBottom()); //初始化好添加到集合中 mBars.add(bar); //更新左边柱状条的位置,为下一次做准备 barLeft += step; } } @Override protected void onDraw(Canvas canvas) { if (enableGrowthAnination) { drawBarWithAnimation(canvas); } else { drawBar(canvas); } } /** * 绘制柱状条没有动画效果 * * @param canvas */ private void drawBar(Canvas canvas) { //遍历所有Bar对象,一个个绘制 for (int i = 0; i < mBars.size(); i++) { Bar bar = mBars.get(i); //绘制x轴坐标文本 String axis = mHorizontalAxis[i]; //计算绘制文本的起始位置坐标(textX,textY),textX为柱状条中心位置,由于我们对画笔mAxisPaint //设置了Paint.AlignCENTER,所以绘制出来的文本中线和柱状图的中线是重合的 float textX = bar.getLeft() + mRadius; float textY = getHeight() - getPaddingBottom(); //绘制坐标文本 canvas.drawText(axis, textX, textY, mAxisPaint); //选中当前的柱状条文字变色 if (i == mSelectedIndex) { mBarPaint.setColor(Color.RED); float x = bar.getLeft() + mRadius; float y = bar.getTop() - mGap; canvas.drawText(String.valueOf(bar.getValue()), x, y, mAxisPaint); } else { mBarPaint.setColor(Color.BLUE); } //设置柱状条矩形 mTemp.set(bar.getLeft(), bar.getTop(), bar.getRight(), bar.getBottom()); //绘制圆角矩形 canvas.drawRoundRect(mTemp, mRadius, mRadius, mBarPaint); } } /** * 带增长动画 * * @param canvas */ private void drawBarWithAnimation(Canvas canvas) { //遍历所有的Bar for (int i = 0; i < mDataList.length; i++) { Bar bar = mBars.get(i); //绘制坐标文本 String axis = mHorizontalAxis[i]; float textX = bar.getLeft() + mRadius; float textY = getHeight() - getPaddingBottom(); canvas.drawText(axis, textX, textY, mAxisPaint); //更新当前柱状条底部位置变量,BAR_GROWTH_STEP为柱状条增长的步长,既让柱状条增长的变化量 bar.setCurrentTop(bar.getCurrentTop() - BAR_GROWTH_STEP); //当增长的值超过顶部说明越界了 if (bar.getCurrentTop() <= bar.getTop()) { //将currentTop重新设为本来top bar.setCurrentTop(bar.getTop()); //高度最高的柱状条顶部位置为paddingTop,如果cuerrentTop==paddingTop,说明高度的进度条也到达 //最高点,需要停止动画了 if (bar.getCurrentTop() == getPaddingTop()) { enableGrowthAnination = false; } } //绘制圆角矩形 mTemp.set(bar.getLeft(), bar.getCurrentTop(), bar.getRight(), bar.getBottom()); canvas.drawRoundRect(mTemp, mRadius, mRadius, mBarPaint); } //延迟重新出发 if (enableGrowthAnination) { postInvalidateDelayed(DELAY); } } @Override public boolean onTouchEvent(MotionEvent event) { if (enableGrowthAnination) { return false; } switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: for (int i = 0; i < mBars.size(); i++) { if (mBars.get(i).isInside(event.getX(), event.getY())) { enableGrowthAnination = false; mSelectedIndex = i; invalidate(); break; } } break; case MotionEvent.ACTION_UP: mSelectedIndex = -1; enableGrowthAnination = false; invalidate(); break; } return true; } /** * 设置X轴坐标值 * * @param horizontalAxis */ public void setHorizontalAxis(String[] horizontalAxis) { mHorizontalAxis = horizontalAxis; } /** * 设置柱状图数据 * * @param dataList * @param max */ public void setDataList(float[] dataList, int max) { mDataList = dataList; mMax = max; } }