我开通微信公众号啦,如果大家喜欢我的文章,欢迎大家关注我的微信号,我会定期为大家推送Android中的热门知识。
今天为大家介绍另一个自定义View——进度指示器,这个在电商App和支付宝等中经常遇到。如在电商App中买一个东西会有如下步骤:
下订单——>支付完成——>已发货——>交易完成
先使用我们的自定义View来展示一下上面的步骤吧
如上图所示,步骤未完成时是灰色(可指定),当步骤完成时显示成绿色(可指定),并且最后一个完成的任务有光晕效果。
同样,我们看看这个自定义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进行绘制,分为三个步骤:
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.
好了,步骤指示器的代码逻辑介绍完了,如果有什么问题欢迎留言讨论…
代码下载地址