Android柱状图(纯手撸、可左右滑动)

前言:

Android本身是没有柱状图这个控件的,不过网上有不少强大的开源库,酷炫又方便,这里不再赘述。

效果图:

(标清+大水印)凑合着看

代码:

皆尽所能的优化/简化了代码,实际代码量就两百来行,教科书级别的注释,复制粘贴就能用

package com.zistone.factorytest0718.view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;

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

/**
 * 柱状图控件
 *
 * @author LiWei
 * @date 2021/2/19 16:17
 * @email 652276536@qq.com
 */
public class MyBarGraphView extends View {

    /**
     * 实现柱状图可以左右滑动的线程
     */
    private class HorizontalScrollRunnable implements Runnable {

        private float _speed;

        public HorizontalScrollRunnable(float speed) {
            this._speed = speed;
        }

        @Override
        public void run() {
            if (Math.abs(_speed) < 30) {
                _isScrolling = false;
                return;
            }
            _isScrolling = true;
            _afterScrollDraw += _speed / 15;
            _speed = _speed / 1.15f;
            //向右滑动
            if ((_speed) > 0) {
                if (_afterScrollDraw > 0) {
                    _afterScrollDraw = 0;
                }
            }
            //向左滑动
            else {
                if (-_afterScrollDraw > GetScreenOutsideLength()) {
                    _afterScrollDraw = -GetScreenOutsideLength();
                }
            }
            postDelayed(this, 20);
            invalidate();
        }
    }

    private static final String TAG = "MyBarGraphView";

    //横向滑动的线程
    private HorizontalScrollRunnable _hScrollRunnable;
    //屏幕的宽度
    private int _screenWidth = 0;
    //控件的高度
    private int _height = DpToPx(180);
    //柱状图之间的间隔
    private int _barInterval = 50;
    //柱状图的宽度
    private int _barWidth = 30;
    //柱状图顶部的文字大小
    private int _topTxtSize = 18;
    //柱状图顶部文字的颜色
    private int _topTxtColor = Color.RED;
    //柱状图底部(X轴下面)的文字大小
    private int _bottomTxtSize = 22;
    //柱状图底部(X轴下面)的文字颜色
    private int _bottomTxtColor = Color.RED;
    //柱状图的颜色
    private int _barColor = Color.GREEN;
    //X轴的颜色
    private int _xLineColor = Color.RED;
    //绘制柱状图顶部文字的画笔
    private Paint _topTxtPaint;
    //绘制柱状图底部(X轴下面)文字的画笔
    private Paint _bottomTxtPaint;
    //绘制柱状图的画笔
    private Paint _barPaint;
    //绘制X轴的画笔
    private Paint _xLinePaint;
    //绘制柱状图的区域,绘制柱状图底部(X轴下面)文字的区域
    private Rect _barRect, _bottomTxtRect;
    //X、Y轴的数据
    private List<String> _listX = new ArrayList<>();
    private List<Integer> _listY = new ArrayList<>();
    //柱状图底部(X轴下面)文字的高度
    private int _bottomTxtHeight = DpToPx(30);
    //柱状图顶部文字区域的高度
    private int _topTxtHeight = DpToPx(30);
    //柱状图的高度比
    private float _heightScale = 1;
    //触摸时按下的横向坐标
    private float _touchDownX = 0;
    //记录按下的时间,用来判断是否滑动
    private long _touchStartTime = 0;
    //滑动以后绘制的起始位置
    private float _afterScrollDraw = 0;
    //横向滑动一次的距离
    private float _hScrollDistance = 0;
    //是否正在滑动
    private boolean _isScrolling = false;

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

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

    public MyBarGraphView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        InitPaint();
    }

    private void InitPaint() {
        //柱状图顶部的文字
        _topTxtPaint = new Paint();
        _topTxtPaint.setTextSize(_topTxtSize);
        _topTxtPaint.setColor(_topTxtColor);
        _topTxtPaint.setStrokeCap(Paint.Cap.ROUND);
        _topTxtPaint.setStyle(Paint.Style.FILL);
        _topTxtPaint.setDither(true);
        //柱状图底部(X轴下面)的文字
        _bottomTxtPaint = new Paint();
        _bottomTxtPaint.setTextSize(_bottomTxtSize);
        _bottomTxtPaint.setColor(_bottomTxtColor);
        _bottomTxtPaint.setStrokeCap(Paint.Cap.ROUND);
        _bottomTxtPaint.setStyle(Paint.Style.FILL);
        _bottomTxtPaint.setDither(true);
        //柱状图
        _barPaint = new Paint();
        _barPaint.setTextSize(_topTxtSize);
        _barPaint.setColor(_barColor);
        _barPaint.setStrokeCap(Paint.Cap.ROUND);
        _barPaint.setStyle(Paint.Style.FILL);
        _barPaint.setDither(true);
        //X轴
        _xLinePaint = new Paint();
        _xLinePaint.setTextSize(_topTxtSize);
        _xLinePaint.setColor(_xLineColor);
        _xLinePaint.setStrokeCap(Paint.Cap.ROUND);
        _xLinePaint.setStyle(Paint.Style.FILL);
        _xLinePaint.setDither(true);
        //设置底部线的宽度
        _xLinePaint.setStrokeWidth(DpToPx(1f));
        //柱状图底部(X轴下面)的文字区域
        _bottomTxtRect = new Rect();
        //柱状图区域
        _barRect = new Rect();
    }

    public void SetData(List<String> listX, List<Integer> listY) {
        _listX = listX;
        _listY = listY;
        _heightScale = (float) GetMaxValue() / (float) (_height - _bottomTxtHeight - _topTxtHeight);
        invalidate();
    }

    /**
     * dp转px
     *
     * @param dp
     * @return
     */
    private int DpToPx(float dp) {
        final float scale = getContext().getResources().getDisplayMetrics().density;
        return (int) (dp * scale + 0.5f);
    }

    /**
     * sp转px
     *
     * @param sp
     * @return
     */
    private int SpToPx(float sp) {
        final float fontScale = getContext().getResources().getDisplayMetrics().scaledDensity;
        return (int) (sp * fontScale + 0.5f);
    }

    /**
     * 获取Y轴数据里最大的值,用于计算每个柱状图的高度
     *
     * @return
     */
    private int GetMaxValue() {
        int max = 0;
        if (_listY.size() > 0) {
            max = _listY.get(0);
            for (int i = 0; i < _listY.size(); i++) {
                if (_listY.get(i) > max) {
                    max = _listY.get(i);
                }
            }
        }
        return max;
    }

    /**
     * 获取屏幕尺寸信息
     *
     * @return
     */
    private ArrayList<Integer> GetScreenProperty() {
        WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics dm = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(dm);
        //屏幕宽度(像素)
        int width = dm.widthPixels;
        //屏幕高度(像素)
        int height = dm.heightPixels;
        //屏幕密度(0.75 / 1.0 / 1.5)
        float density = dm.density;
        //屏幕密度dpi(120 / 160 / 240)
        int densityDpi = dm.densityDpi;
        //屏幕宽度算法:屏幕宽度(像素) / 屏幕密度
        int screenWidth = (int) (width / density);
        int screenHeight = (int) (height / density);
        ArrayList<Integer> integers = new ArrayList<>();
        integers.add(screenWidth);
        integers.add(screenHeight);
        return integers;
    }

    /**
     * 获取超出当前屏幕以外部分的长度
     *
     * @return
     */
    private int GetScreenOutsideLength() {
        //(柱状图的宽度 + 柱状图之间的间隔) * 个数 - 屏幕宽度
        return (_barWidth + _barInterval) * _listX.size() - _screenWidth;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                _touchDownX = event.getX();
                _touchStartTime = System.currentTimeMillis();
                //点击的时候如果正在滑动则停止
                if (_isScrolling) {
                    removeCallbacks(_hScrollRunnable);
                    _isScrolling = false;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                float x = event.getX();
                //移动距离 = 滑动时的坐标 - 按下时的坐标
                float moveX = x - _touchDownX;
                _afterScrollDraw += moveX;
                Log.i(TAG, "moveX = " + moveX);
                Log.i(TAG, "_afterScrollDraw = " + _afterScrollDraw);
                //向右滑动
                if (moveX > 0) {
                    Log.i(TAG, "向右滑动");
                    if (_afterScrollDraw > 0) {
                        _afterScrollDraw = 0;
                    }
                }
                //向左滑动
                else {
                    Log.i(TAG, "向左滑动");
                    if (-_afterScrollDraw > GetScreenOutsideLength()) {
                        _afterScrollDraw = -GetScreenOutsideLength();
                    }
                }
                _hScrollDistance = moveX;
                //如果数据量少,没有充满横屏就没必要重新绘制
                if (GetScreenOutsideLength() > 0) {
                    invalidate();
                }
                _touchDownX = x;
                break;
            case MotionEvent.ACTION_UP:
                long endTime = System.currentTimeMillis();
                //滑动的速度如果大于某个值,并且要绘制的数据大于整个屏幕,才允许横向滑动
                float speed = _hScrollDistance / (endTime - _touchStartTime) * 1000;
                if (Math.abs(speed) > 100 && !_isScrolling && GetScreenOutsideLength() > 0) {
                    _hScrollRunnable = new HorizontalScrollRunnable(speed);
                    this.post(_hScrollRunnable);
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                break;
        }
        return true;
    }

    /**
     * 自定义View尺寸,比onDraw先执行
     * 计算过程参照父容器给出的大小以及自己的特点算出结果
     * <p>
     * EXACTLY:精确模式
     * 父容器能直接计算自定义控件的大小,一般是设置为match_parent或者固定值
     * <p>
     * AT_MOST:至多不超过模式
     * 父容器指定一个大小,自定义控件的大小不能超过这个值,父容器不能直接计算出自定义控件的大小,需要它自己计算,然后再去设置自定义控件的大小(setMeasuredDimension),一般是设置为warp_content
     * <p>
     * UNSPECIFIED:不确定模式
     * 父容器不对子View有任何限制,要多大给多大,多见于ListView、ScrollView、GridView等。
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int width;
        int height;
        if (widthMode == MeasureSpec.EXACTLY) {
            _screenWidth = width = widthSize;
        } else {
            width = GetScreenProperty().get(0);
        }
        if (heightMode == MeasureSpec.EXACTLY) {
            _height = height = heightSize;
        } else {
            height = _height;
        }
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //绘制X轴,Y方向距离加1个像素,避免X轴和柱状图有重叠
        canvas.drawLine(0, _height - _bottomTxtHeight + 1, _listX.size() * (_barWidth + _barInterval), _height - _bottomTxtHeight, _xLinePaint);
        //如果没有数据,绘制loading...
        if (_listY.size() == 0) {
            String txt = "loading...";
            float txtWidth = _bottomTxtPaint.measureText(txt);
            canvas.drawText(txt, _screenWidth / 2 - txtWidth / 2, _height / 2 - 10, _bottomTxtPaint);
        } else {
            //柱状图的横向起始位置
            int startX = (int) (_afterScrollDraw);
            //柱状图的纵向结束位置
            int stopY = _height - _bottomTxtHeight;
            for (int i = 0; i < _listX.size(); i++) {
                String xTxt = _listX.get(i);
                int yTxt = _listY.get(i);
                //每个柱状图的高度
                float barHeight = 0;
                if (_heightScale != 0) {
                    barHeight = (float) yTxt / _heightScale;
                }
                //柱状图的纵向起始位置
                int startY = (int) (_height - _bottomTxtHeight - barHeight);
                float topTxtWidth = _topTxtPaint.measureText(yTxt + "");
                //柱状图顶部文字的坐标
                float topTxtX = startX + _barWidth / 2 - topTxtWidth / 2;
                float topTxtY = startY - 20;
                canvas.drawText(yTxt + "", topTxtX, topTxtY, _topTxtPaint);
                _barRect.set(startX, startY, startX + _barWidth, stopY);
                canvas.drawRect(_barRect, _barPaint);
                float bottomTxtWidth = _bottomTxtPaint.measureText(xTxt);
                //柱状图底部(X轴下面)文字的坐标
                float bottomTxtX = startX + _barWidth / 2 - bottomTxtWidth / 2;
                _bottomTxtPaint.getTextBounds(xTxt, 0, xTxt.length(), _bottomTxtRect);
                float bottomTxtY = _height - _bottomTxtHeight + 20 + _bottomTxtRect.height();
                //绘制底部的文字
                canvas.drawText(xTxt, bottomTxtX, bottomTxtY, _bottomTxtPaint);
                //下一个柱状图开始绘制的位置
                startX = startX + _barWidth + _barInterval;
            }
        }
    }

}

调用:

做了两条又长又假的数据,循环给它赋值,用于演示滑动、跳动

package com.zistone.factorytest0718;

import android.os.Bundle;

import androidx.appcompat.app.AppCompatActivity;

import com.zistone.factorytest0718.view.MyBarGraphView;

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

/**
 * 用来测试一些东西的,没有任何实际功能...
 *
 * @author LiWei
 * @date 2020/7/18 9:33
 * @email 652276536@qq.com
 */
public class Test1Activity extends AppCompatActivity {

    private static final String TAG = "Test1Activity";

    private MyBarGraphView _myBarGraphView;
    private boolean _threadFlag = false;
    private List<String> _listX = new ArrayList<String>() {{
        add("贰");
        add("仨");
        add("肆");
        add("伍");
        add("陆");
        add("染");
        add("捌");
        add("玖");
        add("壹");
        add("拾");
        add("贰");
        add("仨");
        add("肆");
        add("伍");
        add("陆");
        add("染");
        add("捌");
        add("玖");
        add("壹");
        add("1");
        add("2");
        add("3");
        add("4");
        add("5");
        add("6");
        add("7");
        add("8");
        add("9");
        add("10");
    }};
    private List<Integer> _listY1 = new ArrayList<Integer>() {{
        add(10);
        add(56);
        add(1);
        add(10);
        add(78);
        add(89);
        add(0);
        add(67);
        add(23);
        add(4);
        add(78);
        add(90);
        add(3);
        add(54);
        add(12);
        add(56);
        add(89);
        add(8);
        add(69);
        add(0);
        add(9);
        add(0);
        add(7);
        add(0);
        add(36);
        add(5);
        add(3);
        add(36);
        add(234);
    }};
    private List<Integer> _listY2 = new ArrayList<Integer>() {{
       add(10);
        add(56);
        add(0);
        add(89);
        add(0);
        add(67);
        add(54);
        add(10);
        add(78);
        add(89);
        add(234);
        add(89);
        add(0);
        add(67);
        add(67);
        add(23);
        add(4);
        add(78);
        add(90);
        add(3);
        add(54);
        add(12);
        add(56);
        add(89);
        add(8);
        add(69);
        add(36);
        add(54);
        add(12);
    }};

    @Override
    protected void onDestroy() {
        _threadFlag = true;
        super.onDestroy();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test1);
        _myBarGraphView = findViewById(R.id.控件名);
        Thread thread = new Thread(() -> {
            int i = 0;
            while (!_threadFlag) {
                int finalI = i;
                runOnUiThread(() -> {
                    if (finalI % 2 == 0)
                        _myBarGraphView.SetData(_listX, _listY1);
                    else
                        _myBarGraphView.SetData(_listX, _listY2);
                });
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                i++;
            }
        });
        thread.start();
    }

}

star我就不要了,关注、收藏、点赞总要有一个吧?不会一个都没有吧?不会吧?

  • 15
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值