自定义View之StepView(流程/步骤View)

*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

写在前面的话

其实写这个自定义view是有一点故事的,是因为他,我决定写这个view的,由于他的帮助和鼓励,我最终完成了这个view,在此,向他致敬!
ps:故事总是有剧情和结局的,这里不便多讲,送自己句话:且行且珍惜吧!

1.效果图

好了,扯了上面的这些闲话,直接来看效果图吧。


自定义View之StepView(流程/步骤View)_第1张图片
stepview.gif

2.实现思路

  • 首先是画各步骤点之间的线条
  • 接着是画未选中步骤点的图标
  • 第三步是画选中步骤点的图标
  • 最后画出各步骤点对应的说明文字

3.实现细节

3.1概述

StepView继承自View,通过构造方法初始化一些必要参数,然后在onSizeChanged方法中获取View的宽高以及其他额外计算的数据信息,最后通过onDraw方法绘制出View。

3.2首先通过res/values/attrs定义一些细节参数

        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
            
            
            
            
            
        
        
        
            
            
        
        
        
        
        
            
            
        
        
        
    
3.3通过构造方法初始化
public StepView(Context context) {
        this(context, null);
    }

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

    public StepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr);
    }
private void init(Context context, AttributeSet attrs, int defStyleAttr) {
        defaultNormalLineColor = Color.parseColor("#545454");
        defaultPassLineColor = Color.WHITE;
        defaultTextColor = Color.WHITE;
        defaultLineStikeWidth = dp2px(context, 1);
        defaultTextSize = sp2px(context, 80);
        defaultText2DotMargin = dp2px(context, 15);
        defalutMargin = dp2px(context, 100);
        defaultLine2TopMargin = dp2px(context, 30);
        defaultText2BottomMargin = dp2px(context, 20);

        normal_pic = BitmapFactory.decodeResource(getResources(), R.drawable.ic_normal);
        target_pic = BitmapFactory.decodeResource(getResources(), R.drawable.ic_target);
        passed_pic = BitmapFactory.decodeResource(getResources(), R.drawable.ic_passed);

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.StepView, defStyleAttr, 0);
        dotCount = a.getInt(R.styleable.StepView_count, defaultDotCount);
        if (dotCount < 2) {
            throw new IllegalArgumentException("Steps can't be less than 2");
        }
        stepNum = a.getInt(R.styleable.StepView_step, defaultStepNum);
        lineLength = a.getInt(R.styleable.StepView_line_length, defaultLineLength);
        maxDotCount = a.getInt(R.styleable.StepView_max_dot_count, defaultMaxDotCount);
        if (maxDotCount < dotCount) {//当最多点小于设置点数量时,设置线条长度可变
            lineLength = defaultLineLength;
        }
        textLocation = a.getInt(R.styleable.StepView_text_location, defaultTextLocation);
        isTextBelowLine = textLocation == defaultTextLocation;

        normalLineColor = a.getColor(R.styleable.StepView_normal_line_color, defaultNormalLineColor);
        passLineColor = a.getColor(R.styleable.StepView_passed_line_color, defaultPassLineColor);
        lineStikeWidth = a.getDimension(R.styleable.StepView_line_stroke_width, defaultLineStikeWidth);
        textColor = a.getColor(R.styleable.StepView_text_color, defaultTextColor);
        textSize = a.getDimension(R.styleable.StepView_text_size, defaultTextSize);
        text2LineMargin = a.getDimension(R.styleable.StepView_text_to_line_margin, defaultText2DotMargin);
        margin = (int) a.getDimension(R.styleable.StepView_margin, defalutMargin);
        line2TopMargin = a.getDimension(R.styleable.StepView_line_to_top_margin, defaultLine2TopMargin);
        text2BottomMargin = a.getDimension(R.styleable.StepView_text_to_bottom_margin, defaultText2BottomMargin);
        clickable = a.getBoolean(R.styleable.StepView_is_view_clickable, defaultViewClickable);
        a.recycle();
        //当文字在线条上面时,参数倒置
        if (!isTextBelowLine) {
            line2BottomMargin = line2TopMargin;
            text2TopMargin = text2BottomMargin;
        }
        //线条画笔
        linePaint = new Paint();
        linePaint.setAntiAlias(true);
        linePaint.setStrokeWidth(lineStikeWidth);
        //文字画笔
        textPaint = new Paint();
        textPaint.setAntiAlias(true);
        textPaint.setColor(textColor);
        textPaint.setTextSize(textSize);
        //存放说明文字的矩形
        bounds = new Rect();
    }

由这段代码可知,通过init方法,不但初始化了上面的细节参数,还额外初始化了bitmap、paint、bounds等参数。由于调用了dp/sp2px方法,所以先贴一下该方法。

private int dp2px(Context context, int value) {
        float density = context.getResources().getDisplayMetrics().density;
        return (int) (density * value + 0.5f);
    }

    private int sp2px(Context context, int value) {
        float scaledDensity = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (value / scaledDensity + 0.5f);
    }
3.4在onSizeChanged方法中,完成宽高等数据计算。
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        width = w - margin * 2;
        height = h;
        //线条长度是否可变
        if (lineLength == defaultLineLength) {//可变
            perLineLength = width / (dotCount - 1);
        } else {//固定
            perLineLength = width / (maxDotCount - 1);
        }
        passWH = calculateWidthAndHeight(passed_pic);
        normalWH = calculateWidthAndHeight(normal_pic);
        targetWH = calculateWidthAndHeight(target_pic);
    }

此处说明一下,计算bitma宽高的方法,然后把宽高信息存于二维数组中

    /*计算bitmap宽高*/
    private int[] calculateWidthAndHeight(Bitmap bitmap) {
        int[] wh = new int[2];
        int width = bitmap.getWidth();
        int height = bitmap.getHeight();
        wh[0] = width;
        wh[1] = height;
        return wh;
    }
3.5通过onDraw方法绘制View
3.5.1画步骤点之间连线
    /*绘制链接步骤点之间的线条*/
    private void drawConnectLine(Canvas canvas, int stepNum) {
        float startX = 0;
        float stopX = 0;
        for (int i = 0; i < dotCount - 1; i++) {
            /*设置线条起点X轴坐标*/
            if (i == stepNum) {
                startX = margin + perLineLength * i + targetWH[0] / 2;
            } else if (i > stepNum) {
                startX = margin + perLineLength * i + normalWH[0] / 2;
            } else {
                startX = margin + perLineLength * i + passWH[0] / 2;
            }
            /*设置线条终点X轴坐标*/
            if (i + 1 == stepNum) {
                stopX = margin + perLineLength * (i + 1) - targetWH[0] / 2;
            } else if (i + 1 < stepNum) {
                stopX = margin + perLineLength * (i + 1) - passWH[0] / 2;
            } else {
                stopX = margin + perLineLength * (i + 1) - normalWH[0] / 2;
            }
            /*当目标步骤超过i时,线条设置为已过颜色,不超过时,设置为普通颜色*/
            if (stepNum > i) {
                linePaint.setColor(passLineColor);
            } else {
                linePaint.setColor(normalLineColor);
            }
            if (isTextBelowLine) {
                /*当文字在线条下方时,设置线条y轴的位置并绘制*/
                canvas.drawLine(startX, line2TopMargin, stopX, line2TopMargin, linePaint);
            } else {
                canvas.drawLine(startX, height - line2BottomMargin, stopX, height - line2BottomMargin, linePaint);
            }
        }
    }
3.5.2画普通步骤点
    /*绘制一般情况下的步骤点图片*/
    private void drawNormalSquar(Canvas canvas, int stepNum) {
        for (int i = 0; i < dotCount; i++) {
            /*在目标点状态时,普通图片不绘制,跳过,继续下一次循环*/
            if (stepNum == i) {
                continue;
            }
            if (stepNum > i) {
                float left = margin + perLineLength * i - passWH[0] / 2;
                float top = 0;
                if (isTextBelowLine) {
                    top = line2TopMargin - passWH[1] / 2;
                } else {
                    top = height - line2BottomMargin - passWH[1] / 2;
                }
                canvas.drawBitmap(passed_pic, left, top, null);
            } else {
                float left = margin + perLineLength * i - normalWH[0] / 2;
                float top = 0;
                if (isTextBelowLine) {
                    top = line2TopMargin - normalWH[1] / 2;
                } else {
                    top = height - line2BottomMargin - normalWH[1] / 2;
                }
                canvas.drawBitmap(normal_pic, left, top, null);
            }
        }
    }
3.5.3画目标步骤点
    /*绘制目标步骤图片*/
    private void drawTargetSquar(Canvas canvas, int i) {
        float left = margin + perLineLength * i - targetWH[0] / 2;
        float top = 0;
        if (isTextBelowLine) {
            top = line2TopMargin - targetWH[1] / 2;
        } else {
            top = height - line2BottomMargin - targetWH[1] / 2;
        }
        canvas.drawBitmap(target_pic, left, top, null);
    }
3.5.4画步骤点说明文字
    /*绘制各步骤说明文字*/
    private void drawDescText(Canvas canvas) {
        for (int i = 0; i < dotCount; i++) {
            String text = texts[i];
            textPaint.getTextBounds(text, 0, text.length(), bounds);
            int textWidth = bounds.width();
            int textHeight = bounds.height();
            float x = margin + perLineLength * i - textWidth / 2;
            float y;
            if (isTextBelowLine) {
                y = height - text2BottomMargin;
            } else {
                y = text2TopMargin + textHeight;
            }
            canvas.drawText(text, x, y, textPaint);
        }
    }

通过上面这几个步骤就完成StepView的绘制了。

3.6根据用户设置的是否可点击,给StepView添加点击监听

这里先说明一下思路:当用户点击时,View通过touch事件监听用户点击的x/y值,然后转换为point,再通过判断point是否在几个步骤点区域范围内,返回对应的步骤点值,然后重新绘制View。

3.6.1下面看onTouchEvent方法:
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (clickable) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    Point point = new Point();
                    point.x = (int) event.getX();
                    point.y = (int) event.getY();
                    int stepInDots = getStepInDots(point);
                    if (stepInDots != -1) {
                        stepNum = stepInDots;
                        invalidate();
                    }
                    break;
                default:
                    break;
            }
        }
        return super.onTouchEvent(event);
    }
3.6.2获取用户点击点在某个步骤点值:
    /*获取手指触摸点为第几个步骤点,异常时返回-1*/
    private int getStepInDots(Point point) {
        for (int i = 0; i < dotCount; i++) {
            Rect rect = getSetpSquarRects()[i];
            int x = point.x;
            int y = point.y;
            if (rect.contains(x, y)) {
                return i;
            }
        }
        return -1;
    }
3.6.3获取各步骤点矩阵所在区域:
    /*获取所有步骤点的矩阵区域*/
    private Rect[] getSetpSquarRects() {
        Rect[] rects = new Rect[dotCount];
        int left, top, right, bottom;
        for (int i = 0; i < dotCount; i++) {
            /*此处默认所有点的区域范围为被选中图片的区域范围*/
            Rect rect = new Rect();
            left = margin + perLineLength * i - targetWH[0] / 2;
            right = margin + perLineLength * i + targetWH[0] / 2;
            if (isTextBelowLine) {
                top = (int) (line2TopMargin - targetWH[1] / 2);
                bottom = (int) (line2TopMargin + targetWH[1] / 2);
            } else {
                top = (int) (height - line2BottomMargin - targetWH[1] / 2);
                bottom = (int) (height - line2BottomMargin + targetWH[1] / 2);
            }
            rect.set(left, top, right, bottom);
            rects[i] = rect;
        }
        return rects;
    }

至此,StepView的点击事件也完成了。

3.7设置外部调用接口
     /*给外部调用接口,设置步骤总数*/
    public void setDotCount(int count) {
        if (count < 2) {
            throw new IllegalArgumentException("dot count can't be less than 2.");
        }
        dotCount = count;
    }

    /*给外部调用接口,设置说明文字信息*/
    public void setDescription(String[] descs) {
        if (descs == null || descs.length < dotCount) {
            throw new IllegalArgumentException("Descriptions can't be null or its length must maore than dot count");
        }
        texts = descs;
    }

    /*给外部调用接口,设置该view是否可点击*/
    public void setClickable(boolean clickable) {
        this.clickable = clickable;
    }

    /*给外部调用接口,设置步骤*/
    public void setStep(Step step) {
        switch (step) {
            case ONE:
                stepNum = 0;
                break;
            case TWO:
                stepNum = 1;
                break;
            case THREE:
                stepNum = 2;
                break;
            case FOUR:
                stepNum = 3;
                break;
            case FIVE:
                stepNum = 4;
                break;
            default:
                break;
        }
        invalidate();
    }
    /*此处默认最多为5个步骤*/
    public enum Step {
        ONE, TWO, THREE, FOUR, FIVE
    }

通过设置这几个接口,可以很方便的动态设置StepView。

4.部分细节详解

  • 详解1
    画线条时,由于目标步骤点比普通的大,因此在计算线条长度时应计算目标步骤点两端线条长度,避免线条长度画错,影响美观。
            /*设置线条起点X轴坐标*/
            if (i == stepNum) {
                startX = margin + perLineLength * i + targetWH[0] / 2;
            } else if (i > stepNum) {
                startX = margin + perLineLength * i + normalWH[0] / 2;
            } else {
                startX = margin + perLineLength * i + passWH[0] / 2;
            }
            /*设置线条终点X轴坐标*/
            if (i + 1 == stepNum) {
                stopX = margin + perLineLength * (i + 1) - targetWH[0] / 2;
            } else if (i + 1 < stepNum) {
                stopX = margin + perLineLength * (i + 1) - passWH[0] / 2;
            } else {
                stopX = margin + perLineLength * (i + 1) - normalWH[0] / 2;
            }
  • 详解2
    线条长度是否可变(见git view1/view2/view3/view4/view5),当设置线条长度固定时,线条的长度由view_width/(max_dot-1)决定;当设置线条长度不固定时(view6),由图可知,view6的长度与view5完整的长度一致。
  • 详解3
    文字是否在线条下面,默认是。当文字在线条上面的时候,这里采取数据倒置设置,即把设置给view之前的线条距顶部、文字距底部的距离分别设置给了线条距底部、文字距顶部的距离。见如下代码:
        //当文字在线条上面时,参数倒置
        if (!isTextBelowLine) {
            line2BottomMargin = line2TopMargin;
            text2TopMargin = text2BottomMargin;
        }
  • 详解4
    获取各步骤点的矩形区域,首先是分别对各步骤点区域的左上右下进行计算,然后设置给各步骤点矩形。
    /*获取所有步骤点的矩阵区域*/
    private Rect[] getSetpSquarRects() {
        Rect[] rects = new Rect[dotCount];
        int left, top, right, bottom;
        for (int i = 0; i < dotCount; i++) {
            /*此处默认所有点的区域范围为被选中图片的区域范围*/
            Rect rect = new Rect();
            left = margin + perLineLength * i - targetWH[0] / 2;
            right = margin + perLineLength * i + targetWH[0] / 2;
            if (isTextBelowLine) {
                top = (int) (line2TopMargin - targetWH[1] / 2);
                bottom = (int) (line2TopMargin + targetWH[1] / 2);
            } else {
                top = (int) (height - line2BottomMargin - targetWH[1] / 2);
                bottom = (int) (height - line2BottomMargin + targetWH[1] / 2);
            }
            rect.set(left, top, right, bottom);
            rects[i] = rect;
        }
        return rects;
    }

5.调用

  • xml调用

  • 代码调用
    private StepView step1, step2, step3, step4, step5, step6;
    private CheckBox click1, click2, click3, click4, click5, click6;
    private String[] texts = {"确认身份信息", "确认入住信息", "选择房型", "支付押金", "完成入住"};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        step1 = (StepView) findViewById(R.id.step1);
        step2 = (StepView) findViewById(R.id.step2);
        step3 = (StepView) findViewById(R.id.step3);
        step4 = (StepView) findViewById(R.id.step4);
        step5 = (StepView) findViewById(R.id.step5);
        step6 = (StepView) findViewById(R.id.step6);

        click1 = (CheckBox) findViewById(R.id.click1);
        click2 = (CheckBox) findViewById(R.id.click2);
        click3 = (CheckBox) findViewById(R.id.click3);
        click4 = (CheckBox) findViewById(R.id.click4);
        click5 = (CheckBox) findViewById(R.id.click5);
        click6 = (CheckBox) findViewById(R.id.click6);


        step1.setDescription(texts);
        step2.setDescription(texts);
        step3.setDescription(texts);
        step4.setDescription(texts);
        step5.setDescription(texts);
        step6.setDescription(texts);

        step1.setStep(StepView.Step.ONE);
        step2.setStep(StepView.Step.TWO);
        step3.setStep(StepView.Step.THREE);
        step4.setStep(StepView.Step.FOUR);
        step5.setStep(StepView.Step.FIVE);
        step6.setStep(StepView.Step.FOUR);

        checkChaged(click1, step1);
        checkChaged(click2, step2);
        checkChaged(click3, step3);
        checkChaged(click4, step4);
        checkChaged(click5, step5);
        checkChaged(click6, step6);
    }

    private void checkChaged(CheckBox check, final StepView step) {
        check.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                step.setClickable(isChecked);
            }
        });
    }

6.代码托管地址

StepView

你可能感兴趣的:(自定义View之StepView(流程/步骤View))