Android自定义StepView指示器

按照规矩先上图

问:为什么写这个自定义View呢?
答:工作让我成长。
Android自定义StepView指示器_第1张图片

Android关于自定义View的文章不再新鲜,实现方式也是各有千秋,在这里自己写的一个进度指示器实现方式是通过继承自View实现,内部所有的圆圈、线条和文字都是直接绘制,没有以ViewGroupaddView()方式去做。


1.理清思路

以最终效果图为参考,我们要分4步走

  1. 绘制空心的圆圈
  2. 绘制实心的圆
  3. 绘制圆圈之间的线条
  4. 绘制每一个步骤的说明文字

2.准备工作

根据需求来,哪些东西是可以用户自定义的,是通过属性控制还是暴露setter出去自己决定。这里我准备的attribute如下,用户可自定义的就是圆圈大小和色值等

xml




























然后是在View中定义的一些变量


private Paint mStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG);//空心圆圈画笔
private Paint mCenterPaint = new Paint(Paint.ANTI_ALIAS_FLAG);//实心圆圈画笔
private Paint mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);//文字画笔
private Paint mLinePaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);//线条画笔
private RectF mLineRect = new RectF();//线条
private Rect mTextRect = new Rect();//进度文字
private float mGap;//线条与圆圈之间的间隔
private float mMargin;//文字与圆圈垂直方向的间距
private float mStepTextSize;//进度文字大小
private float mLineHeight;//线条高度
private float mStrokeWidth;//外圈的宽度
private float mCenterRadius;//内圈半径
private float mStrokeRadius;//外圈半径
private float mCircleHeight;//绘制圆圈的高度
private float mTextY;//绘制文案的Y值
private int mMode;//文字相对于圆圈的位置,上下
private int mProgress;
private int mMaxCount;
private int mDefaultColor;
private int mReachedColor;
private int mTextColor;
private int mLineColor;
private int mWidth;
private int mHeight;

3.开始写代码

初始化的自定义View代码是需要套公式的,在构造中通过TypedArray获取View的各种属性值、为一些属性提供默认值、定义好属性变量、根据View的需要提供默认的宽高等,然后继续。

3.1重写onMeasure()

onMeasure方法中一定要为你的View添加好宽高限制,关键点是View的测量要避免在MeasureSpec.AT_MOSTMeasureSpec.UNSPECIFIED的情况下获取不到想要的效果,最好提供给一个默认值。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //measure()方法为自己定义的测量方法
        setMeasuredDimension(measure(widthMeasureSpec, true), measure(heightMeasureSpec, false));
    }

    private int measure(int measureSpec, boolean isWidth) {
        int result;
        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);
        int padding = isWidth ? getPaddingLeft() + getPaddingRight() : getPaddingTop() + getPaddingBottom();
        if (mode == MeasureSpec.EXACTLY) {
            result = size;
        } else {
            result = isWidth ? getSuggestedMinimumWidth() : getSuggestedMinimumHeight();
            result += padding;
            if (mode == MeasureSpec.AT_MOST) {
                if (isWidth) {
                    result = Math.max(result, size);
                } else {
                    result = Math.min(result, size);
                }
            }
        }
        return result;
    }

这里给View提供的最小宽高分别是一个圆圈的宽和一个200的高。

    @Override
    protected int getSuggestedMinimumWidth() {
        return (int) (mStrokeRadius * 2);
    }
    @Override
    protected int getSuggestedMinimumHeight() {
        return DEFAULT_HEIGHT;
    }
3.2onDraw()绘制

到现在为止我们已经控制好View的宽高了,其实也可以先绘制一番看看效果了,接着在onDraw里面根据最大的mMaxCount和mProgress绘制圆圈。

  • (这里View绘制的效果是居中显示的)首先考虑绘制最外层的空心圆圈,如果mMaxCount为1的时候圆圈的位置就是屏幕的中心点了,如果mMaxCount为2的时候呢?我的想法是把屏幕可用的宽度(除去padding)2等分,两个圆点之间的宽度权重占一,两点距离左右的边距各自权重为0.5,以此类推。
  • 绘制实心圆圈就是和空心圆的位置完全一样,只需要传入不同的画笔和半径就OK了。
  • 最后绘制圆圈之间的连接线,两个圆圈之间的间隔g的长度是可以计算出来的,然后在这个基础上减去圆圈的半径就OK了,当然友好一点的做法还是要通过一个属性gap去控制线条与圆圈之间的margin值。
  • 为了让线条的高度可以自定义,这里的线条就通过Rect去绘制了。
        int height = getHeight() - getPaddingBottom() - getPaddingTop();
        int usefulWidth = mWidth - getPaddingLeft() - getPaddingRight();//可用宽度
        float g = (usefulWidth / (mMaxCount));
        //起始点
        float startX = getPaddingLeft() + mStrokeRadius;
        startX += g / 2.0f;//这里绘制不能超出paddingRight
        for (int iLoop = 0; iLoop < mMaxCount; iLoop++) {
            float drawX = startX + (iLoop * g);
            //已完成进度的色值
            mStrokePaint.setColor(iLoop <= mProgress - 1 ? mReachedColor : mDefaultColor);
            canvas.drawCircle(drawX, height / 2, mStrokeRadius, mStrokePaint);
            //绘制进度位置
            if (iLoop <= mProgress - 1) {
                canvas.drawCircle(drawX, height / 2, mCenterRadius, mCenterPaint);
            }
        }
        //绘制连接线
        if (mDrawLine) {//mMaxCount大于1的时候就绘制连接线
            int lineCount = mMaxCount - 1;
            for (int lLoop = 0; lLoop < lineCount; lLoop++) {
                float drawXStart = startX + mStrokeRadius + dp2px(mGap) + (lLoop * g);
                float drawXEnd = startX + g - mStrokeRadius - dp2px(mGap) + (lLoop * g);
                mLineRect.left = drawXStart;
                mLineRect.right = drawXEnd;
                canvas.drawRect(mLineRect, mLinePaint);
            }
        }

看下效果,初步就长这个样子,当然具体的各个值是要经过计算和调试的。
Android自定义StepView指示器_第2张图片
接下来就要绘制文字了,文字的绘制位置需要谨慎操作,每一个步骤圆心的位置也就是你每一步文案说明的X轴中心,而Y值需要另行计算,以圆圈在文案上方为例子,圆圈的最底部和文案的最上方应该是Y值相等的,但是首先我们需要保证的是圆圈和文字的绘制位置Y轴一致,然后再去做加减法控制文字的位移,这里控制文字的Y轴是不能通过简单的textSize控制的,关于Paintdecent等方法文末附上链接,于是有了下面这段代码控制绘制文案的Y轴。


    private void calcSize() {
        int usefulHeight = mHeight - getPaddingBottom() - getPaddingTop();
        if (checkSteps()) {
            mDrawText = true;
            mTextPaint.getTextBounds(String.valueOf(mSteps.get(0)), 0, String.valueOf(mSteps.get(0)).length(), mTextRect);
            float center = (mTextPaint.ascent() + mTextPaint.descent()) / 2.0f;
            if (mMode == MODE_BOTTOM) {
                mCircleHeight = usefulHeight / 2.0f - mTextSize;
                mTextY = mCircleHeight - center;
            } else {
                mCircleHeight = usefulHeight / 2.0f + mTextSize;
                mTextY = mCircleHeight - center;
            }
        } else {
            mDrawText = false;
            mCircleHeight = usefulHeight / 2.0f;
        }
        ......
    }

Android自定义StepView指示器_第3张图片

OK,现在文字和圆圈的Y轴一致了,然后我们根据mode来控制文字上下方的位移,如果文字在圆圈上方,那么mTextY的值就需要在原来的基础上减去圆圈半径mStrokeRadius,然后再减去mTextSize的1/2才能正好使得文字的最下方和圆圈的正上方是同一个Y轴。看代码。

        ......
        if (checkSteps()) {
            mDrawText = true;
            mTextPaint.getTextBounds(String.valueOf(mSteps.get(0)), 0, String.valueOf(mSteps.get(0)).length(), mTextRect);//这里假设所有的文案字体大小是一样的,忽略SpannableString带来的影响,严谨一点的做法可以在onDraw()里自行测量
            float center = (mTextPaint.ascent() + mTextPaint.descent()) / 2.0f;
            if (mMode == MODE_BOTTOM) {
                mCircleHeight = usefulHeight / 2.0f - mTextSize;
                mTextY = mCircleHeight - center + mStrokeRadius + mTextSize / 2.0f + dp2px(mMargin);
            } else {
                mCircleHeight = usefulHeight / 2.0f + mTextSize;
                mTextY = mCircleHeight - center - mStrokeRadius - mTextSize / 2.0f - dp2px(mMargin);
            }
        }

然后再来看一下效果
Android自定义StepView指示器_第4张图片

貌似文案的绘制也OK了,接下来就需要修饰一番了。
最后,我加了textMargin属性控制文案和圆圈之间的间隔,indicator_mode属性来控制文案和圆圈的相对位置.最终的结果就是效果图了,代码链接附上。

Android FontMetrics

你可能感兴趣的:(一,Android)