Android StepView

本文将会完成:

1.自定义控件
2.MVP模式、模板模式
3.接口扩展
4.EventBus解耦
5.一句话实例化StepView

最近项目需要实现一个功能,类似于网上某宝购物网站的订单跟踪流程,下单-->送货-->签收等等,我们先看下本文要实现的demo。下单界面点击下一步流程会走到送货界面,再次点击下一步会到签收界面。状态分成完成和未完成。
完成的是下单,未完成的是送货和签收过程


Android StepView_第1张图片
1.png

完成的是下单和送货,未完成的是签收


Android StepView_第2张图片
2.png

完成的是下单、 送货和签收状态
Android StepView_第3张图片
3.png
new StepView.Builder().setTextIndicator(mTextIndicator)//文字列表
                .setCompleteTextColor(ContextCompat.getColor(mStepCompleteView.getStepView().getContext(), R.color.complete_text_color))//完成流程文字的颜色
                .setUncompleteTextColor(ContextCompat.getColor(mStepCompleteView.getStepView().getContext(), R.color.uncomplete_text_color))//未完成流程文字的颜色
                .setCompleteDrawableResIdList(mCompleteDrawableResIdList)//完成流程的背景图片集合
                .setUncompleteDrawableResIdList(mUncompleteDrawableResIdList)//未完成流程的背景图片集合
                .setCurrrentPos(mCurrentPos) //更新当前位置
                .build(mStepCompleteView.getStepView());

整个控件主要分成两个部分,上面图形部分StepViewIndicator,下面是一个RelativeLayout,用于动态添加TextView。下面我们先看下
StepViewIndicator这个控件的实现过程。

一、StepViewIndicator

可以看见要实现这个控件主要分成下面几个步骤的工作:
1.计算控件的尺寸,包括大圆和小圆的尺寸;
2.画大圆,包括完成和未完成的圆,可以动态设置背景图片;
3.画小圆,跟大圆类似,也包括完成和未完成两种状态,也可以设置背景图片;

下面我们分别看下这几个步骤的实现:

1.计算控件尺寸

在这里我们为了实现无论控件都有几个(比如四个、五个),我们的StepViewIndicator都能均匀分布,就需要动态计算控件的尺寸,我们可以在onSizeChanged中手动计算尺寸。

protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        mCenterY = getHeight() * 0.5f; //控件居中

        mBigCircleCenterPosList.clear();
        mSmallCircleCenterPosList.clear();

        for (int i = 0; i < mStepNums; i++) {
            float paddingLeft = (getWidth() - mBigCircleRadius * 2 - mPaddingCircle * (2 * mStepNums - 2)) / 2;

            float bigCircleCenterPos = paddingLeft + mBigCircleRadius + mPaddingCircle * 2 * i;
            mBigCircleCenterPosList.add(bigCircleCenterPos);
            mSmallCircleCenterPosList.add(bigCircleCenterPos + mPaddingCircle);
        }
    }

我们用两个List分别存放大圆和小圆的圆心位置。先计算控件的左边padding, mStepNums就是流程的步骤个数,本文中是3当然可以设置,mPaddingCircle就是相邻两圆的圆心距离,本文中就是大小相邻两圆的圆心距离。我们用中文解释公式:
边距=(控件宽度-圆心距离 * (所有圆心个数 - 1) - 大圆直径)
那么自然我们的左边距就是边距/2。应该很容易看懂,需要小学数学功底,逃:)

4.png
float paddingLeft = (getWidth() - mBigCircleRadius  2 - mPaddingCircle  (2  mStepNums - 2)) / 2;

有了左边距,就可以循环计算圆心的位置,比如第一个大圆的圆心位置:

pos0 = paddingLeft + mBigCircleRadius;

第二个大圆的圆心位置:

pos1 = paddingLeft + mBigCircleRadius + mPaddingCircle * 2(为什么是2,因为中间还有一个小圆喽);

第三个大圆的圆心位置:

pos2 = paddingLeft + mBigCircleRadius + mPaddingCircle *2*2;

归纳总结就是:

float bigCircleCenterPos = paddingLeft + mBigCircleRadius + mPaddingCircle * 2 * i;

有了大圆位置,就容易计算小圆位置了,这里就不再一一列举了。

2.画大圆
for (int i = 0; i < mBigCircleCenterPosList.size(); i++) {
        float bigCircleCenterPos = mBigCircleCenterPosList.get(i);
        Rect rect = new Rect((int) (bigCircleCenterPos - mBigCircleRadius), (int) (mCenterY - mBigCircleRadius),
                (int) (bigCircleCenterPos + mBigCircleRadius), (int)(mCenterY + mBigCircleRadius));

        StepBean stepBean = mStepBeanList.get(i);
        Drawable drawable = stepBean.getDrawable();

        drawable.setBounds(rect);
        drawable.draw(canvas);
}

为了限制背景图片在匹配圆,我们在外面放了个矩形,矩形尺寸有了圆心位置就轻而易举,比如
left = 圆心位置 - 圆半径
top = 圆心位置 + 圆半径
right = 圆心位置 + 圆半径
bottom = 圆心位置 - 圆半径
背景图片可以动态设置,这个请看后面。

3.画小圆

画小圆基本和画大圆逻辑类似,唯一不同的是我们这边考虑到小圆基本只有两种背景图片,完成和未完成的。因为我们在这边是在成员变量里放置了这两种图片的drawable,大圆这是可以设置List,动态获取背景图片。

//画小圆
for (int i = 0; i < mBigCircleCenterPosList.size() - 1; i++) {
        float smallCircleCenterPos = mSmallCircleCenterPosList.get(i);
        Rect rect = new Rect((int) (smallCircleCenterPos - mSamllCircleRadius), (int) (mCenterY - mSamllCircleRadius),
                    (int) (smallCircleCenterPos + mSamllCircleRadius), (int) (mCenterY + mSamllCircleRadius));

        if (i < mCompletedPos){
            mCompleteSmallCircleDrawable.setBounds(rect);
            mCompleteSmallCircleDrawable.draw(canvas);
        }else {
            mUncompleteSmallCircleDrawable.setBounds(rect);
            mUncompleteSmallCircleDrawable.draw(canvas);
        }
}

二、StepView

我们前面说过,StepView包括StepViewIndicator和下方的RelativeLayout. RelativeLayout 可以动态添加下方的文字。

Android StepView_第4张图片
5.png

这里当然是封装一下比较高大上了,我们把每一步的名字、状态和背景图片封装一个bean。

public class StepBean {

    public static final int STEP_COMPLETED = 0;//完成状态
    public static final int STEP_UNCOMPLETED = 1;//未完成状态

    private String name;
    private int state;
    private Drawable mDrawable;
}

我们的布局就比较简单:



    

    


为了更灵活的构建StepView,我们使用了Build模式,先看下Builder。
比较重要的属性就是文字颜色,文字列表(textIndicator),完成状态的resource id(completeDrawableResIdList),未完成状态的resource id(uncompleteDrawableResIdList),还有就是关键数据的list(stepBeanList)

public static class Builder {

        private int uncompleteTextColor;//未完成的文字颜色
        private int completeTextColor;//完成的文字颜色

        private int textSize;

        private List textIndicator;
        private List completeDrawableResIdList;
        private List uncompleteDrawableResIdList;
        private List stepBeanList;

        private int curPos;

那么我们BeanList中的bean怎么来的?主要就在下面的build中:
首先根据客户传进来的textIndicator赋值bean中的name属性;
根据传进来的CurrrentPos,赋值bean中state;
根据传进来的resource id,通过ContextCompat.getDrawable(stepView.getContext(), uncompleteDrawableResIdList.get(i))得到drawable,赋值bean中的drawable属性

public void build(StepView stepView) {
            stepBeanList.clear();
            String name;
            int state = StepBean.STEP_COMPLETED;
            Drawable drawable;
            StepBean stepBean;
            for (int i = 0; i < textIndicator.size(); i++) {
                name = textIndicator.get(i);
                drawable = ContextCompat.getDrawable(stepView.getContext(), completeDrawableResIdList.get(i));
                if (i > curPos) {
                    state = StepBean.STEP_UNCOMPLETED;
                    drawable = ContextCompat.getDrawable(stepView.getContext(), uncompleteDrawableResIdList.get(i));
                }
                stepBean = new StepBean(name, state, drawable);
                stepBeanList.add(stepBean);
            }

            stepView.setTextSize(textSize)
                    .setCompleteTextColor(completeTextColor)
                    .setUncompleteTextColor(uncompleteTextColor)
                    .setStepBeanList(stepBeanList);
        }

数据构造完成后通知StepViewIndicator进行绘制,几个重要属性我们这里就可以看到赋值过程,一个就是mStepNums,是根据文字列表的个数进行赋值;当前位置也是根据StepBean中状态进行判断赋值;最后就是requestLayout来通知StepView进行绘制

public StepView setStepBeanList(List stepBeanList) {
        mStepBeanList = stepBeanList;
        mStepViewIndicator.setStepBeanList(stepBeanList);
        return this;
}

public void setStepBeanList(List stepBeanList) {
        mStepBeanList = stepBeanList;
        mStepNums = stepBeanList.size();

        if (null != mStepBeanList && mStepNums > 0) {
            for (int i = 0; i < mStepNums; i++) {
                StepBean stepBean = mStepBeanList.get(i);
                if (stepBean.getState() == StepBean.STEP_COMPLETED) {
                    mCompletedPos = i;
                }
            }
        }
        requestLayout();
    }

那么问题来了,我们在通知StepViewIndicator进行绘制的时候,RelativeLayout怎么办?在StepViewIndicator中设置一个RelativeLayout引用,再一一通知?这就破坏了封装性,毕竟textView的绘制是自己的工作,不应该掺杂在StepViewIndicator中,因此我们考虑在StepViewIndicator中留一个接口,这样方便我们外面进行扩展

public interface onDrawIndicatorListener {
        void onDrawIndicator();

在StepView中实现这个接口,在其中动态添加TextView

@Override
public void onDrawIndicator() {
        if (null != mTextContainer) {
            mTextContainer.removeAllViews();

            List bigCircleCenterPosList = mStepViewIndicator.getBigCircleCenterPosList();
            int completedPos = mStepViewIndicator.getCompletedPos();
            if (null == bigCircleCenterPosList || null == mStepBeanList || !(bigCircleCenterPosList.size() > 0)) {
                return;
            }
            for (int i = 0; i < mStepBeanList.size(); i++) {
                TextView textView = new TextView(getContext());
                StepBean stepBean = mStepBeanList.get(i);
                textView.setTextSize(mTextSize);
                textView.setText(stepBean.getName());

                int spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
                textView.measure(spec, spec);
                // getMeasuredWidth
                int measuredWidth = textView.getMeasuredWidth();
                textView.setX(bigCircleCenterPosList.get(i) - measuredWidth / 2);
                textView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));

                if (i <= completedPos) {
                    textView.setTextColor(mCompleteTextColor);
                } else {
                    textView.setTextColor(mUncompleteTextColor);
                }
                mTextContainer.addView(textView);
            }
        }
}

然后我们在StepViewIndicator中的onDraw方法中,进行调用:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    if (mOnDrawIndicatorListener != null) {
        mOnDrawIndicatorListener.onDrawIndicator();
    }
   ……
}

三、Fragment

在三个Fragment中可以看见有几个共性,比如都有按钮,按钮的点击事件,因此我们可以抽取出来一个BaseFragment.

public abstract class BaseFragment extends Fragment {

    public static final String REFRESH_STEPVIEW = "refresh_stepview";

    private Button mButton;
    private ButtonClickListener mButtonClickListener;


    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(getResourceId(), container, false);
        mButton = (Button) view.findViewById(findButton());
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (null != mButtonClickListener) {
                    mButtonClickListener.onBtnClick(v);
                }
                EventBus.getDefault().post(REFRESH_STEPVIEW);
            }
        });
        return view;
    }

    public void setButtonClickListener(ButtonClickListener buttonClickListener) {
        mButtonClickListener = buttonClickListener;
    }

    abstract int getResourceId();

    abstract int findButton();

    public interface ButtonClickListener {
        void onBtnClick(View v);
    }
    protected void show(Fragment fragment) {
        FragmentManager fragmentManager = getFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragmentTransaction.replace(R.id.fragment_content, fragment).commit();
    }
}

抽取出来显示的方法show,只需要传递进来需要显示的Fragment即可。
主界面布局:



    


        

        

    


那么点击按钮要通知StepView切换怎么实现呢?这个就跟我们Demo的组织关系有关联了,整个Demo采用的是MVP的模式,整个目录如下:

Android StepView_第5张图片
6.png

面向接口StepViewContract编程

public interface StepViewContract {
    interface StepCompleteView {
        void setPresenter(StepPresenter stepPresenter);
        StepView getStepView();
    }
    interface StepPresenter {
        void initData(List textIndicators, List completeRes, List uncompleteRes);

        /**
         * 更新数据
         */
        void refreshData();
    }
}

MainActivity中可以拿到Presenter,在点击事件发生的时候通过Presenter去更新数据就可以达到更新StepView的目的,看下代码就清晰了,在MainActivity中调用mPresenter.refreshData:

@Subscribe(threadMode = ThreadMode.MAIN)
public void onRrefreshData(String refreshData){
        if (refreshData == BaseFragment.REFRESH_STEPVIEW){
            mPresenter.refreshData();
        }
}

在StepViewPresenter中:

@Override
public void refreshData() {
        refreshCurPos();
        refreshStepView();
}

private void refreshStepView() {
    getBuilder().setCurrrentPos(mCurrentPos).build(mStepCompleteView.getStepView());
}

private void refreshCurPos() {
        mCurrentPos++;
        if (mCurrentPos >= mTextIndicator.size()) {
            mCurrentPos = 0;
        }
}

那么在点击事件发生的时候,BaseFragment中怎么通知MainActivity呢?为了解耦我们用的是EventBus来进行通知工作。

mButton.setOnClickListener(new View.OnClickListener() {
     @Override
      public void onClick(View v) {
           if (null != mButtonClickListener) {
                mButtonClickListener.onBtnClick(v);
           }
           EventBus.getDefault().post(REFRESH_STEPVIEW);
      }
});

MainActivity中为了收到通知事件,需要三步工作,因为我们今天的重点不是这个,所以就简单说下:

第一步注册
@Override
protected void onStart() {
        super.onStart();
        EventBus.getDefault().register(this);
}
第二步接收事件
@Subscribe(threadMode = ThreadMode.MAIN)
public void onRrefreshData(String refreshData){
  if (refreshData == BaseFragment.REFRESH_STEPVIEW){
      mPresenter.refreshData();
  }
}
第三步解注册
@Override
protected void onStop() {
    super.onStop();
    EventBus.getDefault().unregister(this);
}

好了到这我们今天的StepView的工作就完成了,这个Demo的源码已经放到网上,有需要的可以看下。
GitHub链接:https://github.com/juexingzhe/MyStepView

参考链接:
https://github.com/baoyachi/StepView/blob/master/Introduction.md

欢迎关注公众号:JueCode

你可能感兴趣的:(Android StepView)