Android StepView

本文将会完成:

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,271评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,725评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,252评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,634评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,549评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,985评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,471评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,128评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,257评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,233评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,235评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,940评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,528评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,623评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,858评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,245评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,790评论 2 339

推荐阅读更多精彩内容