Android StepView

作者: juexingzhe | 来源:发表于2017-05-14 01:02 被阅读414次

    本文将会完成:

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

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


    1.png

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


    2.png
    完成的是下单、 送货和签收状态
    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 可以动态添加下方的文字。

    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;
    }
    

    我们的布局就比较简单:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent"
                  android:orientation="vertical">
    
        <com.example.didi.myapplication.view.StepViewIndicator
            android:id="@+id/stepview_indicator"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="6dp"/>
    
        <RelativeLayout
            android:id="@+id/stepview_text_container"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
    
    </LinearLayout>
    

    为了更灵活的构建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<String> textIndicator;
            private List<Integer> completeDrawableResIdList;
            private List<Integer> uncompleteDrawableResIdList;
            private List<StepBean> 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<StepBean> stepBeanList) {
            mStepBeanList = stepBeanList;
            mStepViewIndicator.setStepBeanList(stepBeanList);
            return this;
    }
    
    public void setStepBeanList(List<StepBean> 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<Float> 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即可。
    主界面布局:

    <RelativeLayout
        android:id="@+id/activity_main"
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        tools:context="com.example.juexingzhe.MyStepView.view.MainActivity">
    
        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">
    
    
            <com.example.juexingzhe.MyStepView.view.StepView
                android:id="@+id/stepview"
                android:layout_width="match_parent"
                android:layout_height="150dp"/>
    
            <FrameLayout
                android:id="@+id/fragment_content"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>
    
        </FrameLayout>
    
    </RelativeLayout>
    

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

    6.png

    面向接口StepViewContract编程

    public interface StepViewContract {
        interface StepCompleteView {
            void setPresenter(StepPresenter stepPresenter);
            StepView getStepView();
        }
        interface StepPresenter {
            void initData(List<String> textIndicators, List<Integer> completeRes, List<Integer> 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

      本文链接:https://www.haomeiwen.com/subject/knqfxxtx.html