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

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

写在前面的话

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

1.效果图

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


stepview.gif

2.实现思路

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

3.实现细节

3.1概述

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

3.2首先通过res/values/attrs定义一些细节参数
<declare-styleable name="StepView">
        <!--步骤点个数-->
        <attr name="count" format="integer" />
        <!--链接步骤点之间的线条颜色以及线宽-->
        <attr name="normal_line_color" format="color" />
        <attr name="passed_line_color" format="color" />
        <attr name="line_stroke_width" format="dimension" />
        <!--文字说明信息-->
        <attr name="text_color" format="color" />
        <attr name="text_size" format="dimension" />
        <attr name="text_to_line_margin" format="dimension" />
        <!--边距-->
        <attr name="margin" format="dimension" />
        <!--默认文字在线条下面,线条距顶部距离、文字距底部距离-->
        <attr name="line_to_top_margin" format="dimension" />
        <attr name="text_to_bottom_margin" format="dimension" />
        <!--设置步骤点在哪儿-->
        <attr name="step" format="enum">
            <enum name="one" value="0" />
            <enum name="two" value="1" />
            <enum name="three" value="2" />
            <enum name="four" value="3" />
            <enum name="five" value="4" />
        </attr>
        <!--线条长度是否可变,默认是-->
        <attr name="line_length" format="enum">
            <enum name="variable_length" value="0" />
            <enum name="fixed_length" value="1" />
        </attr>
        <!--根据最大步骤点数量,计算出线条长度不变时线条长度,线条长度可变时,该数据无效-->
        <attr name="max_dot_count" format="integer" />
        <!--文字是否在线条下面,默认是-->
        <attr name="text_location" format="enum">
            <enum name="down" value="0" />
            <enum name="up" value="1" />
        </attr>
        <!--此view是否可点击-->
        <attr name="is_view_clickable" format="boolean" />
    </declare-styleable>
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调用
<com.qb.code.yidian.StepView
      android:id="@+id/step1"
      android:layout_width="match_parent"
      android:layout_height="50dp"
      android:background="#21201D"
      app:count="2"
      app:line_length="fixed_length"
      app:line_stroke_width="1dp"
      app:line_to_top_margin="18dp"
      app:margin="90dp"
      app:max_dot_count="5"
      app:step="one"
      app:text_size="12sp"
      app:text_to_bottom_margin="8dp" />
  • 代码调用
    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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,755评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,305评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,138评论 0 355
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,791评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,794评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,631评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,362评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,264评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,724评论 1 315
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,900评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,040评论 1 350
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,742评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,364评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,944评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,060评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,247评论 3 371
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,979评论 2 355