Android 进阶自定义View(3)图表统计BarChartView柱状图的实现

简介: 《一》导语:最近写了几个统计数据相关的图表,刚好放在自定义View这块的跟大家分享一下。对于图表这块,可能对于很多App的开发都用得不是很多,但是对于一些有数据分析统计需求相关的,例如P2P类型的,就比较常用了。

《一》导语:

最近写了几个统计数据相关的图表,刚好放在自定义View这块的跟大家分享一下。对于图表这块,可能对于很多App的开发都用得不是很多,但是对于一些有数据分析统计需求相关的,例如P2P类型的,就比较常用了。不过我们可能也就需要用到那么一两个图表,例如:曲线图、折线图、柱状图,饼状图等等,只是对于不同的业务需求,页面设计会有所差别,我们需要实现的效果可能也会有一些差别。接下来的几篇文章我会逐一介绍以下几个常用的图表统计图的实现过程,都是一个类完成一个图表的实现,很轻量,希望对各位读者有所帮助:

  • 柱状图

  • 曲线图 / 折线图

  • 饼状图

《二》小小的建议:

当遇到一些我们用系统的控件无法实现的需求的时候,我们就要去思考要不要自定义实现了,所以遇到这种需求,建议先不要急着去找网上写好的图表库,当然有一些写得很好的库,但是建议你百度后,只是仅限于参考学习,而不是去单纯的修修改改。

两点原因:

1、现成的UI库,往往功能太多,你不仅要筛选出合适的那个,往往还需要按照自己的设计图去修改UI,而修改别人的代码,是相当费时的一件事。
2、 修改别人写好的东西,一定程度上也能学习,但是肯定是没有自己动手学习得更多,而且自己写的,以后有变动,想咋改咋改。所以呢,还不如自己动手写一个,其实也没那么难。今天讲的是柱状统计图,先看一下我实现的效果图,为了好看点,还整了个背景图。

img_dd3f8098bf6329c196aad3050d0074e0.png
image.png

《三》根据下图,分析一下View绘制的思路:

1、确定坐标原点,作为绘制的参考点
2、绘制横向的X轴及上方的刻度线
3、绘制Y轴及Y轴的刻度文字值
4、绘制不同颜色的柱状条、X轴刻度值及柱状条上方的值
5、测量View需要的最大宽高。


img_281467e3357d51eb1b2ee2c0712f8c00.png
image.png

涉及到的几个绘制图形的方法:drawLine(),drawText(),drawRect(),灰常的简单。

(1)通过上图可以确定绘制起点(startX,starty)的坐标。

      //文字+刻度宽度+文字与刻度之间间距
        startX = mMaxTextWidth  + keduWidth+ keduTextSpace;
        //坐标原点 y轴起点。 isShowValueText :是否要展示柱状条上的值
        startY = keduSpace * (yAxisList.size() - 1) + mMaxTextHeight + (isShowValueText ? keduTextSpace : 0);

(2)确定好绘制起点的坐标,一切都很简单了,接下来就是按照步骤,一点点实现,总的代码很简单,注释也比较清楚,这里就不赘述了,文末有完整的代码。
(3)有个小细节在这里说一下下。一般而言呢,X轴坐标,是能确定的,比如月份、年份,直接根据接口数据传过来稍微处理一下就行。但是Y轴坐标,一般是随着时间的推移变动的,当你的Y轴坐标的范围不确定的时候,这个时候你就必须处理一下Y轴的坐标数据,使其能够动态跟着实际数据的变化而变化,以免超出坐标轴范围,下面是我项目中用到的一种处理的方法,大家可以参考一下。由于我的测试的Activity是Kotlin写的,我就直接用Kotlin代码了,看一下大概思路就行。

        
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        var datas = listOf<Int>(40, 76, 90, 50, 187)
        var xList = listOf<String>("1月份", "2月份", "3月份", "4月份", "5月份")

        //根据数据的最大值生成上下对应的Y轴坐标范围
        var ylist = mutableListOf<Int>()
        var maxYAxis: Int? = Collections.max(datas)
        if (maxYAxis!! % 2 == 0) {
            maxYAxis = maxYAxis + 2
        } else {
            maxYAxis = maxYAxis + 1
        }
        var keduSpace = (maxYAxis / datas.size) + 1
        for (i in 0..datas.size) {
            ylist.add(0 + keduSpace * i)
        }
        barchartview.updateValueData(datas, xList, ylist)

    }
}

(4)下面是BarChartView完整代码:

package com.example.jojo.learn.customview;

import android.content.Context;
import android.graphics.BlurMaskFilter;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

import com.example.jojo.learn.R;

import org.jetbrains.annotations.NotNull;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by JoJo on 2018/8/2.
 * wechat:18510829974
 * description:柱状统计图
 */

public class BarChartView extends View {
    private Context mContext;

    private Paint mPaintBar;
    private Paint mPaintLline;
    private Paint mPaintText;
    //柱状条对应的颜色数组
    private int[] colors;
    private int keduTextSpace = 10;//刻度与文字之间的间距
    private int keduWidth = 20; //坐标轴上横向标识线宽度
    private int keduSpace = 100; //每个刻度之间的间距 px
    private int itemSpace = 60;//柱状条之间的间距
    private int itemWidth = 100;//柱状条的宽度
    //刻度递增的值
    private int valueSpace = 40;
    //绘制柱形图的坐标起点
    private int startX;
    private int startY;
    private int mTextSize = 25;
    private int mMaxTextWidth;
    private int mMaxTextHeight;
    private Rect mXMaxTextRect;
    private Rect mYMaxTextRect;
    //是否要展示柱状条对应的值
    private boolean isShowValueText = true;
    //数据值
    private List<Integer> mData = new ArrayList<>();
    private List<Integer> yAxisList = new ArrayList<>();
    private List<String> xAxisList = new ArrayList<>();

    public BarChartView(Context context) {
        this(context, null);
    }

    public BarChartView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);

    }

    public BarChartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mContext = context;
        colors = new int[]{ContextCompat.getColor(context, R.color.color_07f2ab), ContextCompat.getColor(context, R.color.color_79d4d8), ContextCompat.getColor(context, R.color.color_4388bc), ContextCompat.getColor(context, R.color.color_07f2ab), ContextCompat.getColor(context, R.color.color_4388bc)};
        init(context, false);
    }

    private void init(Context context, boolean isUpdate) {
        if (!isUpdate) {
            initData();
        }
        //设置边缘特殊效果
        BlurMaskFilter PaintBGBlur = new BlurMaskFilter(
                1, BlurMaskFilter.Blur.INNER);
        //绘制柱状图的画笔
        mPaintBar = new Paint();
        mPaintBar.setStyle(Paint.Style.FILL);
        mPaintBar.setStrokeWidth(4);
        mPaintBar.setMaskFilter(PaintBGBlur);
        //绘制直线的画笔
        mPaintLline = new Paint();
        mPaintLline.setColor(ContextCompat.getColor(context, R.color.color_274782));
        mPaintLline.setAntiAlias(true);
        mPaintLline.setStrokeWidth(2);

        //绘制文字的画笔
        mPaintText = new Paint();
        mPaintText.setTextSize(mTextSize);
        mPaintText.setColor(ContextCompat.getColor(context, R.color.color_a9c6d6));
        mPaintText.setAntiAlias(true);
        mPaintText.setStrokeWidth(1);

        mYMaxTextRect = new Rect();
        mXMaxTextRect = new Rect();
        mPaintText.getTextBounds(Integer.toString(yAxisList.get(yAxisList.size() - 1)), 0, Integer.toString(yAxisList.get(yAxisList.size() - 1)).length(), mYMaxTextRect);
        mPaintText.getTextBounds(xAxisList.get(xAxisList.size() - 1), 0, xAxisList.get(xAxisList.size() - 1).length(), mXMaxTextRect);
        //绘制的刻度文字的最大值所占的宽高
        mMaxTextWidth = mYMaxTextRect.width() > mXMaxTextRect.width() ? mYMaxTextRect.width() : mXMaxTextRect.width();
        mMaxTextHeight = mYMaxTextRect.height() > mXMaxTextRect.height() ? mYMaxTextRect.height() : mXMaxTextRect.height();

        if (yAxisList.size() >= 2) {
            valueSpace = yAxisList.get(1) - yAxisList.get(0);
        }
        //文字+刻度宽度+文字与刻度之间间距
        startX = mMaxTextWidth + keduWidth + keduTextSpace;
        //坐标原点 y轴起点
        startY = keduSpace * (yAxisList.size() - 1) + mMaxTextHeight + (isShowValueText ? keduTextSpace : 0);

    }

    /**
     * 初始化数据
     */
    private void initData() {
        int[] data = {80, 160, 30, 40, 100};
        for (int i = 0; i < 5; i++) {
            mData.add(data[i]);
            yAxisList.add(0 + i * valueSpace);
        }
        String[] xAxis = {"1月", "2月", "3月", "4月", "5月"};
        for (int i = 0; i < mData.size(); i++) {
            xAxisList.add(xAxis[i]);
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.e("TAG", "onMeasure()");
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        if (heightMode == MeasureSpec.AT_MOST) {
            if (keduWidth > mMaxTextHeight + keduTextSpace) {
                heightSize = (yAxisList.size() - 1) * keduSpace + keduWidth + mMaxTextHeight;
            } else {
                heightSize = (yAxisList.size() - 1) * keduSpace + (mMaxTextHeight + keduTextSpace) + mMaxTextHeight;
            }
            heightSize = heightSize + keduTextSpace + (isShowValueText ? keduTextSpace : 0);//x轴刻度对应的文字距离底部的padding:keduTextSpace
        }
        if (widthMode == MeasureSpec.AT_MOST) {
            widthSize = startX + mData.size() * itemWidth + (mData.size() + 1) * itemSpace;
        }
        Log.e("TAG", "heightSize=" + heightSize + "widthSize=" + widthSize);
        //保存测量结果
        setMeasuredDimension(widthSize, heightSize);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.e("TAG", "onDraw()");

        //从下往上绘制Y 轴
        canvas.drawLine(startX, startY + keduWidth, startX, startY - (yAxisList.size() - 1) * keduSpace, mPaintLline);

        for (int i = 0; i < yAxisList.size(); i++) {

            //绘制Y轴的文字
            Rect textRect = new Rect();
            mPaintText.getTextBounds(Integer.toString(yAxisList.get(i)), 0, Integer.toString(yAxisList.get(i)).length(), textRect);
            canvas.drawText(Integer.toString(yAxisList.get(i)), (startX - keduWidth) - textRect.width() - keduTextSpace, startY - (i + 1) * keduSpace + keduSpace, mPaintText);

            //画X轴及上方横向的刻度线
            canvas.drawLine(startX - keduWidth, startY - keduSpace * i, startX + mData.size() * itemWidth + itemSpace * (mData.size() + 1), startY - keduSpace * i, mPaintLline);

        }

        for (int j = 0; j < xAxisList.size(); j++) {
            //绘制X轴的文字
            Rect rect = new Rect();
            mPaintText.getTextBounds(xAxisList.get(j), 0, xAxisList.get(j).length(), rect);
            canvas.drawText(xAxisList.get(j), startX + itemSpace * (j + 1) + itemWidth * j + itemWidth / 2 - rect.width() / 2, startY + rect.height() + keduTextSpace, mPaintText);

            if (isShowValueText) {
                Rect rectText = new Rect();
                mPaintText.getTextBounds(mData.get(j) + "", 0, (mData.get(j) + "").length(), rectText);
                //绘制柱状条上的值
                canvas.drawText(mData.get(j) + "", startX + itemSpace * (j + 1) + itemWidth * j + itemWidth / 2 - rectText.width() / 2, (float) (startY - keduTextSpace - (mData.get(j) * (keduSpace * 1.0 / valueSpace))), mPaintText);
            }
            //绘制柱状条
            mPaintBar.setColor(colors[j]);
            //(mData.get(j) * (keduSpace * 1.0 / valueSpace)):为每个柱状条所占的高度值px
            int initx = startX + itemSpace * (j + 1) + j * itemWidth;
            canvas.drawRect(initx, (float) (startY - (mData.get(j) * (keduSpace * 1.0 / valueSpace))), initx + itemWidth, startY, mPaintBar);
        }
    }

    /**
     * 根据真实的数据刷新界面
     *
     * @param datas
     * @param xList
     * @param yList
     */
    public void updateValueData(@NotNull List<Integer> datas, @NotNull List<String> xList, @NotNull List<Integer> yList) {
        this.mData = datas;
        this.xAxisList = xList;
        this.yAxisList = yList;
        init(mContext, true);
        invalidate();
    }
}

大家如果需要学习一些负责的图表统计图,可以参考以下几个强大的图表库:

MpChart
hellocharts-android
AndroidCharts

相关文章
|
4天前
|
Android开发
Android面试题之自定义View注意事项
在Android开发中,自定义View主要分为四类:直接继承View重写onDraw,继承ViewGroup创建布局,扩展特定View如TextView,以及继承特定ViewGroup。实现时需注意:支持wrap_content通过onMeasure处理,支持padding需在onDraw或onMeasure/onLayout中处理。避免在View中使用Handler,使用post系列方法代替。记得在onDetachedFromWindow时停止线程和动画以防止内存泄漏。处理滑动嵌套时解决滑动冲突,并避免在onDraw中大量创建临时对象。
14 4
|
1天前
|
Android开发
Android面试题之View的invalidate方法和postInvalidate方法有什么区别
本文探讨了Android自定义View中`invalidate()`和`postInvalidate()`的区别。`invalidate()`在UI线程中刷新View,而`postInvalidate()`用于非UI线程,通过消息机制切换到UI线程执行`invalidate()`。源码分析显示,`postInvalidate()`最终调用`ViewRootImpl`的`dispatchInvalidateDelayed`,通过Handler发送消息到UI线程执行刷新。
11 1
|
7天前
|
前端开发 API Android开发
Android自定义View之Canvas一文搞定
这篇文章介绍了Android自定义View中如何使用Canvas和Paint来绘制图形。Canvas可理解为画布,用于绘制各种形状如文字、点、线、矩形、圆角矩形、圆和弧。常见API包括`drawText()`、`drawPoint()`、`drawLine()`、`drawRect()`等。文章还提到了Canvas的保存、恢复、平移和旋转方法,通过绘制钟表盘的例子展示了如何实际应用。总结关键点:Canvas与Paint结合用于图像绘制,掌握Canvas的基本绘图函数及坐标变换操作是自定义View的关键。
7 0
Android自定义View之Canvas一文搞定
|
7天前
|
消息中间件 前端开发 Android开发
Android面试题自定义View之Window、ViewRootImpl和View的三大流程
Android开发中,View的三大核心流程包括measure(测量)、layout(布局)和draw(绘制)。MeasureSpec类在测量过程中起到关键作用,它结合尺寸大小和模式(EXACTLY、AT_MOST、UNSPECIFIED)来指定View应如何测量。onMeasure方法用于自定义View的测量,布局阶段,ViewGroup调用onLayout确定子元素位置,而draw阶段按照特定顺序绘制背景、内容、子元素和装饰。整个流程始于ViewRootImpl的performTraversals,该方法触发测量、布局和绘制。
13 0
|
缓存 Java Android开发
|
5天前
|
开发工具 Android开发 iOS开发
探索Android与iOS开发的差异与挑战
【7月更文挑战第11天】在移动应用开发的广阔天地中,Android和iOS两大平台如同双子星座般耀眼,各自拥有独特的开发生态和用户群体。本文将深入分析这两个平台的显著差异,从技术架构到开发工具,再到市场定位,揭示它们之间的异同。通过比较,我们不仅能够更好地理解各自的优势和局限,还能洞察未来移动应用开发的趋势。
|
8天前
|
Java Android开发 iOS开发
探索Android与iOS开发的差异:平台选择对项目成功的影响
【7月更文挑战第8天】在移动应用开发的广阔天地中,Android与iOS两大平台各自占据着半壁江山。本文将深入探讨这两个平台在开发环境、用户界面设计、性能优化以及市场覆盖等方面的根本差异,并分析这些差异如何影响项目的成功。通过比较和分析,旨在为开发者在选择平台时提供更全面的视角,帮助他们根据项目需求和目标市场做出更明智的决策。
|
1天前
|
Android开发 Kotlin
kotlin开发安卓app,如何让布局自适应系统传统导航和全面屏导航
使用`navigationBarsPadding()`修饰符实现界面自适应,自动处理底部导航栏的内边距,再加上`.padding(bottom = 10.dp)`设定内容与屏幕底部的距离,以完成全面的布局适配。示例代码采用Kotlin。
32 15
|
1天前
|
存储 API Android开发
kotlin开发安卓app,使用webivew 触发 onShowFileChooser, 但只能触发一次,第二次无法触发,是怎么回事。 如何解决
在Android WebView开发中,`onShowFileChooser`方法用于开启文件选择。当用户只能选择一次文件可能是因为未正确处理选择回调。解决此问题需确保:1) 实现`WebChromeClient`并覆写`onShowFileChooser`;2) 用户选择文件后调用`ValueCallback.onReceiveValue`传递URI;3) 传递结果后将`ValueCallback`设为`null`以允许再次选择。下面是一个Kotlin示例,展示如何处理文件选择和结果回调。别忘了在Android 6.0+动态请求存储权限,以及在Android 10+处理分区存储。
|
3天前
|
存储 移动开发 Android开发
使用kotlin Jetpack Compose框架开发安卓app, webview中h5如何访问手机存储上传文件
在Kotlin和Jetpack Compose中,集成WebView以支持HTML5页面访问手机存储及上传音频文件涉及关键步骤:1) 添加`READ_EXTERNAL_STORAGE`和`WRITE_EXTERNAL_STORAGE`权限,考虑Android 11的分区存储;2) 配置WebView允许JavaScript和文件访问,启用`javaScriptEnabled`、`allowFileAccess`等设置;3) HTML5页面使用`<input type="file">`让用户选择文件,利用File API;