[置顶] Android自定义View系列之进度指示控件

我开通微信公众号啦,如果大家喜欢我的文章,欢迎大家关注我的微信号,我会定期为大家推送Android中的热门知识。
[置顶] Android自定义View系列之进度指示控件_第1张图片

今天为大家介绍另一个自定义View——进度指示器,这个在电商App和支付宝等中经常遇到。如在电商App中买一个东西会有如下步骤:
下订单——>支付完成——>已发货——>交易完成
先使用我们的自定义View来展示一下上面的步骤吧
[置顶] Android自定义View系列之进度指示控件_第2张图片

如上图所示,步骤未完成时是灰色(可指定),当步骤完成时显示成绿色(可指定),并且最后一个完成的任务有光晕效果。
同样,我们看看这个自定义View是如何实现的吧。
由于需要自定义 “完成颜色”和“未完成颜色”等属性,所以我们需要定义如下属性:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="StepView">
        <attr name="lineheight" format="dimension"></attr>
        <attr name="smallradius" format="dimension"></attr>
        <attr name="largeradius" format="dimension"></attr>
        <attr name="undonecolor" format="color"></attr>
        <attr name="donecolor" format="color"></attr>
        <attr name="totalstep" format="integer"></attr>
        <attr name="completestep" format="integer"></attr>
    </declare-styleable>
</resources>

这些属性的意义如下:
lineheight :表示指示器中线条的高度
smallradius:表示指示器中圆点的半径
undonecolor:表示没有完成的步骤的颜色
donecolor:表示已经完成步骤的颜色
totalstep:表示总步骤数
completestep:表示已经完成的步骤

如果你还需要自定义其他属性,也可以在这里自行添加。
接下来我们看看代码的实现吧,我们分两部分实现:
1、StepBar部分,这部分主要用来显示进度条部分
2、SetpView部分,这部分主要依赖StepBar完成titile显示

在Android目前存在的View中,没有具备类似功能的View,所以我们不能通过继承某个View来实现,而是需要通过继承普通的View,并完成View绘制的三部曲(measure,layout,draw)。
首先看第一部曲:

    /** * View的绘制的第一阶段调用 * @param widthMeasureSpec * @param heightMeasureSpec */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width=getDefaultWidth();
        if(MeasureSpec.UNSPECIFIED!=MeasureSpec.getMode(widthMeasureSpec)){
            width=MeasureSpec.getSize(widthMeasureSpec);
        }

        int height=120;
        if(MeasureSpec.UNSPECIFIED!=MeasureSpec.getMode(heightMeasureSpec)){
            height=MeasureSpec.getSize(heightMeasureSpec);
        }
        Log.d(TAG, "onMeasure-->width:" + width + " height:" + height);
        setMeasuredDimension(width, height);

    }

onMeasure 方法中,首先判断MeasureSpec 中的mode部分是否为MeasureSpec.UNSPECIFIED,如果是的,则宽度通过getDefaultWidth() 方法获取,高度为120,如果不是,那么拿到widthMeasureSpec/heightMeasureSpec中的size部分。如果你对于为什么这样做的原因不清楚,欢迎你阅读我的关于View绘制相关文章:
Android中View的绘制机制源码分析一
Android中View的绘制机制源码分析二
Android中View的绘制机制源码分析三
Android中View的绘制机制源码分析四

第二部曲:

    /* * 在View的绘制的第二阶段(layout)中,当尺寸发生变化时调用 * 注意:第二阶段本来是调用onLayout方法,此方法是在onLayout方法中被调用 * @param w * @param h * @param oldw * @param oldh */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //计算位置
        mCenterY=this.getHeight()/2;
        mLeftX=this.getLeft()+getPaddingLeft();
        mLeftY=mCenterY-mLineHeight/2;
        mRightX=this.getRight()-getPaddingRight();
        mRightY=mCenterY+mLineHeight/2;
        Log.d(TAG,"onSizeChanged->mLeftX:"+mLeftX);
        Log.d(TAG, "onSizeChanged->mRightX:" + mRightX);
        if(mTotalStep>1){
            mDistance=(mRightX-mLeftX)/(mTotalStep-1);
            Log.d(TAG,"onSizeChanged->mDistance:"+mDistance);
        }
    }

本来第二部应该改写的时onDraw() 方法的,但这里改写的却是onSizeChange() 方法,由于这里我只关注View的大小改变,并且onSizeChange() 方法是在 onDraw() 中被调用(只有尺寸发生变化才会调用),在onSizeChange() 方法中我们计算了自定义View的纵向中心位置,左边坐标,右边左边,小圆点之间的距离等等。在这里可以思考一下,为啥要在这里计算尺寸相关变量?这个问题我已经在View的绘制机制相关文章做出了解答。

下面看看最重要的第三部曲:


    /** * View的绘制的第三阶段调用 * @param canvas */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if(mTotalStep<=0 || mCompleteStep<0 || mCompleteStep>mTotalStep){
            return;
        }
        Paint mCirclePaint=new Paint();
        mCirclePaint.setAntiAlias(true);
        mCirclePaint.setStyle(Paint.Style.FILL);
        mCirclePaint.setColor(mUnDoneColor);

        canvas.drawRect(mLeftX,mLeftY,mRightX,mRightY,mCirclePaint);
        float xLoc=mLeftX;
        //画所有的步骤(圆形)
        for(int i=0;i<mTotalStep;i++){
            canvas.drawCircle(xLoc, mLeftY, mSmallRadius, mCirclePaint);
            xLoc=xLoc+mDistance;
        }

        //画已经完成的步骤(圆形加矩形)
        xLoc=mLeftX;
        mCirclePaint.setColor(mDoneColor);
        for(int i=0;i<mCompleteStep;i++){
            if(i>0){
                canvas.drawRect(xLoc-mDistance,mLeftY,xLoc,mRightY,mCirclePaint);
            }
            canvas.drawCircle(xLoc, mLeftY, mSmallRadius, mCirclePaint);


            //画当前步骤(加光晕效果)
            if(i==mCompleteStep-1){
                mCirclePaint.setColor(getTranspartColorByAlpha(mDoneColor,0.2f));
                canvas.drawCircle(xLoc, mLeftY, mLargeRadius, mCirclePaint);
            }else {
                xLoc=xLoc+mDistance;
            }

        }
    }

onDraw() 方法中,主要是对UI进行绘制,分为三个步骤:

  1. 绘制横线
  2. 根据总步骤的个数,绘制小圆点
  3. 绘制已经完成步骤的小圆点(颜色和未完成的步骤不同),并未最后一个完成步骤添加光晕效果

StepBar的主要逻辑已经介绍完了,下面介绍一下SetpView的逻辑,在使用的时候,我们只会使用SetpView,SetpView是对SetpBar的一个封装,并提供了title的属性。

SetpView的UI布局:

<?xml version="1.0" encoding="utf-8"?>
<merge  xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" >
    <FrameLayout  android:id="@+id/step_title" android:layout_gravity="left" android:layout_width="match_parent" android:layout_height="wrap_content" >
    </FrameLayout>
    <com.gavin.step.StepBar  android:id="@+id/step_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/step_title" android:paddingLeft="30dp" android:paddingRight="30dp" />
</merge>

这个布局很简单,相信不用我介绍,我们直接看SetpView的代码部分吧

public StepView(Context context) {
        super(context);
        init(context,null,0);
    }

    public StepView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs, 0);
    }

    public StepView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr);
    }
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public StepView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context,attrs,defStyleAttr);
    }

    private void init(Context mContext,AttributeSet attrs,int defStyleAttr){
        LayoutInflater.from(mContext).inflate(R.layout.step_view,this,true);
        mStepBar=(StepBar)this.findViewById(R.id.step_bar);
        mTitleGroup=(FrameLayout)this.findViewById(R.id.step_title);
        TypedArray array = mContext.obtainStyledAttributes(attrs,R.styleable.StepView,defStyleAttr,0);

        mStepBar.setLineHeight(array.getDimensionPixelOffset(R.styleable.StepView_lineheight, StepBar.DEFAULT_LINE_HEIGHT));
        mStepBar.setSmallRadius(array.getDimensionPixelOffset(R.styleable.StepView_smallradius, StepBar.DEFAULT_SMALL_CIRCLE_RADIUS));
        mStepBar.setLargeRadius(array.getDimensionPixelOffset(R.styleable.StepView_largeradius, StepBar.DEFAULT_LARGE_CIRCLE_RADIUS));
        mStepBar.setUnDoneColor(array.getColor(R.styleable.StepView_undonecolor, StepBar.COLOR_BAR_UNDONE));
        mStepBar.setDoneColor(array.getColor(R.styleable.StepView_undonecolor, StepBar.COLOR_BAR_DONE));
        mStepBar.setTotalStep(array.getInteger(R.styleable.StepView_totalstep, 0));
        mStepBar.setCompleteStep(array.getInteger(R.styleable.StepView_completestep, 0));

        //在StepBar布局完成之后开始添加title
        mStepBar.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener(){

            @Override
            public void onGlobalLayout() {
                initStepTitle();
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
                    mStepBar.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                } else {
                    mStepBar.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                }
            }
        });
        array.recycle();
    }

这里我们主要看看init() 方法,这个方法是在构造函数中被调用,主要用来解析自定义属性的,关于自定义属性的逻辑我不在多说,这里主要看为SetpBar添加title的逻辑,我并没有直接在init() 方法中调用添加title的逻辑,而是等SetpBar第二部曲完成之后才添加title的逻辑。为什么呢?先看看添加title的逻辑再来解答此问题吧

private void initStepTitle(){
        if(mStepTitles==null){
            return;
        }
        mTitleGroup.removeAllViews();

        if(mStepTitles.size()!=mStepBar.getTotalStep()){
            throw new IllegalStateException("设置的Title的个数和步骤数不一致!");
        }
        int stepNum=mStepBar.getTotalStep();
        for(int i=1;i<=stepNum;i++){
            final float stepPos=mStepBar.getPositionByStep(i);
            final TextView title=new TextView(this.getContext());
            title.setText(mStepTitles.get(i - 1));
            title.setSingleLine();
            title.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    title.setTranslationX(stepPos - title.getMeasuredWidth() / 2);
                    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
                        title.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                    } else {
                        title.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                    }
                }
            });
            FrameLayout.LayoutParams lp=new FrameLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,LinearLayout.LayoutParams.WRAP_CONTENT);
            mTitleGroup.addView(title,lp);
        }
    }

由于在添加title的时候,需要用到小圆点的位置,这个位置只有SetpBar的第二部曲完成之后才可以确定,这就是为什么在刚才需要在SetpBar第二部曲完成之后才能添加title.

好了,步骤指示器的代码逻辑介绍完了,如果有什么问题欢迎留言讨论…
代码下载地址

你可能感兴趣的:([置顶] Android自定义View系列之进度指示控件)