本文将会完成:
1.自定义控件
2.MVP模式、模板模式
3.接口扩展
4.EventBus解耦
5.一句话实例化StepView
最近项目需要实现一个功能,类似于网上某宝购物网站的订单跟踪流程,下单-->送货-->签收等等,我们先看下本文要实现的demo。下单界面点击下一步流程会走到送货界面,再次点击下一步会到签收界面。状态分成完成和未完成。
完成的是下单,未完成的是送货和签收过程
完成的是下单和送货,未完成的是签收
完成的是下单、 送货和签收状态
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。应该很容易看懂,需要小学数学功底,逃:)
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 可以动态添加下方的文字。
这里当然是封装一下比较高大上了,我们把每一步的名字、状态和背景图片封装一个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的模式,整个目录如下:
面向接口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