导语
有些时候看着别人家的自定义控件很酷炫。
别人家的控件
淘宝
Paste_Image.png美团外卖
Paste_Image.png自己家的控件
Paste_Image.png做开发的时候,难免产品会给出类似的需求和设计,看到这样的物流状态图,你第一时间会想到用什么方法去解决?当然,解决的办法N多种,比如用ListView去实现,或是用动态布局去实现,唯一需要解决的就是隐藏cell直接的间隔线,无缝连接每个cell之间的进度线,做一些布局上的巧妙处理。看到类似的物流状态图,我第一反应总是想用自己的办法去解决,脑子里浮现的是用一个自定义控件去搞定这件事情,总觉得用ListView控件去实现这种数据量不大的界面表现有点大材小用。于是便有了下面的思路和解决办法:
阅读本文需要你了解这几个知识点:
1、自定义View
2、手势监听
3、View滑动
4、适配器设计模式
思路整理
从界面呈现上
从整体来看,界面上呈现的数据是一个数组或是List形式的数据封装,然后根据数据的容量大小for循环动态绘制,绑定相关数据到界面展示。
从一个cell局部来看,一个cell包含的内容分6部分,分别是左上角的状态提示点、左边的状态进度线、cell的背景、主文字区域、时间文字区域以及文字按钮区域。
从其他来看,其实还包括每个cell之间的间隔距离、整个View和父控件的间隔距离、文字之间的距离、文字和cell间的距离等等。
从逻辑上
根据for循环动态填充界面元素,最主要的逻辑是要计算每个元素的坐标位置,然后根据坐标位置绘制相关元素。cell中某些元素的高度是给的定值,有些则是自适应内容高度,高度不定,这些高度值需要做累加处理,才能准确计算下个元素的坐标位置。比如cell中的主文字高度是要根据文字字数自适配的,我们需要计算出文字的高度。
左边的物流状态进度线也会根据cell的高度绘制,其实这里我们可以换个角度去思考,左边的进度线我们并不需要每一次for循环都去计算进度线需要绘制的长度或是计算绘制起点和终点的坐标位置,我们只需要得到物流状态的第一个点和最后一个点的坐标位置,然后用一条线连接即可,这样即可贯串中间的所有物流状态点。
从交互上
控件能承受的数据长度是不定的,数据量偏大,绘制肯定会超出屏幕,这里需要做一个滑动;每个cell中的按钮文字区域,需要计算每个文字按钮的有效触摸区域,在触摸事件结束的时候响应监听事件,模拟一个点击事件。
从适配上
控件能接受的数据源的数据结构肯定是多样的,这里需要为控件做一个数据适配器,以适应各种数据接口的数据源。
运行效果(刷新可看动图)
代码实现
定义相关参数
<pre>
private int screenWidth;
private int screenHeight;
private int width, height; //当前View宽高
private int viewWidth, viewHeight; //当前View宽高
private int firstExpressCircleMarginLeft = DeviceUtils.dipToPx(getContext(), 16);
private int firstExpressCircleMarginTop = DeviceUtils.dipToPx(getContext(), 40);
private int expressCircleRadius = DeviceUtils.dipToPx(getContext(), 6);//物流状态提示圈半径
private int expressCircleCurrentRadius = DeviceUtils.dipToPx(getContext(), 3);//物流状态提示圈半径
private int expressCircleOuterRadius = DeviceUtils.dipToPx(getContext(), 8);//物流状态提示圈外半径
private int circleToTextMargin = DeviceUtils.dipToPx(getContext(), 12);//物流状态提示圈到文字背景的距离
private int expressTextBackgroundWidth; //文字背景宽
private int expressTextMargin = DeviceUtils.dipToPx(getContext(), 8); //文字距离背景边距
private int expressTextVecPadding = DeviceUtils.dipToPx(getContext(), 5); //每个物流信息竖直方向的间距
private int expressTextToTimeTextPadding = DeviceUtils.dipToPx(getContext(), 6); //物流文字距离时间文字的间距
private int expressButtonTextHeight; //按钮文字高度
private boolean isTimeButtonVisible = false; //是否需要显示时间和按钮
private Paint expressCirclePaint; //物流状态提示圈
private Paint expressTextBackgroundPaint; //文字背景
private TextPaint expressTextPaint; //文字
private TextPaint timeTextPaint; //时间文字
private TextPaint buttonTextPaint; //按钮文字
private Paint bgPaint;
private Paint expressLinePaint;//物流线条
private int expressTextSize;//文字大小
private int expressTimeTextSize;//时间文字大小
private int contentDrawHeight; //根据内容绘制的高度
</pre>
在res/values/attrs.xml中自定义控件相关属性
<pre>
<declare-styleable name="ExpressView">
<attr name="firstExpressCircleMarginLeft" format="dimension">16</attr>
<attr name="firstExpressCircleMarginTop" format="dimension">16</attr>
<attr name="expressCircleRadius" format="dimension">6</attr>
<attr name="expressCircleOuterRadius" format="dimension">8</attr>
<attr name="circleToTextMargin" format="dimension">12</attr>
<attr name="expressTextMargin" format="dimension">8</attr>
<attr name="expressTextVecPadding" format="dimension">5</attr>
<attr name="expressTextSize" format="dimension">18</attr>
<attr name="expressTimeTextSize" format="dimension">14</attr>
<attr name="isTimeButtonVisible" format="boolean">false</attr>
<attr name="progressBarStyleHorizontal" format="reference" ></attr>
</declare-styleable>
</pre>
其中
firstExpressCircleMarginLeft 第一个物流状态点距离父控件坐边的间距
firstExpressCircleMarginTop 第一个物流状态点距离父控件上边的间距
expressCircleRadius 物流状态点内圈半径
expressCircleOuterRadius 物流状态点外圈半径
circleToTextMargin 物流状态提示圈到文字背景的距离
expressTextMargin 文字距离背景边距
expressTextVecPadding 每个物流信息竖直方向的间距
expressTextSize 文字大小
expressTimeTextSize 时间文字大小
isTimeButtonVisible 是否显示时间和文字按钮
如果设置isTimeButtonVisible为false,界面显示
Paste_Image.png初始化属性值
<pre>
private void initTypeArray(Context context, AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ExpressView);
firstExpressCircleMarginLeft = (int) array.getDimension(R.styleable.ExpressView_firstExpressCircleMarginLeft, 16);
firstExpressCircleMarginTop = (int) array.getDimension(R.styleable.ExpressView_firstExpressCircleMarginTop, 16);
expressCircleRadius = (int) array.getDimension(R.styleable.ExpressView_expressCircleRadius, 6);
expressCircleOuterRadius = (int) array.getDimension(R.styleable.ExpressView_expressCircleOuterRadius, 8);
circleToTextMargin = (int) array.getDimension(R.styleable.ExpressView_circleToTextMargin, 12);
expressTextMargin = (int) array.getDimension(R.styleable.ExpressView_expressTextMargin, 8);
expressTextVecPadding = (int) array.getDimension(R.styleable.ExpressView_expressTextVecPadding, 5);
expressTextSize = (int) array.getDimension(R.styleable.ExpressView_expressTextSize, 16);
expressTimeTextSize = (int) array.getDimension(R.styleable.ExpressView_expressTimeTextSize, 16);
isTimeButtonVisible = array.getBoolean(R.styleable.ExpressView_isTimeButtonVisible, false);
array.recycle();
buttonTopPositionMap = new HashMap<>();
buttonTopPositionMap.put(0, new Point(0, 0)); //设置初始值
}
</pre>
初始化画笔
<pre>
private void initPaint(Context context) {
touchDistance = ViewConfiguration.get(context).getScaledTouchSlop();
screenHeight = DeviceUtils.getScreenHeight(context);
screenWidth = DeviceUtils.getScreenWidth(context);
//物流状态提示圈
expressCirclePaint = new Paint();
expressCirclePaint.setColor(Color.parseColor("#969696"));
expressCirclePaint.setStyle(Paint.Style.FILL);
expressCirclePaint.setAntiAlias(true);
expressCirclePaint.setStrokeWidth(DeviceUtils.dipToPx(getContext(), 2));
expressTextBackgroundPaint = new Paint();
expressTextBackgroundPaint.setAntiAlias(true);
expressTextBackgroundPaint.setColor(Color.WHITE);
expressTextBackgroundPaint.setStyle(Paint.Style.FILL);
expressCirclePaint.setStrokeWidth(DeviceUtils.dipToPx(getContext(), 2));
expressTextPaint = new TextPaint();
expressTextPaint.setAntiAlias(true);
expressTextPaint.setColor(Color.BLACK);
expressTextPaint.setTextSize(expressTextSize);
expressTextPaint.setStyle(Paint.Style.FILL);
timeTextPaint = new TextPaint();
timeTextPaint.setAntiAlias(true);
timeTextPaint.setColor(Color.parseColor("#969696"));
timeTextPaint.setTextSize(expressTimeTextSize);
timeTextPaint.setStyle(Paint.Style.FILL);
buttonTextPaint = new TextPaint();
buttonTextPaint.setAntiAlias(true);
buttonTextPaint.setColor(Color.parseColor("#4682B4"));
buttonTextPaint.setTextSize(expressTextSize);
buttonTextPaint.setStyle(Paint.Style.FILL);
expressLinePaint = new Paint();
expressLinePaint.setAntiAlias(true);
expressLinePaint.setColor(Color.parseColor("#969696"));
expressLinePaint.setStyle(Paint.Style.FILL);
expressLinePaint.setStrokeWidth(DeviceUtils.dipToPx(getContext(), 1));
bgPaint = new Paint();
bgPaint.setAntiAlias(true);
bgPaint.setAlpha(30);
bgPaint.setColor(Color.parseColor("#969696"));
bgPaint.setStyle(Paint.Style.STROKE);
}
</pre>
这两个方法的初始化都放在构造方法里
<pre>
public ExpressView(Context context) {
super(context);
initPaint(context);
}
public ExpressView(Context context, AttributeSet attrs) {
super(context, attrs);
initTypeArray(context, attrs);
initPaint(context);
}
public ExpressView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initTypeArray(context, attrs);
initPaint(context);
}
</pre>
重写onMeasure方法
<pre>
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
Log.e("ExpressView", "屏幕宽高 " + screenWidth + " " + screenHeight);
if (modeWidth == MeasureSpec.EXACTLY) {
viewWidth = sizeWidth;
Log.e("ExpressView", "精确测量宽 " + viewWidth);
} else {
viewWidth = width;
Log.e("ExpressView", "粗略测量宽 " + viewWidth);
}
if (modeHeight == MeasureSpec.EXACTLY) {
viewHeight = sizeHeight;
Log.e("ExpressView", "精确测量高 " + viewHeight);
} else {
viewHeight = height;
Log.e("ExpressView", "粗略测量高 " + viewHeight);
}
viewWidth = viewWidth > screenWidth ? screenWidth : viewWidth;
viewHeight = viewHeight > screenHeight ? screenHeight : viewHeight;
setMeasuredDimension(viewWidth, viewHeight);
expressTextBackgroundWidth = viewWidth - 2 * (firstExpressCircleMarginLeft - expressCircleRadius) - 2 * circleToTextMargin;
Log.e("ExpressView", "View宽度 " + viewWidth + "绘制的文字背景宽度 " + expressTextBackgroundWidth);
}
</pre>
重写onDraw方法
这里主要讲解下cell中文字的绘制以及文字高度的获取
用StaticLayout即可解决文字自动换行以及获取文字的高度,具体使用方法如下:
<pre>
StaticLayout layout = new StaticLayout(text, expressTextPaint, expressTextBackgroundWidth - 2 * expressTextMargin, Layout.Alignment.ALIGN_NORMAL, 1.0F, 0.0F, true);
int textHeight = layout.getHeight();//计算文字高度
layout.draw(canvas); //绘制到画布
</pre>
StaticLayout中四个参数的意思:文字内容、画笔、文字宽度、对其方式、相对行间距、在基础行距上添加值、是否包含间距值。
重写onTouchEvent方法,处理手势监听
定义一个HashMap集合,用于存储文字按钮坐标值
<pre>
private Map<Integer, Point> buttonTopPositionMap; //用于存储点击按钮左上角坐标
private int firstButtonPositionY; //记录第一个按钮位置坐标
</pre>
手势监听
<pre>
@Override
public boolean onTouchEvent(MotionEvent event) {
this.getParent().requestDisallowInterceptTouchEvent(true);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downY = (int) event.getY();
lastY = downY;
lastMoveY = downY;
isMoving = false;
break;
case MotionEvent.ACTION_MOVE:
downY = (int) event.getY();
int transY = downY - lastY;
int transMoveY = downY - lastMoveY; //每次手指按下到滑动停止的滑动距离
isMoving = Math.abs(transMoveY) > touchDistance ? true : false; //判定是否滑动
Log.e("ExpressViewTouch", "当前滑动距离 " + Math.abs(transMoveY) + " 是否滑动 " + isMoving);
if (isMoving) {
transDistance += transY;
Log.e("ExpressViewOnScreen", "滑动距离" + transDistance);
scrollBy(0, -transY);
}
lastY = downY;
break;
case MotionEvent.ACTION_UP:
if (isTimeButtonVisible) {
Log.e("ExpressViewTouch", "累计滑动总距离 " + transDistance);
if (isMoving || (!isMoving && (buttonTopPositionMap.get(0).y == firstButtonPositionY))) {
Iterator<Map.Entry<Integer, Point>> it = buttonTopPositionMap.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<Integer, Point> entry = it.next();
entry.getValue().y += transDistance;
}
}
Log.e("ExpressViewTouch", "手指离开屏幕位置信息 " + JsonUtils.objectToString(buttonTopPositionMap, Map.class));
onActionUpEvent(isMoving, event, buttonTopPositionMap);
this.getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_CANCEL:
this.getParent().requestDisallowInterceptTouchEvent(false);
break;
}
return true;
}
</pre>
这里需要注意一点是,每一次滑动操作,文字按钮的坐标信息都会发生变动,需要随滑动过程随时记录最新的坐标位置。
手指离开屏幕需要检验当前点击是否是一次有效点击事件并处理点击回调事件
<pre>
private void onActionUpEvent(boolean isMoving, MotionEvent event, Map<Integer, Point> maps) {
int touchX = (int) event.getX();
int touchY = (int) event.getY();
Iterator<Map.Entry<Integer, Point>> it = maps.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<Integer, Point> entry = it.next();
if (!isMoving) { //只有点击事件才检验点击是否有效并响应点击事件
if (isBooleanXY1(touchX, touchY, entry)) {
onExpressItemButtonClickListener.onExpressItemButtonClick(entry.getKey(), 0);
} else if (isBooleanXY2(touchX, touchY, entry)) {
onExpressItemButtonClickListener.onExpressItemButtonClick(entry.getKey(), 1);
}
}
}
}
</pre>
定义数据适配器
由于我们的物流状态控件只需要四种数据:主文字、时间文字、按钮坐边文字以及按钮右边文字,所以要对数据源做适配处理。
针对物流控件封装好数据格式
<pre>
public class ExpressViewData {
private String content; //内容
private String time; //时间
private String leftBtnText; //左按钮文字
private String rightBtnText; //右按钮文字
@Override
public String toString() {
return "ExpressViewData{" +
"content='" + content + '\'' +
", time='" + time + '\'' +
", leftBtnText='" + leftBtnText + '\'' +
", rightBtnText='" + rightBtnText + '\'' +
'}';
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getLeftBtnText() {
return leftBtnText;
}
public void setLeftBtnText(String leftBtnText) {
this.leftBtnText = leftBtnText;
}
public String getRightBtnText() {
return rightBtnText;
}
public void setRightBtnText(String rightBtnText) {
this.rightBtnText = rightBtnText;
}
public String getTime() {
return time;
}
public void setTime(String time) {
this.time = time;
}
}
</pre>
定义一个抽象适配器类
<pre>
public abstract class ExpressViewAdapter<T> {
private OnDataChangedListener onDataChangedListener;
private List<T> dataList;
public ExpressViewAdapter(List<T> dataList) {
this.dataList = dataList;
}
public int getCount(){
return dataList == null ? 0 : dataList.size();
}
public T getItem(int position){
return dataList == null ? null : dataList.get(position);
}
public abstract ExpressViewData bindData(ExpressView expressView, int position, T t);
public void notifyDataChanged(){
onDataChangedListener.onDataChanged();
}
public interface OnDataChangedListener {
void onDataChanged();
}
public void setOnDataChangedListener(OnDataChangedListener onDataChangedListener) {
this.onDataChangedListener = onDataChangedListener;
}
}
</pre>
数据适配具体实现
<pre>
adapter = new ExpressViewAdapter<ExpressMessageBean>(list) {
@Override
public ExpressViewData bindData(ExpressView expressView, int position, ExpressMessageBean expressMessageBean) {
ExpressViewData data = new ExpressViewData();
data.setContent(expressMessageBean.getOpContent());
data.setTime(expressMessageBean.getCreateTimeFormat());
data.setLeftBtnText(expressMessageBean.getFlowStateBtLeft());
data.setRightBtnText(expressMessageBean.getFlowStateBtRight());
return data;
}
};
</pre>
控件中具体取值
<pre>
ExpressViewData expressViewData = mAdapter.bindData(this, i, mAdapter.getItem(i));
</pre>
适配器的使用
<pre>
expressView.setAdapter(adapter);
adapter.notifyDataChanged();
</pre>
通过适配器,即可把任意的数据源做适配展示到物流状态控件上。
源码
ExpressView
<pre>
/**
- 物流状态View
- Created by licheng on 19/2/17.
*/
public class ExpressView extends View implements ExpressViewAdapter.OnDataChangedListener {
private int screenWidth;
private int screenHeight;
private int width, height; //当前View宽高
private int viewWidth, viewHeight; //当前View宽高
private int firstExpressCircleMarginLeft = DeviceUtils.dipToPx(getContext(), 16);
private int firstExpressCircleMarginTop = DeviceUtils.dipToPx(getContext(), 40);
private int expressCircleRadius = DeviceUtils.dipToPx(getContext(), 6);//物流状态提示圈半径
private int expressCircleCurrentRadius = DeviceUtils.dipToPx(getContext(), 3);//物流状态提示圈半径
private int expressCircleOuterRadius = DeviceUtils.dipToPx(getContext(), 8);//物流状态提示圈外半径
private int circleToTextMargin = DeviceUtils.dipToPx(getContext(), 12);//物流状态提示圈到文字背景的距离
private int expressTextBackgroundWidth; //文字背景宽
private int expressTextMargin = DeviceUtils.dipToPx(getContext(), 8); //文字距离背景边距
private int expressTextVecPadding = DeviceUtils.dipToPx(getContext(), 5); //每个物流信息竖直方向的间距
private int expressTextToTimeTextPadding = DeviceUtils.dipToPx(getContext(), 6); //物流文字距离时间文字的间距
private int expressButtonTextHeight; //按钮文字高度
private boolean isTimeButtonVisible = false; //是否需要显示时间和按钮
private Paint expressCirclePaint; //物流状态提示圈
private Paint expressTextBackgroundPaint; //文字背景
private TextPaint expressTextPaint; //文字
private TextPaint timeTextPaint; //时间文字
private TextPaint buttonTextPaint; //按钮文字
private Paint bgPaint;
private Paint expressLinePaint;//物流线条
private int expressTextSize;//文字大小
private int expressTimeTextSize;//时间文字大小
private int contentDrawHeight; //根据内容绘制的高度
private Map<Integer, Point> buttonTopPositionMap; //用于存储点击按钮左上角坐标
private int firstButtonPositionY; //记录第一个按钮位置坐标
// private int[] location = new int[2];
private ExpressViewAdapter mAdapter;
public ExpressView(Context context) {
super(context);
initPaint(context);
}
public ExpressView(Context context, AttributeSet attrs) {
super(context, attrs);
initTypeArray(context, attrs);
initPaint(context);
}
public ExpressView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initTypeArray(context, attrs);
initPaint(context);
}
private void initTypeArray(Context context, AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ExpressView);
firstExpressCircleMarginLeft = (int) array.getDimension(R.styleable.ExpressView_firstExpressCircleMarginLeft, 16);
firstExpressCircleMarginTop = (int) array.getDimension(R.styleable.ExpressView_firstExpressCircleMarginTop, 16);
expressCircleRadius = (int) array.getDimension(R.styleable.ExpressView_expressCircleRadius, 6);
expressCircleOuterRadius = (int) array.getDimension(R.styleable.ExpressView_expressCircleOuterRadius, 8);
circleToTextMargin = (int) array.getDimension(R.styleable.ExpressView_circleToTextMargin, 12);
expressTextMargin = (int) array.getDimension(R.styleable.ExpressView_expressTextMargin, 8);
expressTextVecPadding = (int) array.getDimension(R.styleable.ExpressView_expressTextVecPadding, 5);
expressTextSize = (int) array.getDimension(R.styleable.ExpressView_expressTextSize, 16);
expressTimeTextSize = (int) array.getDimension(R.styleable.ExpressView_expressTimeTextSize, 16);
isTimeButtonVisible = array.getBoolean(R.styleable.ExpressView_isTimeButtonVisible, false);
array.recycle();
buttonTopPositionMap = new HashMap<>();
buttonTopPositionMap.put(0, new Point(0, 0)); //设置初始值
}
private void initPaint(Context context) {
touchDistance = ViewConfiguration.get(context).getScaledTouchSlop();
screenHeight = DeviceUtils.getScreenHeight(context);
screenWidth = DeviceUtils.getScreenWidth(context);
//物流状态提示圈
expressCirclePaint = new Paint();
expressCirclePaint.setColor(Color.parseColor("#969696"));
expressCirclePaint.setStyle(Paint.Style.FILL);
expressCirclePaint.setAntiAlias(true);
expressCirclePaint.setStrokeWidth(DeviceUtils.dipToPx(getContext(), 2));
expressTextBackgroundPaint = new Paint();
expressTextBackgroundPaint.setAntiAlias(true);
expressTextBackgroundPaint.setColor(Color.WHITE);
expressTextBackgroundPaint.setStyle(Paint.Style.FILL);
expressCirclePaint.setStrokeWidth(DeviceUtils.dipToPx(getContext(), 2));
expressTextPaint = new TextPaint();
expressTextPaint.setAntiAlias(true);
expressTextPaint.setColor(Color.BLACK);
expressTextPaint.setTextSize(expressTextSize);
expressTextPaint.setStyle(Paint.Style.FILL);
timeTextPaint = new TextPaint();
timeTextPaint.setAntiAlias(true);
timeTextPaint.setColor(Color.parseColor("#969696"));
timeTextPaint.setTextSize(expressTimeTextSize);
timeTextPaint.setStyle(Paint.Style.FILL);
buttonTextPaint = new TextPaint();
buttonTextPaint.setAntiAlias(true);
buttonTextPaint.setColor(Color.parseColor("#4682B4"));
buttonTextPaint.setTextSize(expressTextSize);
buttonTextPaint.setStyle(Paint.Style.FILL);
expressLinePaint = new Paint();
expressLinePaint.setAntiAlias(true);
expressLinePaint.setColor(Color.parseColor("#969696"));
expressLinePaint.setStyle(Paint.Style.FILL);
expressLinePaint.setStrokeWidth(DeviceUtils.dipToPx(getContext(), 1));
bgPaint = new Paint();
bgPaint.setAntiAlias(true);
bgPaint.setAlpha(30);
bgPaint.setColor(Color.parseColor("#969696"));
bgPaint.setStyle(Paint.Style.STROKE);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
width = w;
height = h;
Log.e("ExpressView", "当前View的width " + width + " height " + height);
// getLocationOnScreen(location);
// Log.e("ExpressViewOnScreen", location[0] + " " + location[1]);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
Log.e("ExpressView", "屏幕宽高 " + screenWidth + " " + screenHeight);
if (modeWidth == MeasureSpec.EXACTLY) {
viewWidth = sizeWidth;
Log.e("ExpressView", "精确测量宽 " + viewWidth);
} else {
viewWidth = width;
Log.e("ExpressView", "粗略测量宽 " + viewWidth);
}
if (modeHeight == MeasureSpec.EXACTLY) {
viewHeight = sizeHeight;
Log.e("ExpressView", "精确测量高 " + viewHeight);
} else {
viewHeight = height;
Log.e("ExpressView", "粗略测量高 " + viewHeight);
}
viewWidth = viewWidth > screenWidth ? screenWidth : viewWidth;
viewHeight = viewHeight > screenHeight ? screenHeight : viewHeight;
setMeasuredDimension(viewWidth, viewHeight);
expressTextBackgroundWidth = viewWidth - 2 * (firstExpressCircleMarginLeft - expressCircleRadius) - 2 * circleToTextMargin;
Log.e("ExpressView", "View宽度 " + viewWidth + "绘制的文字背景宽度 " + expressTextBackgroundWidth);
}
@Override
protected void onDraw(Canvas canvas) {
int expressTextBgHeightSum = 0;
int firstCirclePoint = 0, lastCirclePoint = 0; //记录第一个绘制和最后一个绘制的点位置
if (isAdapterNull()) {
for (int i = 0; i < mAdapter.getCount(); i++) {
//物流文字高度测量
ExpressViewData expressViewData = mAdapter.bindData(this, i, mAdapter.getItem(i));
String text = expressViewData.getContent();
StaticLayout layout = new StaticLayout(text, expressTextPaint, expressTextBackgroundWidth - 2 * expressTextMargin, Layout.Alignment.ALIGN_NORMAL, 1.0F, 0.0F, true);
int textHeight = layout.getHeight();//计算文字高度
int expressTextBgHeight = textHeight + 2 * expressTextMargin;
//时间文字高度测量
String timeText = "2017-02-16 23:44:31";
StaticLayout timeLayout = new StaticLayout(timeText, timeTextPaint, (expressTextBackgroundWidth - 2 * expressTextMargin) / 2, Layout.Alignment.ALIGN_NORMAL, 1.0F, 0.0F, true);
int timeTextHeight = timeLayout.getHeight();
String timeShowText = expressViewData.getTime();
//按钮文字高度测量
String buttonLeft = expressViewData.getLeftBtnText();
String buttonRight = expressViewData.getRightBtnText();
String buttoTxt = "立即购买";
StaticLayout buttonLayout = new StaticLayout(buttoTxt, buttonTextPaint, (expressTextBackgroundWidth - 2 * expressTextMargin) / 4, Layout.Alignment.ALIGN_NORMAL, 1.0F, 0.0F, true);
int buttonTextHeight = buttonLayout.getHeight();
int buttonTextWidth = buttonLayout.getWidth();
expressButtonTextHeight = buttonTextHeight;
timeTextHeight = Math.max(timeTextHeight, buttonTextHeight);
if (!isTimeButtonVisible) {
timeTextHeight = 0;
expressTextToTimeTextPadding = 0;
}
//绘制提示圆圈
canvas.save();
if (i == 0) {
expressCirclePaint.setColor(Color.parseColor("#D2691E"));
canvas.drawCircle(firstExpressCircleMarginLeft,
firstExpressCircleMarginTop + expressTextBgHeightSum + (expressTextVecPadding + expressTextToTimeTextPadding) * i,
expressCircleOuterRadius,
expressCirclePaint);
expressCirclePaint.setColor(Color.parseColor("#ffffff"));
canvas.drawCircle(firstExpressCircleMarginLeft,
firstExpressCircleMarginTop + expressTextBgHeightSum + (expressTextVecPadding + expressTextToTimeTextPadding) * i,
expressCircleCurrentRadius,
expressCirclePaint);
} else {
expressCirclePaint.setColor(Color.parseColor("#969696"));
canvas.drawCircle(firstExpressCircleMarginLeft,
firstExpressCircleMarginTop + expressTextBgHeightSum + (expressTextVecPadding + expressTextToTimeTextPadding) * i,
expressCircleRadius,
expressCirclePaint);
}
canvas.restore();
//获取第一个提示点和最后一个提示点坐标
if (i == 0)
firstCirclePoint = firstExpressCircleMarginTop + expressTextBgHeightSum + (expressTextVecPadding + expressTextToTimeTextPadding) * i;
if (i == mAdapter.getCount() - 1)
lastCirclePoint = firstExpressCircleMarginTop + expressTextBgHeightSum + (expressTextVecPadding + expressTextToTimeTextPadding) * i;
//绘制文字背景
canvas.save();
if (i == 0) {
canvas.translate(firstExpressCircleMarginLeft + circleToTextMargin + expressCircleRadius,
firstExpressCircleMarginTop + expressTextBgHeightSum + (expressTextVecPadding + expressTextToTimeTextPadding) * i);
expressTextBackgroundPaint.setColor(Color.parseColor("#D2691E"));
canvas.drawRect(0, 0, expressTextBackgroundWidth, expressTextBgHeight + expressTextToTimeTextPadding + timeTextHeight, expressTextBackgroundPaint);
expressTextBackgroundPaint.setColor(Color.parseColor("#ffffff"));
canvas.drawRect(2, 2, expressTextBackgroundWidth - 2, expressTextBgHeight + expressTextToTimeTextPadding + timeTextHeight - 2, expressTextBackgroundPaint);
} else {
canvas.translate(firstExpressCircleMarginLeft + circleToTextMargin + expressCircleRadius,
firstExpressCircleMarginTop + expressTextBgHeightSum + (expressTextVecPadding + expressTextToTimeTextPadding) * i);
expressTextBackgroundPaint.setColor(Color.parseColor("#ffffff"));
canvas.drawRect(0, 0, expressTextBackgroundWidth, expressTextBgHeight + expressTextToTimeTextPadding + timeTextHeight, expressTextBackgroundPaint);
}
if (i == mAdapter.getCount() - 1) { //记录最后一个文字背景的坐标位置
contentDrawHeight = firstExpressCircleMarginTop + expressTextBgHeightSum + (expressTextVecPadding + expressTextToTimeTextPadding) * i + expressTextBgHeight + expressTextToTimeTextPadding + timeTextHeight + expressTextVecPadding;
Log.e("ExpressView", "最后一个文字背景坐标 " + contentDrawHeight);
}
canvas.restore();
//绘制物流文字
drawLeftButton(canvas, layout, firstExpressCircleMarginLeft + circleToTextMargin + expressCircleRadius + expressTextMargin, firstExpressCircleMarginTop + expressTextMargin + expressTextBgHeightSum + (expressTextVecPadding + expressTextToTimeTextPadding) * i);
if (isTimeButtonVisible) {
//绘制时间文字
if (!StringUtils.isBlank(timeShowText)) {
StaticLayout tl = new StaticLayout(timeShowText, timeTextPaint, (expressTextBackgroundWidth - 2 * expressTextMargin) / 2, Layout.Alignment.ALIGN_NORMAL, 1.0F, 0.0F, true);
drawLeftButton(canvas, tl, firstExpressCircleMarginLeft + circleToTextMargin + expressCircleRadius + expressTextMargin, firstExpressCircleMarginTop + expressTextMargin + expressTextBgHeightSum + (expressTextVecPadding + expressTextToTimeTextPadding) * i + textHeight + expressTextToTimeTextPadding);
}
//绘制左边按钮
if (!StringUtils.isBlank(buttonLeft)) {
StaticLayout sll = new StaticLayout(buttonLeft, buttonTextPaint, (expressTextBackgroundWidth - 2 * expressTextMargin) / 4, Layout.Alignment.ALIGN_NORMAL, 1.0F, 0.0F, true);
drawLeftButton(canvas, sll, firstExpressCircleMarginLeft + circleToTextMargin + expressCircleRadius + (expressTextBackgroundWidth - 2 * expressTextMargin) * 8 / 15, firstExpressCircleMarginTop + expressTextMargin + expressTextBgHeightSum + (expressTextVecPadding + expressTextToTimeTextPadding) * i + textHeight + expressTextToTimeTextPadding);
}
//绘制右边按钮
if (!StringUtils.isBlank(buttonRight)) {
StaticLayout sll = new StaticLayout(buttonRight, buttonTextPaint, (expressTextBackgroundWidth - 2 * expressTextMargin) / 4, Layout.Alignment.ALIGN_NORMAL, 1.0F, 0.0F, true);
drawRightButton(canvas, expressTextBgHeightSum, i, textHeight, sll, buttonTextWidth);
}
}
if (i == 0)
firstButtonPositionY = firstExpressCircleMarginTop + expressTextMargin + expressTextBgHeightSum + (expressTextVecPadding + expressTextToTimeTextPadding) * i + textHeight + expressTextToTimeTextPadding; //记录第一按钮位置坐标
//存储左边按钮坐标
Point point = new Point();
point.set(firstExpressCircleMarginLeft + circleToTextMargin + expressCircleRadius + (expressTextBackgroundWidth - 2 * expressTextMargin) * 8 / 15, firstExpressCircleMarginTop + expressTextMargin + expressTextBgHeightSum + (expressTextVecPadding + expressTextToTimeTextPadding) * i + textHeight + expressTextToTimeTextPadding);
buttonTopPositionMap.put(i, point);
expressTextBgHeightSum += (expressTextBgHeight + timeTextHeight);
}
}
if (isAdapterNull()) {
canvas.save();
canvas.drawLine(firstExpressCircleMarginLeft, firstCirclePoint + expressCircleOuterRadius, firstExpressCircleMarginLeft, lastCirclePoint, expressLinePaint);
canvas.restore();
}
Log.e("ExpressView", "按钮位置信息 " + JsonUtils.objectToString(buttonTopPositionMap, Map.class));
}
//绘制左边边按钮
private void drawLeftButton(Canvas canvas, StaticLayout buttonLayout, int dx, int dy) {
canvas.save();
canvas.translate(dx, dy);
buttonLayout.draw(canvas);
canvas.restore();
}
//绘制右边按钮
private void drawRightButton(Canvas canvas, int expressTextBgHeightSum, int i, int textHeight, StaticLayout buttonLayout, int buttonTextWidth) {
drawLeftButton(canvas, buttonLayout, firstExpressCircleMarginLeft + circleToTextMargin + expressCircleRadius + (expressTextBackgroundWidth - 2 * expressTextMargin) * 8 / 15 + buttonTextWidth, firstExpressCircleMarginTop + expressTextMargin + expressTextBgHeightSum + (expressTextVecPadding + expressTextToTimeTextPadding) * i + textHeight + expressTextToTimeTextPadding);
}
/**
* 判断适配器是否为null
*
* @return
*/
private boolean isAdapterNull() {
return null != mAdapter;
}
int downY = 0;
int lastY = 0;
int lastMoveY = 0;
int transDistance; //累计滑动距离
int touchDistance; //系统判定滑动的最小距离
boolean isMoving = false; //是否滑动
@Override
public boolean onTouchEvent(MotionEvent event) {
this.getParent().requestDisallowInterceptTouchEvent(true);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downY = (int) event.getY();
lastY = downY;
lastMoveY = downY;
isMoving = false;
break;
case MotionEvent.ACTION_MOVE:
downY = (int) event.getY();
int transY = downY - lastY;
int transMoveY = downY - lastMoveY; //每次手指按下到滑动停止的滑动距离
isMoving = Math.abs(transMoveY) > touchDistance ? true : false; //判定是否滑动
Log.e("ExpressViewTouch", "当前滑动距离 " + Math.abs(transMoveY) + " 是否滑动 " + isMoving);
if (isMoving) {
transDistance += transY;
Log.e("ExpressViewOnScreen", "滑动距离" + transDistance);
scrollBy(0, -transY);
// if (Math.abs(transDistance) <= contentDrawHeight - screenHeight + location[1] && Math.abs(transDistance) > 0) {
// scrollBy(0, -transY);
// } else if (Math.abs(transDistance) == 0) {
// Log.e("ExpressViewOnScreen", "底部");
// } else {
// Log.e("ExpressViewOnScreen", "顶部");
// }
}
lastY = downY;
break;
case MotionEvent.ACTION_UP:
if (isTimeButtonVisible) {
Log.e("ExpressViewTouch", "累计滑动总距离 " + transDistance);
if (isMoving || (!isMoving && (buttonTopPositionMap.get(0).y == firstButtonPositionY))) {
Iterator<Map.Entry<Integer, Point>> it = buttonTopPositionMap.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<Integer, Point> entry = it.next();
entry.getValue().y += transDistance;
}
}
Log.e("ExpressViewTouch", "手指离开屏幕位置信息 " + JsonUtils.objectToString(buttonTopPositionMap, Map.class));
onActionUpEvent(isMoving, event, buttonTopPositionMap);
this.getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_CANCEL:
this.getParent().requestDisallowInterceptTouchEvent(false);
break;
}
return true;
}
private void onActionUpEvent(boolean isMoving, MotionEvent event, Map<Integer, Point> maps) {
int touchX = (int) event.getX();
int touchY = (int) event.getY();
Iterator<Map.Entry<Integer, Point>> it = maps.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<Integer, Point> entry = it.next();
if (!isMoving) { //只有点击事件才检验点击是否有效并响应点击事件
if (isBooleanXY1(touchX, touchY, entry)) {
onExpressItemButtonClickListener.onExpressItemButtonClick(entry.getKey(), 0);
} else if (isBooleanXY2(touchX, touchY, entry)) {
onExpressItemButtonClickListener.onExpressItemButtonClick(entry.getKey(), 1);
}
}
}
}
public void setTimeButtonVisible(boolean timeButtonVisible) {
isTimeButtonVisible = timeButtonVisible;
}
private boolean isBooleanXY1(int touchX, int touchY, Map.Entry<Integer, Point> entry) {
return isBooleanX1(touchX, entry) && isBooleanY(touchY, entry);
}
private boolean isBooleanY(int touchY, Map.Entry<Integer, Point> entry) {
return touchY > entry.getValue().y && touchY < entry.getValue().y + expressButtonTextHeight;
}
private boolean isBooleanX1(int touchX, Map.Entry<Integer, Point> entry) {
return touchX > entry.getValue().x && touchX < entry.getValue().x + (expressTextBackgroundWidth - 2 * expressTextMargin) / 4;
}
private boolean isBooleanXY2(int touchX, int touchY, Map.Entry<Integer, Point> entry) {
return isBooleanX2(touchX, entry) && isBooleanY(touchY, entry);
}
private boolean isBooleanX2(int touchX, Map.Entry<Integer, Point> entry) {
return touchX > entry.getValue().x + (expressTextBackgroundWidth - 2 * expressTextMargin) / 4 && touchX < entry.getValue().x + 2 * ((expressTextBackgroundWidth - 2 * expressTextMargin) / 4);
}
@Override
public void onDataChanged() {
invalidate();
}
public interface OnExpressItemButtonClickListener {
void onExpressItemButtonClick(int position, int status);
}
private OnExpressItemButtonClickListener onExpressItemButtonClickListener;
public void setOnExpressItemButtonClickListener(OnExpressItemButtonClickListener onExpressItemButtonClickListener) {
this.onExpressItemButtonClickListener = onExpressItemButtonClickListener;
}
public void setAdapter(ExpressViewAdapter adapter){
mAdapter = adapter;
mAdapter.setOnDataChangedListener(this);
}
}
</pre>
ExpressViewAdapter
<pre>
/**
- Created by licheng on 19/3/17.
*/
public abstract class ExpressViewAdapter<T> {
private OnDataChangedListener onDataChangedListener;
private List<T> dataList;
public ExpressViewAdapter(List<T> dataList) {
this.dataList = dataList;
}
public int getCount(){
return dataList == null ? 0 : dataList.size();
}
public T getItem(int position){
return dataList == null ? null : dataList.get(position);
}
public abstract ExpressViewData bindData(ExpressView expressView, int position, T t);
public void notifyDataChanged(){
onDataChangedListener.onDataChanged();
}
public interface OnDataChangedListener {
void onDataChanged();
}
public void setOnDataChangedListener(OnDataChangedListener onDataChangedListener) {
this.onDataChangedListener = onDataChangedListener;
}
}
</pre>
ExpressViewData
<pre>
/**
- 物流控件数据接口
- Created by licheng on 19/3/17.
*/
public class ExpressViewData {
private String content; //内容
private String time; //时间
private String leftBtnText; //左按钮文字
private String rightBtnText; //右按钮文字
@Override
public String toString() {
return "ExpressViewData{" +
"content='" + content + '\'' +
", time='" + time + '\'' +
", leftBtnText='" + leftBtnText + '\'' +
", rightBtnText='" + rightBtnText + '\'' +
'}';
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getLeftBtnText() {
return leftBtnText;
}
public void setLeftBtnText(String leftBtnText) {
this.leftBtnText = leftBtnText;
}
public String getRightBtnText() {
return rightBtnText;
}
public void setRightBtnText(String rightBtnText) {
this.rightBtnText = rightBtnText;
}
public String getTime() {
return time;
}
public void setTime(String time) {
this.time = time;
}
}
</pre>
布局文件中的使用
<pre>
ExpressView
android:id="@+id/expressview"
android:layout_width="match_parent"
android:layout_height="match_parent"
express:circleToTextMargin="12dp"
express:expressCircleOuterRadius="8dp"
express:expressCircleRadius="6dp"
express:expressTextMargin="12dp"
express:expressTextSize="14sp"
express:expressTextVecPadding="5dp"
express:expressTimeTextSize="10sp"
express:firstExpressCircleMarginLeft="16dp"
express:firstExpressCircleMarginTop="16dp"
express:isTimeButtonVisible="true" />
</pre>
客户端代码
<pre>
//数据源
final List<ExpressMessageBean> list = new ArrayList<>();
ExpressMessageBean bean = new ExpressMessageBean();
bean.setFlowState(1);
bean.setFlowStateBtRight("购买流程");
bean.setCreateTime(1487259871184l);
bean.setCreateTimeFormat(TimeUtils.millis2String(1487259871184l));
bean.setOpContent("您已付款0.1200元,购买 地下城与勇士/广东区/广东1区帐号,请联系卖家卡罗特将密保手机绑定您的手机号 18827065959");
list.add(bean);
bean = new ExpressMessageBean();
bean.setFlowState(2);
bean.setCreateTime(1487259991260l);
bean.setCreateTimeFormat(TimeUtils.millis2String(1487259991260l));
bean.setOpContent("天空套 0.1200 1个-申请退款");
list.add(bean);
bean = new ExpressMessageBean();
bean.setFlowState(3);
bean.setCreateTime(1487259871184l);
bean.setCreateTimeFormat(TimeUtils.millis2String(1487259871184l));
bean.setOpContent("您已付款0.1200元,购买 地下城与勇士/广东区/广东1区帐号,请联系卖家卡罗特将密保手机绑定您的手机号 18827065959");
list.add(bean);
bean = new ExpressMessageBean();
bean.setFlowState(4);
bean.setFlowStateBtLeft("同意退款"); //设置左右按钮文字
bean.setFlowStateBtRight("拒绝退款");
bean.setCreateTime(1487259991260l);
bean.setCreateTimeFormat(TimeUtils.millis2String(1487259991260l));
bean.setOpContent("天空套 0.1200 1个-申请退款");
list.add(bean);
bean = new ExpressMessageBean();
bean.setFlowState(5);
bean.setCreateTime(1487259871184l);
bean.setCreateTimeFormat(TimeUtils.millis2String(1487259871184l));
bean.setOpContent("您已付款0.1200元,购买 地下城与勇士/广东区/广东1区帐号,请联系卖家卡罗特将密保手机绑定您的手机号 18827065959");
list.add(bean);
bean = new ExpressMessageBean();
bean.setFlowState(6);
bean.setCreateTime(1487259991260l);
bean.setCreateTimeFormat(TimeUtils.millis2String(1487259991260l));
bean.setOpContent("天空套 0.1200 1个-申请退款");
list.add(bean);
bean = new ExpressMessageBean();
bean.setFlowState(7);
bean.setCreateTime(1487259871184l);
bean.setCreateTimeFormat(TimeUtils.millis2String(1487259871184l));
bean.setOpContent("您已付款0.1200元,购买 地下城与勇士/广东区/广东1区帐号,请联系卖家卡罗特将密保手机绑定您的手机号 18827065959");
list.add(bean);
bean = new ExpressMessageBean();
bean.setFlowState(1);
bean.setFlowStateBtRight("购买流程"); //设置右按钮文字
bean.setCreateTime(1487259991260l);
bean.setCreateTimeFormat(TimeUtils.millis2String(1487259991260l));
bean.setOpContent("天空套 0.1200 1个-申请退款");
list.add(bean);
//数据源适配
adapter = new ExpressViewAdapter<ExpressMessageBean>(list) {
@Override
public ExpressViewData bindData(ExpressView expressView, int position, ExpressMessageBean expressMessageBean) {
ExpressViewData data = new ExpressViewData();
data.setContent(expressMessageBean.getOpContent());
data.setTime(expressMessageBean.getCreateTimeFormat());
data.setLeftBtnText(expressMessageBean.getFlowStateBtLeft());
data.setRightBtnText(expressMessageBean.getFlowStateBtRight());
return data;
}
};
expressView.setAdapter(adapter);
adapter.notifyDataChanged();
//延迟4秒添加2条数据
expressView.postDelayed(new Runnable() {
@Override
public void run() {
ExpressMessageBean bean = new ExpressMessageBean();
bean.setFlowState(1);
bean.setFlowStateBtRight("购买流程"); //设置右按钮文字
bean.setCreateTime(1487259991260l);
bean.setCreateTimeFormat(TimeUtils.millis2String(1487259991260l));
bean.setOpContent("天空套 0.1200 1个-申请退款");
list.add(bean);
bean = new ExpressMessageBean();
bean.setFlowState(1);
bean.setFlowStateBtRight("购买流程"); //设置右按钮文字
bean.setCreateTime(1487259991260l);
bean.setCreateTimeFormat(TimeUtils.millis2String(1487259991260l));
bean.setOpContent("天空套 0.1200 1个-申请退款");
list.add(bean);
adapter.notifyDataChanged();
}
}, 4000);
//处理点击事件
expressView.setOnExpressItemButtonClickListener(new ExpressView.OnExpressItemButtonClickListener() {
@Override
public void onExpressItemButtonClick(int position, int status) {
switch (list.get(position).getFlowState()){
case 1:
if(status == 1){ //购买流程
ToastUtil.ToastBottow(TestActivity.this, list.get(position).getFlowStateBtRight());
}
break;
case 4:
if(status == 0) { //同意退款
ToastUtil.ToastBottow(TestActivity.this, list.get(position).getFlowStateBtLeft());
} else if(status == 1){ //拒绝退款
ToastUtil.ToastBottow(TestActivity.this, list.get(position).getFlowStateBtRight());
}
break;
default:
break;
}
}
});
</pre>
目前控件存在的问题
1、没有处理滑动冲突
2、没有处理滑动到顶部和到底部停止滑动的逻辑
3、没有实现弹性滑动的效果
源码地址:
https://github.com/xiaomanzijia/ExpressView
本文中物流状态控件的实现仅供参考学习,用于实际项目还需优化。
网友评论