Android自定义View--自己撸一个柱状图也没那么难

简介: 版权声明:本文为博主原创文章,转载请标明出处。 https://blog.csdn.net/lyhhj/article/details/53771477 本文已授权微信公众号:鸿洋(hongyangAndroid)在微信公众号平台原创首发。
版权声明:本文为博主原创文章,转载请标明出处。 https://blog.csdn.net/lyhhj/article/details/53771477
本文已授权微信公众号:鸿洋(hongyangAndroid)在微信公众号平台原创首发。

绪论

*转眼间,2016伴随着互联网寒冬和帝都的雾霾马上就过去了,不知道大家今年一整年过得怎么样?最近票圈被各个城市的雾霾刷屏,
内心难免会动荡,庆幸自己早出来一年,也担忧着自己的未来的职业规划。无所谓了,既然选择了这个行业,我觉得大家就应该坚持下去,路是自己走的,及时再寒冬,只要你足够优秀,足够努力,相信你最后还是会找到自己满意的工作的。最后还要感谢今年博客之星大家对我的投票支持,非常感谢。不多说了,今天的主题是它–对,自定义View柱状图。
先来说说我最近在做什么吧?好久没有写博客了,最近手里有两个项目,闲的时候一直在忙着做项目,也封装了属于自己的一套Library,抽下来我会把它分享出来的。公司的项目也一直在忙,今天的柱状图就是公司的项目所用到的。先来看一下效果吧


*

这里写图片描述

背景

需求刚下来的时候我去网上找了一些开源的项目比如:
MpChart
hellocharts-android
AndroidCharts
等等有好多,但是最后为什么我选择了自定义,一来是这些开源库都很“重”,好多都用不到;二来很少有双条的柱状图;三来也想提高一下自己,仔细研究一下发现也并没有那么难。感兴趣的话可以去研究一下开源的那些图表,不过我曾经想过在上面的那几个基础上去修改,发现很难,所幸自己定义吧。


具体实现

可以看到,今天的柱状图分为三类:双条竖向柱状图、单条竖向柱状图以及单条横向柱状图,其实原理都是一样的,下面我们具体看一下怎么实现,怎么去画一个这样的柱状图。

双条竖向

我们可以看到这个柱状图主要包括下面几个方面:

  1. 双条柱状图
  2. 横坐标月份
  3. 点击tips显示具体数值
  4. 灰色阴影(图上没有显示具体看代码)
  5. 柱状图渐变、圆角、点击变色

好了上面五点就是需求和UI所提出来的所有东西,我们开始着手去“画”吧。


1.首先我们定义一些资源style供使用

包括
leftColor 左侧柱状图顶部颜色
leftColorBottom 左侧柱状图底部颜色
rightColor 右侧柱状图顶部颜色
rightColorBottom 右侧柱状图底部颜色
selectRightColor 左侧点击选中颜色
selectRightColor 右侧点击选中颜色
xyColor 横轴字体颜色

底部和顶部颜色是用于渐变用的

<declare-styleable name="MyChartView">
        <attr name="leftColor" format="color"></attr>
        <attr name="leftColorBottom" format="color"></attr>
        <attr name="selectLeftColor" format="color"></attr>
        <attr name="rightColor" format="color"></attr>
        <attr name="rightColorBottom" format="color"></attr>
        <attr name="selectRightColor" format="color"></attr>
        <attr name="xyColor" format="color"></attr>
    </declare-styleable>

2.接下来我们看具体代码,注释写的很详细了,仔细看:

  • 初始化属性、画笔、所用的size等
  • 测量计算高宽度等
  • 画坐标轴、画月份、画柱状图、画阴影
  • 柱状图渐变以及点击变色
  • touch点击事件判断点击所属哪个月份,接口回调给activity显示具体月份数值

    注意:onWindowVisibilityChanged这个方法(当屏幕焦点变化时重新侧向起始位置,必须重写次方法,否则当焦点变化时柱状图会跑到屏幕外面)
    

下面主要说一下绘制部分吧

OnDraw()部分

我们将每次onTouch的条的索引放到selectIndexRoles数组中,然后当这个数组包含该绘制的柱状图的索引是我们设置不用颜色以及不设置渐变;
同时我们给每两个双条之间的的空白处绘制成阴影;
最后drawRoundRect()就绘制了一个圆角的矩形。

//画柱状图
        for (int i = 0; i < list.size(); i++) {
            int size = mHeight / 120;
            if (selectIndexRoles.contains(i)) {
                //偶数
                mChartPaint.setShader(null);
                if (i % 2 == 0) {
                    mChartPaint.setColor(selectLeftColor);
                } else {
                    mChartPaint.setColor(selectRightColor);
                }
            } else {
                //偶数
                if (i % 2 == 0) {
                    LinearGradient lg = new LinearGradient(mChartWidth, mChartWidth + mSize, mHeight - 100,
                        (float) (mHeight - 100 - list.get(i) * size), lefrColorBottom, leftColor, Shader.TileMode.MIRROR);
                    mChartPaint.setShader(lg);
                } else {
                    LinearGradient lg = new LinearGradient(mChartWidth, mChartWidth + mSize, mHeight - 100,
                        (float) (mHeight - 100 - list.get(i) * size), rightColorBottom, rightColor, Shader.TileMode.MIRROR);
                    mChartPaint.setShader(lg);
                }
            }

            mChartPaint.setStyle(Paint.Style.FILL);
            //画阴影
            if (i == number * 2 || i == number * 2 + 1) {
                mShadowPaint.setColor(Color.BLUE);
            } else {
                mShadowPaint.setColor(Color.WHITE);
            }

            //画柱状图
            RectF rectF = new RectF();
            rectF.left = mChartWidth;
            rectF.right = mChartWidth + mSize;
            rectF.bottom = mHeight - 100;
            rectF.top = (float) (mHeight - 100 - list.get(i) * size);
            canvas.drawRoundRect(rectF, 10, 10, mChartPaint);
            //canvas.drawRect(mChartWidth, mHeight - 100 - list.get(i) * size, mChartWidth + mSize, mHeight - 100, mChartPaint)
            // ;// 长方形
            mChartWidth += (i % 2 == 0) ? (3 + getWidth() / 39) : (getWidth() / 13 - 3 - mSize);
        }
全部代码
package com.hankkin.mycartdemo.chatview;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import com.hankkin.mycartdemo.R;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by Hankkin on 2016/12/10.
 */

public class MyChartView extends View {

    private int leftColor;//双柱左侧
    private int rightColor;//双柱右侧
    private int lineColor;//横轴线
    private int selectLeftColor;//点击选中左侧
    private int selectRightColor;//点击选中右侧
    private int lefrColorBottom;//左侧底部
    private int rightColorBottom;//右侧底部
    private Paint mPaint, mChartPaint, mShadowPaint;//横轴画笔、柱状图画笔、阴影画笔
    private int mWidth, mHeight, mStartWidth, mChartWidth, mSize;//屏幕宽度高度、柱状图起始位置、柱状图宽度
    private Rect mBound;
    private List<Float> list = new ArrayList<>();//柱状图高度占比
    private Rect rect;//柱状图矩形
    private getNumberListener listener;//点击接口
    private int number = 1000;//柱状图最大值
    private int selectIndex = -1;//点击选中柱状图索引
    private List<Integer> selectIndexRoles = new ArrayList<>();

    public void setList(List<Float> list) {
        this.list = list;
        mSize = getWidth() / 39;
        mStartWidth = getWidth() / 13;
        mChartWidth = getWidth() / 13 - mSize - 3;
        invalidate();
    }

    public void setListener(getNumberListener listener) {
        this.listener = listener;
    }

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

    public MyChartView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyChartView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //获取我们自定义的样式属性
        TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyChartView, defStyleAttr, 0);
        int n = array.getIndexCount();
        for (int i = 0; i < n; i++) {
            int attr = array.getIndex(i);
            switch (attr) {
                case R.styleable.MyChartView_leftColor:
                    // 默认颜色设置为黑色
                    leftColor = array.getColor(attr, Color.BLACK);
                    break;
                case R.styleable.MyChartView_selectLeftColor:
                    // 默认颜色设置为黑色
                    selectLeftColor = array.getColor(attr, Color.BLACK);
                    break;
                case R.styleable.MyChartView_rightColor:
                    rightColor = array.getColor(attr, Color.BLACK);
                    break;
                case R.styleable.MyChartView_selectRightColor:
                    selectRightColor = array.getColor(attr, Color.BLACK);
                    break;
                case R.styleable.MyChartView_xyColor:
                    lineColor = array.getColor(attr, Color.BLACK);
                    break;
                case R.styleable.MyChartView_leftColorBottom:
                    lefrColorBottom = array.getColor(attr, Color.BLACK);
                    break;
                case R.styleable.MyChartView_rightColorBottom:
                    rightColorBottom = array.getColor(attr, Color.BLACK);
                    break;
            }
        }
        array.recycle();
        init();
    }

    //初始化画笔
    private void init() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mBound = new Rect();
        mChartPaint = new Paint();
        mChartPaint.setAntiAlias(true);
        mShadowPaint = new Paint();
        mShadowPaint.setAntiAlias(true);
        mShadowPaint.setColor(Color.WHITE);
    }

    //测量高宽度
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width;
        int height;
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        if (widthMode == MeasureSpec.EXACTLY) {
            width = widthSize;
        } else {
            width = widthSize * 1 / 2;
        }
        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else {
            height = heightSize * 1 / 2;
        }

        setMeasuredDimension(width, height);
    }

    //计算高度宽度
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        mWidth = getWidth();
        mHeight = getHeight();
        mStartWidth = getWidth() / 13;
        mSize = getWidth() / 39;
        mChartWidth = getWidth() / 13 - mSize;
    }

    //重写onDraw绘制
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        mPaint.setColor(lineColor);
        //画坐标轴
        //canvas.drawLine(0, mHeight - 100, mWidth, mHeight - 100, mPaint);
        for (int i = 0; i < 12; i++) {
            //画刻度线
            //canvas.drawLine(mStartWidth, mHeight - 100, mStartWidth, mHeight - 80, mPaint);
            //画数字
            mPaint.setTextSize(35);
            mPaint.setTextAlign(Paint.Align.CENTER);
            mPaint.getTextBounds(String.valueOf(i + 1) + "", 0, String.valueOf(i).length(), mBound);
            canvas.drawText(String.valueOf(i + 1) + "月", mStartWidth - mBound.width() * 1 / 2,
                mHeight - 60 + mBound.height() * 1 / 2, mPaint);
            mStartWidth += getWidth() / 13;
        }
        //画柱状图
        for (int i = 0; i < list.size(); i++) {
            int size = mHeight / 120;
            if (selectIndexRoles.contains(i)) {
                //偶数
                mChartPaint.setShader(null);
                if (i % 2 == 0) {
                    mChartPaint.setColor(selectLeftColor);
                } else {
                    mChartPaint.setColor(selectRightColor);
                }
            } else {
                //偶数
                if (i % 2 == 0) {
                    LinearGradient lg = new LinearGradient(mChartWidth, mChartWidth + mSize, mHeight - 100,
                        (float) (mHeight - 100 - list.get(i) * size), lefrColorBottom, leftColor, Shader.TileMode.MIRROR);
                    mChartPaint.setShader(lg);
                } else {
                    LinearGradient lg = new LinearGradient(mChartWidth, mChartWidth + mSize, mHeight - 100,
                        (float) (mHeight - 100 - list.get(i) * size), rightColorBottom, rightColor, Shader.TileMode.MIRROR);
                    mChartPaint.setShader(lg);
                }
            }

            mChartPaint.setStyle(Paint.Style.FILL);
            //画阴影
            if (i == number * 2 || i == number * 2 + 1) {
                mShadowPaint.setColor(Color.BLUE);
            } else {
                mShadowPaint.setColor(Color.WHITE);
            }

            //画柱状图
            RectF rectF = new RectF();
            rectF.left = mChartWidth;
            rectF.right = mChartWidth + mSize;
            rectF.bottom = mHeight - 100;
            rectF.top = (float) (mHeight - 100 - list.get(i) * size);
            canvas.drawRoundRect(rectF, 10, 10, mChartPaint);
            //canvas.drawRect(mChartWidth, mHeight - 100 - list.get(i) * size, mChartWidth + mSize, mHeight - 100, mChartPaint)
            // ;// 长方形
            mChartWidth += (i % 2 == 0) ? (3 + getWidth() / 39) : (getWidth() / 13 - 3 - mSize);
        }
    }

    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);
        if (hasWindowFocus) {

        }
    }

    /**
     * 注意:
     * 当屏幕焦点变化时重新侧向起始位置,必须重写次方法,否则当焦点变化时柱状图会跑到屏幕外面
     */

    @Override
    protected void onWindowVisibilityChanged(int visibility) {
        super.onWindowVisibilityChanged(visibility);
        if (visibility == VISIBLE) {
            mSize = getWidth() / 39;
            mStartWidth = getWidth() / 13;
            mChartWidth = getWidth() / 13 - mSize - 3;
        }
    }

    /**
     * 柱状图touch事件
     * 获取触摸位置计算属于哪个月份的
     * @param ev
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent ev) {

        int x = (int) ev.getX();
        int y = (int) ev.getY();
        int left = 0;
        int top = 0;
        int right = mWidth / 12;
        int bottom = mHeight - 100;
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                for (int i = 0; i < 12; i++) {
                    rect = new Rect(left, top, right, bottom);
                    left += mWidth / 12;
                    right += mWidth / 12;
                    if (rect.contains(x, y)) {
                        listener.getNumber(i, x, y);
                        number = i;
                        selectIndex = i;
                        selectIndexRoles.clear();
                        ;
                        selectIndexRoles.add(selectIndex * 2 + 1);
                        selectIndexRoles.add(selectIndex * 2);
                        invalidate();
                    }
                }
                break;
            case MotionEvent.ACTION_UP:

                break;
        }
        return true;
    }

    public interface getNumberListener {
        void getNumber(int number, int x, int y);
    }

    public int getLeftColor() {
        return leftColor;
    }

    public void setLeftColor(int leftColor) {
        this.leftColor = leftColor;
    }

    public int getRightColor() {
        return rightColor;
    }

    public void setRightColor(int rightColor) {
        this.rightColor = rightColor;
    }

    public int getLineColor() {
        return lineColor;
    }

    public void setLineColor(int lineColor) {
        this.lineColor = lineColor;
    }

    public int getSelectLeftColor() {
        return selectLeftColor;
    }

    public void setSelectLeftColor(int selectLeftColor) {
        this.selectLeftColor = selectLeftColor;
    }

    public int getSelectRightColor() {
        return selectRightColor;
    }

    public void setSelectRightColor(int selectRightColor) {
        this.selectRightColor = selectRightColor;
    }

    public int getLefrColorBottom() {
        return lefrColorBottom;
    }

    public void setLefrColorBottom(int lefrColorBottom) {
        this.lefrColorBottom = lefrColorBottom;
    }

    public int getRightColorBottom() {
        return rightColorBottom;
    }

    public void setRightColorBottom(int rightColorBottom) {
        this.rightColorBottom = rightColorBottom;
    }
}

3.具体使用:

private void initChatView() {

        myChartView.setLefrColorBottom(getResources().getColor(R.color.leftColorBottom));
        myChartView.setLeftColor(getResources().getColor(R.color.leftColor));
        myChartView.setRightColor(getResources().getColor(R.color.rightColor));
        myChartView.setRightColorBottom(getResources().getColor(R.color.rightBottomColor));
        myChartView.setSelectLeftColor(getResources().getColor(R.color.selectLeftColor));
        myChartView.setSelectRightColor(getResources().getColor(R.color.selectRightColor));
        myChartView.setLineColor(getResources().getColor(R.color.xyColor));
        chartList = new ArrayList<>();

        relativeLayout = (RelativeLayout) findViewById(R.id.linearLayout);
        relativeLayout.removeView(llChart);
        Random random = new Random();
        while (chartList.size() < 24) {
            int randomInt = random.nextInt(100);
            chartList.add((float) randomInt);
        }
        myChartView.setList(chartList);
        myChartView.setListener(new MyChartView.getNumberListener() {
            @Override
            public void getNumber(int number, int x, int y) {
                relativeLayout.removeView(llChart);
                //反射加载点击柱状图弹出布局
                llChart = (LinearLayout) LayoutInflater.from(MainActivity.this).inflate(R.layout.layout_shouru_zhichu, null);
                TextView tvZhichu = (TextView) llChart.findViewById(R.id.tv_zhichu);
                TextView tvShouru = (TextView) llChart.findViewById(R.id.tv_shouru);
                tvZhichu.setText((number + 1) + "月支出" + " " + chartList.get(number * 2));
                tvShouru.setText ( "收入: " + chartList.get(number * 2 + 1));
                llChart.measure(0, 0);//调用该方法后才能获取到布局的宽度
                RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT,
                    RelativeLayout.LayoutParams.WRAP_CONTENT);
                params.leftMargin = x - 100;
                if (x - 100 < 0) {
                    params.leftMargin = 0;
                } else if (x - 100 > relativeLayout.getWidth() - llChart.getMeasuredWidth()) {
                    //设置布局距左侧屏幕宽度减去布局宽度
                    params.leftMargin = relativeLayout.getWidth() - llChart.getMeasuredWidth();
                }
                llChart.setLayoutParams(params);
                relativeLayout.addView(llChart);
            }
        });
    }

经过以上步骤,我们的双条竖向柱状图就绘制完成了,也显示出来了。其实自己坐下来仔细拿笔算一下,画一下,也没有想象的那么难。至于单条和横向的实现原理都一样,比这个要简单的多。哦对了,横向的我只是自定义了一个横向的柱状图View,然后用ListView显示的各个部门的具体内容。好了最后感谢我的朋友帮了我很多忙Young_Kai,大家也可以看看他的博客,很不错的。

代码我已经上传我的Github,欢迎大家star,fork:
https://github.com/Hankkin/MyCartDemo

最后推荐一下老刘解析的MPChart

http://blog.csdn.net/qq_26787115

相关文章
|
14天前
|
存储 Shell Android开发
基于Android P,自定义Android开机动画的方法
本文详细介绍了基于Android P系统自定义开机动画的步骤,包括动画文件结构、脚本编写、ZIP打包方法以及如何将自定义动画集成到AOSP源码中。
40 2
基于Android P,自定义Android开机动画的方法
|
12天前
|
供应链 物联网 区块链
未来触手可及:探索新兴技术的趋势与应用安卓开发中的自定义视图:从基础到进阶
【8月更文挑战第30天】随着科技的飞速发展,新兴技术如区块链、物联网和虚拟现实正在重塑我们的世界。本文将深入探讨这些技术的发展趋势和应用场景,带你领略未来的可能性。
|
14天前
|
测试技术 Android开发 Python
探索软件测试的艺术:从基础到高级安卓应用开发中的自定义视图
【8月更文挑战第29天】在软件开发的世界中,测试是不可或缺的一环。它如同艺术一般,需要精细的技巧和深厚的知识。本文旨在通过浅显易懂的语言,引领读者从软件测试的基础出发,逐步深入到更复杂的测试策略和工具的使用,最终达到能够独立进行高效测试的水平。我们将一起探索如何通过不同的测试方法来确保软件的质量和性能,就像艺术家通过不同的色彩和笔触来完成一幅画作一样。
|
3天前
|
Android开发
Android中SurfaceView的双缓冲机制和普通View叠加问题解决办法
本文介绍了 Android 平台上的 SurfaceView,这是一种高效的图形渲染控件,尤其适用于视频播放、游戏和图形动画等场景。文章详细解释了其双缓冲机制,该机制通过前后缓冲区交换来减少图像闪烁,提升视觉体验。然而,SurfaceView 与普通 View 叠加时可能存在 Z-Order 不一致、同步问题及混合渲染难题。文中提供了使用 TextureView、调整 Z-Order 和创建自定义组合控件等多种解决方案。
26 9
|
6天前
|
Android开发 容器
Android经典实战之如何获取View和ViewGroup的中心点
本文介绍了在Android中如何获取`View`和`ViewGroup`的中心点坐标,包括计算相对坐标和屏幕上的绝对坐标,并提供了示例代码。特别注意在视图未完成测量时可能出现的宽高为0的问题及解决方案。
18 7
|
12天前
|
XML 搜索推荐 Android开发
安卓开发中的自定义View组件实践
【8月更文挑战第30天】探索Android世界,自定义View是提升应用界面的关键。本文以简洁的语言带你了解如何创建自定义View,从基础到高级技巧,一步步打造个性化的UI组件。
|
14天前
|
Android开发
Android在rootdir根目录创建自定义目录和挂载点的方法
本文介绍了在Android高通平台的根目录下创建自定义目录和挂载点的方法,通过修改Android.mk文件并使用`LOCAL_POST_INSTALL_CMD`变量在编译过程中添加目录,最终在ramdisk.img的系统根路径下成功创建了`/factory/bin`目录。
32 1
|
4天前
|
前端开发 搜索推荐 Android开发
探索安卓开发中的自定义视图##
【9月更文挑战第6天】 在安卓应用开发的世界里,自定义视图如同绘画艺术中的色彩,它们为界面设计增添了无限可能。通过掌握自定义视图的绘制技巧,开发者能够创造出既符合品牌形象又提升用户体验的独特界面元素。本文将深入浅出地介绍如何从零开始构建一个自定义视图,包括基础框架搭建、关键绘图方法实现、事件处理机制以及性能优化策略。准备好让你的安卓应用与众不同了吗?让我们开始吧! ##
|
17天前
|
前端开发 Android开发 开发者
安卓开发中的自定义视图:构建你的第一个控件
【8月更文挑战第26天】在安卓开发的浩瀚海洋中,自定义视图是一块充满魔力的乐土。它不仅是开发者展示创造力的舞台,更是实现独特用户体验的关键。本文将带你步入自定义视图的世界,从基础概念到实战应用,一步步教你如何打造自己的第一个控件。无论你是初学者还是有经验的开发者,这篇文章都将为你的开发之旅增添新的风景。
|
7天前
|
存储 Android开发 开发者
探索安卓开发之旅:从新手到专家的必经之路
【9月更文挑战第3天】在这篇文章中,我们将踏上一场激动人心的旅程,深入探索安卓开发的广阔天地。无论你是初涉编程世界的新手,还是期望提升技能的开发者,这里都有你需要的知识与技巧。我们将从基础概念讲起,逐步引导你了解安卓应用的核心组件,并分享实用的开发建议。准备好了吗?让我们一起开启这段成长之旅吧!