自定义控件之柱状图

本文详细介绍了一种自定义柱状图的绘制方法,包括数据准备、初始化、绘制静态及带增长动画的柱状图,以及处理触摸事件。通过具体代码实现,展示了如何在Android应用中灵活使用自定义视图绘制数据图表。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在某些应用场景中,比如运动类或者金融类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;
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值