自定义柱状折线图

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq77485042/article/details/79110634

最近项目中有用到折线柱状图,带渐变色的圆角矩形。听起来好像很可怕的样子。。。但是做安卓开发,自定义控件又是我们必不可少需要具备的技能,所以就花了两天时间写了三个柱状图的控件。

效果图如下:
这里写图片描述

这篇博客我就讲讲最上面的控件的绘制思路,下面的两个控件的内容基本上都包括在第一个里面了。

我们拿到一个需求需要慢慢分析,把大的需求拆分成一个一个的小需求,那么我就来分析一下这个自定义控件的思路吧。

首先我们分析需要绘制的部分:

1.绘制左边的纵坐标值,从底部往上绘制。
2.绘制背景色。
3.绘制日期上边的灰色横线。
4.绘制控件下面的日期值。
5.绘制柱状图。
6.绘制柱状图相连的折线。
7.绘制选中的灰色柱状图。
8.绘制选中的柱状图上面的小箭头图片。

其他需求分析:

9.柱状图的点击事件。
10.控件的滑动事件。

经过我的拆分完成此自定义控件需要完成以上10个小需求。

这里我先说说我遇到的坑,因为我这边的滑动事件是平移整个画布,一开始我把上面1-8个步骤绘制在一个控件中,会发现我向左滑动时把左边的数值也给滑动出去了。(当然也可以在我滑动时不从绘制左边的值,但是柱子还是会挡住左边的值,我们的需求是以左边的值为边界,不能让柱状图超过左边的值,所以为了完成这个需求我就把绘制也拆分成了两个部分)

我把1、2、3步绘制在一个通用的控件中,把剩余步骤绘制在另外一个控件中。

先来说说左边的通用控件:(HistogramLeftView)

public class HistogramLeftView extends View {
    private int mWidth;//控件宽度
    private int mHeight;//控件高度

    //左边数据值的间距
    private int valueSpacing = 20;

    //普通画笔的宽度
    private int paintWidth = 2;

    //数据文字的大小
    private int valueSize = 18;

    //画笔,设置抗锯齿
    private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

    //左边的值距离控件左边及右边的间距
    private int leftForViewSpacing = 20;

    //控件的宽度和高度的比例
    private float viewSizeScale = 0.7f;

    //底部线条距离底部的距离
    private int lineForViewBottom = 40;

    public HistogramLeftView(Context context, AttributeSet attrs) {
        super(context, attrs);
        WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        mWidth = windowManager.getDefaultDisplay().getWidth();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        mHeight = (int) (mWidth * viewSizeScale);
        setMeasuredDimension(mWidth, mHeight);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawColor(Color.parseColor("#e6e6e6"));
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(Color.parseColor("#adabac"));
        paint.setStrokeWidth(paintWidth);
        canvas.drawLine(leftForViewSpacing, (mHeight - lineForViewBottom), (mWidth - leftForViewSpacing), (mHeight - lineForViewBottom), paint);
        drawLeftValueText(canvas);
    }

    /**
     * 绘制左边的文字
     */
    private void drawLeftValueText(Canvas canvas) {
        paint.setColor(Color.parseColor("#b3b3b3"));
        paint.setStyle(Paint.Style.FILL);
        paint.setTextSize(valueSize);
        paint.setStrokeWidth(paintWidth);
        for (int i = 3; i <= 18; i += 3) {
            canvas.drawText(i + "", leftForViewSpacing, (mHeight - valueSpacing * i) - (lineForViewBottom + paintWidth), paint);
        }
    }
}

以上就是左边控件的代码:因为我们这个自定义控件需要重新绘制所以只能继承自安卓原生的View。

因为我测量了美工给的图片,宽和高的比例是10:7。
所以就出来了这个变量

private float viewSizeScale = 0.7f;

因为我们这个控件需要占满整个屏幕,所以我就得动态测量到屏幕的宽度所以在初始化中就调用了

WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        mWidth = windowManager.getDefaultDisplay().getWidth();

在通过比例动态的算出控件的高度

然后在onMeasure()方法中调用:

setMeasuredDimension(mWidth, mHeight);

调用此方法就是把控件的大小强制的设置为我们想要的大小,不管你在xml中如何写改控件都是全屏的宽度。

当onMeasure()方法走完了就会去调用onDraw()方法

首先绘制背景色调用:

canvas.drawColor(Color.parseColor("#e6e6e6"));

这样就完成了我们的第二个步骤

然后绘制底部的线条:

paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.parseColor("#adabac"));
paint.setStrokeWidth(paintWidth);
canvas.drawLine(leftForViewSpacing, (mHeight - lineForViewBottom), (mWidth - leftForViewSpacing), (mHeight - lineForViewBottom), paint);

这里设置画笔的样式,可以是空心的也可以是实心的,这个只针对画图形,对于我们这个画线来说没作用,然后设置画笔的颜色,也就是我们绘制的线段需要用到什么颜色。最后调用canvas的drawLine()方法,方法中的5个参数分别代表:线的起始点的x坐标点、线的起始点的y坐标点,线的终点的x坐标点,线的终点的y坐标点和画笔。

这样就完成了我们的第3个步骤

然后绘制左边的值:
调用了drawLeftValueText(canvas)方法

private void drawLeftValueText(Canvas canvas) {
        paint.setColor(Color.parseColor("#b3b3b3"));
        paint.setStyle(Paint.Style.FILL);
        paint.setTextSize(valueSize);
        paint.setStrokeWidth(paintWidth);
        for (int i = 3; i <= 18; i += 3) {
            canvas.drawText(i + "", leftForViewSpacing, (mHeight - valueSpacing * i) - (lineForViewBottom + paintWidth), paint);
        }
    }

这里要注意一下绘制文字时,需要设置setTextSize();设置文字的大小,因为我这边是需要绘制从3开始的6个值,所以我这里就调用到了循环绘制,这里可以看到绘制左边值的时候x坐标是不需要改变的,只需要改变Y坐标。最后调用canvas.drawText();该方法中的4个参数分别代表:需要绘制的文本,需要绘制文本的左上角x坐标,需要绘制文本的左上角y坐标和画笔。

这样就完成了我们的第一个步骤

左边的通用控件也就绘制完成。(也就是不移动的部分绘制完成)

下面就分析分析右边可以滑动的控件,也是这个控件的重头戏(DiscountedGradientView)还是一样的先把代码方法上去,嘿嘿

public class DiscountedGradientView extends View {
    //一屏幕只有7根圆柱的模式
    private static int SEVEN_MODE = 0;
    //一屏幕有15根圆柱的模式
    private static int FIFTEEN_MODE = 1;
    //当前模式的标志值
    private int currentMode = -1;


    private int mWidth;//控件宽度
    private int mHeight;//控件高度
    //左边数据值的间距(一个刻度的距离)
    private int valueSpacing = 20;
    //画笔,设置抗锯齿
    private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    //日期文字的大小
    private int dateSize = 18;

    //左边的值距离控件的间距
    private int leftForViewSpacing = 20;

    //控件的宽度和高度的比例
    private float viewSizeScale = 0.7f;

    //柱状图的宽度
    private float pillarWidth;
    //柱状图的间距大小
    private float pillarSpacing;
    //数据集合
    private List<Values> values;
    //资源文件
    private Drawable shape1;
    private Drawable shape2;
    private Drawable shape3;
    private Drawable shape4;
    private Bitmap bitmap;
    //柱状图和数据捆绑的集合
    private List<Task> tasks = new ArrayList<>();
    //监听器
    private OnItemPillarClickListeners onItemPillarClickListeners;
    //是否点击
    private boolean isClick;
    //手指按下时的位置
    private float startX;
    private float startY;
    private float moveX;
    private int currentPos = -1;
    private float currentX = -1;

    public OnItemPillarClickListeners getOnItemPillarClickListeners() {
        return onItemPillarClickListeners;
    }

    public void setOnItemPillarClickListeners(OnItemPillarClickListeners onItemPillarClickListeners) {
        this.onItemPillarClickListeners = onItemPillarClickListeners;
    }

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

    public DiscountedGradientView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setData();
        //3种shape的资源
        shape1 = context.getResources().getDrawable(R.drawable.shape1);
        shape2 = context.getResources().getDrawable(R.drawable.shape2);
        shape3 = context.getResources().getDrawable(R.drawable.shape3);
        shape4 = context.getResources().getDrawable(R.drawable.shape4);
        bitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.bottom);
        //动态获取屏幕宽度
        WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        mWidth = windowManager.getDefaultDisplay().getWidth();
        //设定屏幕高度
        mHeight = (int) (mWidth * viewSizeScale);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //获取父容器给到的最大宽度
        mWidth = MeasureSpec.getSize(widthMeasureSpec);
        //柱状图宽度
        pillarWidth = (float) mWidth / 30;
        if (currentMode == SEVEN_MODE) {
            //当一屏只有7根柱子时,间距的宽度
            pillarSpacing = (mWidth - (values.size() * pillarWidth)) / (values.size());
        } else {
            //当一屏有15根柱子时,间距的宽度
            pillarSpacing = (float) mWidth / 30;
        }
        //强制把控件设置成我们想要的固定的大小
        setMeasuredDimension(mWidth, mHeight);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //平移画布(平移的距离时moveX,为负数时为向左移动,为正数时为向右移动)
        canvas.translate(moveX, 0);
        //画柱状图
        drawPillarView(canvas);
        //画柱状图中虚线
        drawDottedLine(canvas);
        //画日期值
        drawDate(canvas);
    }

    /**
     * 绘制柱状图
     *
     * @param canvas
     */
    private void drawPillarView(Canvas canvas) {
        //每次滑动都会调用绘制的方法(所以应该清空集合)
        tasks.clear();
        //添加灰色选中背景
        for (int i = 0; i < values.size(); i++) {
            if (currentPos == i) {
                //灰色阴影的矩形范围
                Rect rect2 = new Rect((int) currentX, mHeight - (42 + leftForViewSpacing * 18), (int) (currentX + pillarWidth), mHeight - 42);
                shape4.setBounds(rect2);
                shape4.draw(canvas);
                //绘制箭头图片,选择图片的哪个位置
                Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
                //箭头图片在控件中的矩形位置
                Rect rect3 = new Rect((int) currentX, mHeight - (42 + leftForViewSpacing * 18) - 30, (int) currentX + 23, mHeight - (42 + leftForViewSpacing * 18) - 10);
                canvas.drawBitmap(bitmap, rect, rect3, paint);
            }
            //渐变色的柱状图位置确认
            Rect rect5 = new Rect((int) (i * (pillarWidth + pillarSpacing) + pillarSpacing / 2), mHeight - values.get(i).getMax() * 20 - 42, (int) ((1 + i) * pillarWidth + i * pillarSpacing + pillarSpacing / 2), mHeight - values.get(i).getMin() * 20 - 42);
            //根据不同的情况显示不同的渐变色
            if (values.get(i).getMax() >= 10 && values.get(i).getMax() < 13) {
                shape3.setBounds(rect5);
                shape3.draw(canvas);
            } else if (values.get(i).getMax() >= 13) {
                shape1.setBounds(rect5);
                shape1.draw(canvas);
            } else {
                shape2.setBounds(rect5);
                shape2.draw(canvas);
            }
            //把数据和矩形绑在一起
            tasks.add(new Task(rect5, values.get(i)));
        }
    }

    /**
     * 绘制虚线
     *
     * @param canvas
     */
    private void drawDottedLine(Canvas canvas) {
        paint.setColor(Color.parseColor("#222222"));
        paint.setStrokeWidth(0.5f);
        paint.setPathEffect(new DashPathEffect(new float[]{5, 5}, 0));
        float average2 = 0;
        for (int i = 0; i < values.size(); i++) {
            Values values = this.values.get(i);
            float average = (float) (values.getMax() + values.getMin()) / 2;
            if (i + 1 < this.values.size()) {
                Values v2 = this.values.get(i + 1);
                average2 = (float) (v2.getMax() + v2.getMin()) / 2;
            }
            canvas.drawLine(i * (pillarWidth + pillarSpacing) + pillarSpacing / 2 + pillarWidth, (mHeight - valueSpacing * average) - 42, ((1 + i) * pillarSpacing + pillarWidth * i + pillarSpacing / 2 + pillarWidth), (mHeight - valueSpacing * average2) - 42, paint);
        }
    }

    /**
     * 绘制日期
     *
     * @param canvas
     */
    private void drawDate(Canvas canvas) {
        paint.setColor(Color.parseColor("#9e9c9d"));
        paint.setStyle(Paint.Style.FILL);
        paint.setTextSize(dateSize);
        for (int i = 0; i < values.size(); i++) {
            canvas.drawText(values.get(i).getDate() + "日", i * pillarWidth + pillarSpacing * i + (pillarSpacing / 2), mHeight - 10, paint);
        }
    }

    /**
     * 设置数据
     */
    public void setData() {
        this.values = new ArrayList<>();
        Random random = new Random();
        for (int i = 1; i <= 30; i++) {
            Values v = new Values(random.nextInt(13) + 5, random.nextInt(5), i);
            this.values.add(v);
        }
        if (values.size() <= 7) {
            currentMode = SEVEN_MODE;
        } else if (values.size() > 15) {
            currentMode = FIFTEEN_MODE;
        } else {
            currentMode = -1;
        }
    }

    /**
     * 覆盖手势
     *
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = x;
                startY = y;
                isClick = true;
                Log.e("lee", "ACTION_DOWN" + x);
                break;
            case MotionEvent.ACTION_MOVE:
                if (currentMode == SEVEN_MODE || currentMode == -1) {
                    return true;
                }
                if (Math.abs(x - startY) > 10) {//视为滑动
                    isClick = false;
                }
                //计算滑动的距离
                moveX = moveX + (x - startX);
                if (moveX >= 0) {
                    moveX = 0;
                }
                if (moveX <= -((values.size() - 15) * (pillarWidth + pillarSpacing)) + pillarSpacing / 2) {
                    moveX = -((values.size() - 15) * (pillarWidth + pillarSpacing) + pillarSpacing / 2);
                }
                Log.e("lee", "ACTION_MOVE" + x);
                this.invalidate();
                break;
            case MotionEvent.ACTION_UP:
                Log.e("lee", "ACTION_UP" + x);
                if (isClick) {
                    for (int i = 0; i < tasks.size(); i++) {
                        Rect rect = tasks.get(i).getRect();
                        if (rect.left < (x - moveX) && rect.right > (x - moveX)) {
                            currentPos = i;
                            currentX = rect.left;
                            postInvalidate();
                            if (getOnItemPillarClickListeners() != null) {
                                onItemPillarClickListeners.clickItem(tasks.get(i).getValues());
                            }
                        }
                    }
                }
                break;
        }
        return true;
    }

    /**
     * 回调接口
     */
    public interface OnItemPillarClickListeners {
        //回调方法
        void clickItem(Values Values);
    }
}

仔细的小伙伴会发现有一个图是一个屏幕只有7根柱状图的,有的是一个屏幕15根柱状图的。
就是通过下面的变量来控制模式。

    //一屏幕只有7根圆柱的模式
    private static int SEVEN_MODE = 0;
    //一屏幕有15根圆柱的模式
    private static int FIFTEEN_MODE = 1;
    //当前模式的标志值
    private int currentMode = -1;

在setData的时候判断模式:

/**
     * 设置数据
     */
    public void setData() {
        this.values = new ArrayList<>();
        Random random = new Random();
        for (int i = 1; i <= 30; i++) {
            Values v = new Values(random.nextInt(13) + 5, random.nextInt(5), i);
            this.values.add(v);
        }
        //判断模式
        if (values.size() <= 7) {
            currentMode = SEVEN_MODE;
        } else if (values.size() > 15) {
            currentMode = FIFTEEN_MODE;
        } else {
            currentMode = -1;
        }
    }

这里在初始化的时候引用了shape资源文件

//3种shape的资源
        shape1 = context.getResources().getDrawable(R.drawable.shape1);
        shape2 = context.getResources().getDrawable(R.drawable.shape2);
        shape3 = context.getResources().getDrawable(R.drawable.shape3);
        shape4 = context.getResources().getDrawable(R.drawable.shape4);
        //箭头图片
        bitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.bottom);

在onMeasure()方法中和第一个控件有些不同的地方:

//获取父容器给到的最大宽度
        mWidth = MeasureSpec.getSize(widthMeasureSpec);
        //柱状图宽度
        pillarWidth = (float) mWidth / 30;
        if (currentMode == SEVEN_MODE) {
            //当一屏只有7根柱子时,间距的宽度
            pillarSpacing = (mWidth - (values.size() * pillarWidth)) / (values.size());
        } else {
            //当一屏有15根柱子时,间距的宽度
            pillarSpacing = (float) mWidth / 30;
        }
        //强制把控件设置成我们想要的固定的大小
        setMeasuredDimension(mWidth, mHeight);

改控件获取宽度是通过获取到父容器给到的最大宽度,因为我给该控件设置了magin值,但是高度的话还是要屏幕的宽度乘以比例。所以在初始化的时候就把高度定死了。这里通过判断动态的计算两根柱子之间的间距。

然后调用onDraw();方法

//平移画布(平移的距离时moveX,为负数时为向左移动,为正数时为向右移动)
        canvas.translate(moveX, 0);
        //画柱状图
        drawPillarView(canvas);
        //画柱状图中虚线
        drawDottedLine(canvas);
        //画日期值
        drawDate(canvas);

就会调用以上几个方法(暂时先忽略canvas.translate(moveX, 0);方法)

画柱状图的方法:

/**
     * 绘制柱状图
     *
     * @param canvas
     */
    private void drawPillarView(Canvas canvas) {
        //每次滑动都会调用绘制的方法(所以应该清空集合)
        tasks.clear();
        //添加灰色选中背景
        for (int i = 0; i < values.size(); i++) {
            if (currentPos == i) {
                //灰色阴影的矩形范围
                Rect rect2 = new Rect((int) currentX, mHeight - (42 + leftForViewSpacing * 18), (int) (currentX + pillarWidth), mHeight - 42);
                shape4.setBounds(rect2);
                shape4.draw(canvas);
                //绘制箭头图片,选择图片的哪个位置
                Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
                //箭头图片在控件中的矩形位置
                Rect rect3 = new Rect((int) currentX, mHeight - (42 + leftForViewSpacing * 18) - 30, (int) currentX + 23, mHeight - (42 + leftForViewSpacing * 18) - 10);
                canvas.drawBitmap(bitmap, rect, rect3, paint);
            }
            //渐变色的柱状图位置确认
            Rect rect5 = new Rect((int) (i * (pillarWidth + pillarSpacing) + pillarSpacing / 2), mHeight - values.get(i).getMax() * 20 - 42, (int) ((1 + i) * pillarWidth + i * pillarSpacing + pillarSpacing / 2), mHeight - values.get(i).getMin() * 20 - 42);
            //根据不同的情况显示不同的渐变色
            if (values.get(i).getMax() >= 10 && values.get(i).getMax() < 13) {
                shape3.setBounds(rect5);
                shape3.draw(canvas);
            } else if (values.get(i).getMax() >= 13) {
                shape1.setBounds(rect5);
                shape1.draw(canvas);
            } else {
                shape2.setBounds(rect5);
                shape2.draw(canvas);
            }
            //把数据和矩形绑在一起
            tasks.add(new Task(rect5, values.get(i)));
        }
    }

这里主要讲讲后半截:调用shape3.setBounds();方法把矩形放进去确认位置,然后根据不同的情况调用不同的shape3.draw(canvas);方法绘制出来。其实渐变色和矩形是在shape的xml文件中写好了的。

这样也就完成了第5个步骤

绘制虚线:

/**
     * 绘制虚线
     *
     * @param canvas
     */
    private void drawDottedLine(Canvas canvas) {
        paint.setColor(Color.parseColor("#222222"));
        paint.setStrokeWidth(0.5f);
        float average2 = 0;
        for (int i = 0; i < values.size(); i++) {
            Values values = this.values.get(i);
            float average = (float) (values.getMax() + values.getMin()) / 2;
            if (i + 1 < this.values.size()) {
                Values v2 = this.values.get(i + 1);
                average2 = (float) (v2.getMax() + v2.getMin()) / 2;
            }
            canvas.drawLine(i * (pillarWidth + pillarSpacing) + pillarSpacing / 2 + pillarWidth, (mHeight - valueSpacing * average) - 42, ((1 + i) * pillarSpacing + pillarWidth * i + pillarSpacing / 2 + pillarWidth), (mHeight - valueSpacing * average2) - 42, paint);
        }
    }

先设置画笔颜色,也就是线段的颜色,然后设置画笔粗细,也就是线段的宽度,这里主要介绍一下canvas.drawLine()这个方法上面已经说过了就是绘制线段。

这样也就完成了第6个步骤

绘制日期:

/**
     * 绘制日期
     *
     * @param canvas
     */
    private void drawDate(Canvas canvas) {
        paint.setColor(Color.parseColor("#9e9c9d"));
        paint.setStyle(Paint.Style.FILL);
        paint.setTextSize(dateSize);
        for (int i = 0; i < values.size(); i++) {
            canvas.drawText(values.get(i).getDate() + "日", i * pillarWidth + pillarSpacing * i + (pillarSpacing / 2), mHeight - 10, paint);
        }
    }

这里也没什么好说的绘制文本上面也已经说过了,只不过里面的参数是通过计算的,上面也有注释。

这样也就完成了第4个步骤:
所有有关绘制的流程完成了

下面就讲事件处理的逻辑:

  @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = x;
                startY = y;
                isClick = true;
                Log.e("lee", "ACTION_DOWN" + x);
                break;
            case MotionEvent.ACTION_MOVE:
                if (currentMode == SEVEN_MODE || currentMode == -1) {
                    return true;
                }
                if (Math.abs(x - startY) > 10) {//视为滑动
                    isClick = false;
                }
                //计算滑动的距离
                moveX = moveX + (x - startX);
                if (moveX >= 0) {
                    moveX = 0;
                }
                if (moveX <= -((values.size() - 15) * (pillarWidth + pillarSpacing)) + pillarSpacing / 2) {
                    moveX = -((values.size() - 15) * (pillarWidth + pillarSpacing) + pillarSpacing / 2);
                }
                Log.e("lee", "ACTION_MOVE" + x);
                this.invalidate();
                break;
            case MotionEvent.ACTION_UP:
                Log.e("lee", "ACTION_UP" + x);
                if (isClick) {
                    for (int i = 0; i < tasks.size(); i++) {
                        Rect rect = tasks.get(i).getRect();
                        if (rect.left < (x - moveX) && rect.right > (x - moveX)) {
                            currentPos = i;
                            currentX = rect.left;
                            postInvalidate();
                            if (getOnItemPillarClickListeners() != null) {
                                onItemPillarClickListeners.clickItem(tasks.get(i).getValues());
                            }
                        }
                    }
                }
                break;
        }
        return true;
    }

大家都知道只要触碰了屏幕的话就会走到onTouchEvent();方法中。只不过会根据不同的手势走进不同的case中去。
1.当用户按下时先记录到了用户按下的坐标x和y,然后进入down事件中把startX,startY记录下来,然后判断点击事件的标识isClick就为true了,假设用户立马松开手了就会走到up事件中,此时isclick为true就会走里面的代码,然后遍历这个矩形以及数据的集合判断当前的x坐标属于哪个矩形中把矩形的最左边记录下来,把数据回调出去,然后调用postInvalidate();方法。调用了此方法后会重新走一遍onDraw();方法了,其他的不说了主要说这一段代码:

            if (currentPos == i) {
                //灰色阴影的矩形范围
                Rect rect2 = new Rect((int) currentX, mHeight - (42 + leftForViewSpacing * 18), (int) (currentX + pillarWidth), mHeight - 42);
                shape4.setBounds(rect2);
                shape4.draw(canvas);
                //绘制箭头图片,选择图片的哪个位置
                Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
                //箭头图片在控件中的矩形位置
                Rect rect3 = new Rect((int) currentX, mHeight - (42 + leftForViewSpacing * 18) - 30, (int) currentX + 23, mHeight - (42 + leftForViewSpacing * 18) - 10);
                canvas.drawBitmap(bitmap, rect, rect3, paint);
            }

在up事件中记录下了currentPos的值,重绘制时来匹配用户点击的是哪个矩形然后绘制灰的矩形,绘制上面的小箭头。

回到onTouchEvent();方法中:
当用户点击了屏幕时,还是一样先记录 x和y 然后走到down事件中记录下startX和srartY的位置,该值是固定的。如果当用户滑动屏幕时就会走到move事件中去,此时先要判断是不是可以滑动的模式,如果不是直接返回true把事件消费了。如果滑动的距离大于10个像素的话视为滑动,先把点击事件的值改为false,让待会滑动完之后抬手时不会执行up事件中的代码。然后在记录下滑动的距离滑动的距离就是(x - startX);值为正数时往右滑,反之,往左滑,这里设置了一个边距,不可能让他永远滑下去,所以当滑动的值>=0时就是最左边了不允许在往左滑了,反之右边的边距也是手动计算出来的,然后调用 this.invalidate();方法重新绘制。该控件也就绘制完成了。

把其中一个shape:代码展示出来

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <gradient
        android:angle="90"
        android:centerColor="#4b78a2"
        android:endColor="#4e2f7c"
        android:gradientRadius="800"
        android:startColor="#4cc2c4" />
    <corners android:radius="20dp" />
</shape>

activity类:

public class MainActivity extends AppCompatActivity implements DiscountedGradientView.OnItemPillarClickListeners{
    private DiscountedGradientView view;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        view = (DiscountedGradientView) findViewById(R.id.view);
        view.setOnItemPillarClickListeners(this);
    }

    @Override
    public void clickItem(Values Values) {
        Toast.makeText(MainActivity.this, "第" + Values.getDate() + "天" + "最大值为" + Values.getMax() + "最小值为" + Values.getMin(), Toast.LENGTH_SHORT).show();
    }
}

xml文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.example.lee.myapplication.MainActivity">

            <FrameLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content">

                <com.example.lee.myapplication.HistogramLeftView
                    android:layout_width="match_parent"
                    android:layout_height="match_parent" />

                <com.example.lee.myapplication.DiscountedGradientView
                    android:id="@+id/view"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:layout_marginLeft="25dp"
                    android:layout_marginRight="25dp" />
            </FrameLayout>

</LinearLayout>

猜你喜欢

转载自blog.csdn.net/qq77485042/article/details/79110634