本Demo主要目的為學(xué)習(xí)及研究自定義View,通過(guò)實(shí)現(xiàn)一個(gè)圖表的數(shù)據(jù)展示功能舔哪,熟悉和了解View的繪制過(guò)程
先看一下產(chǎn)品需求
- X軸和Y軸坐標(biāo)分別表示時(shí)間及對(duì)應(yīng)的數(shù)值
- Y軸坐標(biāo)依數(shù)據(jù)顯示5-10行攻走,Y軸輔助線顯示3-5條
- X軸依時(shí)間文字的長(zhǎng)短進(jìn)行展示,要求X軸坐標(biāo)值不重合
- 各坐標(biāo)點(diǎn)用直線相連嚎花,且連線與X軸區(qū)域添加漸變色
- 添加touch時(shí)間,當(dāng)觸摸至坐標(biāo)點(diǎn)時(shí)顯示文本框,顯示說(shuō)明文本按咒,繪制坐標(biāo)點(diǎn)圓圈及X、Y軸輔助線
功能分析
為完成產(chǎn)品的需求仓手,我們需要解決如下的6個(gè)問(wèn)題:
1.首先胖齐,我們需要計(jì)算出繪圖區(qū)域及坐標(biāo)軸文字顯示區(qū)域玻淑;
2.繪制坐標(biāo)軸文字;
3.繪制平行于X軸的輔助線呀伙;
4.計(jì)算各坐標(biāo)點(diǎn)位置补履;
5.連接各坐標(biāo)點(diǎn)并繪制漸變區(qū)域;
6.捕捉touch事件并添加回調(diào)剿另;
7.依據(jù)回調(diào)設(shè)置提示框內(nèi)容并繪制提示框箫锤。
代碼實(shí)現(xiàn)
因?yàn)橐故緮?shù)據(jù),所以需要自定義View暴露對(duì)外的設(shè)置數(shù)據(jù)的接口雨女,同時(shí)數(shù)據(jù)需要如下三個(gè)屬性:顏色(繪制連接線時(shí)連接線的顏色)谚攒、Y軸坐標(biāo)(選用string類型,因?yàn)闄M坐標(biāo)可能是周一氛堕、二……)馏臭、X軸坐標(biāo)值(這里選用double類型)。因此讼稚,在自定義View中可以使用內(nèi)部類Units來(lái)作為坐標(biāo)點(diǎn)括儒,同時(shí)用Map<Color,Units>來(lái)保存需要展示的數(shù)據(jù)。
//坐標(biāo)點(diǎn)位置
public static class Units {
public double y;
String x;
public Units(double y, String x) {
this.x = x;
this.y = y;
}
}
//對(duì)外暴露的接口锐想,用以設(shè)置數(shù)據(jù)
public void resetData(Map<Integer, List<Units>> map) {
this.mDatas.clear();
Iterator<Integer> it = map.keySet().iterator();
while (it.hasNext()) {
Integer color = it.next();
mDatas.put(color, map.get(color));
}
invalidate();
}
我們知道帮寻,因?yàn)槭亲远xView,所以我們需要添加一些atrrs屬性赠摇,便于對(duì)View進(jìn)行一些設(shè)置固逗;
本demo中添加的一些屬性如下
屬性名 | 類型 | 說(shuō)明 |
---|---|---|
min_size | integer | view最小尺寸 |
base_stroke_width | integer | 基礎(chǔ)線條寬度 |
base_stroke_color | color | 基礎(chǔ)線條顏色 |
base_text_size | integer | 坐標(biāo)文字大小 |
help_text_size | integer | 彈出提示框文字大小 |
help_text_margin | integer | 彈出提示框Margin |
text_margin_y | integer | Y方向文字與表格間距 |
point_size | integer | 觸摸時(shí)顯示坐標(biāo)點(diǎn)的大小 |
point_touch_size | integer | 觸摸范圍 |
text_margin_x | integer | X方向文字與表格間距 |
zero_start | boolean | Y軸是否從零開(kāi)始 |
help_text_bg_res | reference | 觸摸響應(yīng)說(shuō)明背景 |
shader | boolean | 是否添加X(jué)坐標(biāo)與連線間的漸變 |
有了如上屬性,我們?cè)谧远x初始化的初始化這些屬性,同時(shí)初始化Paint
private void init(AttributeSet attrs) {
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.RouteeFormView);
mMinSize = a.getInteger(R.styleable.RouteeFormView_min_size, 0);
mBaseColor = a.getColor(R.styleable.RouteeFormView_base_stroke_color, Color.parseColor("#d0d0d0"));
mBaseStrokeWidth = a.getInteger(R.styleable.RouteeFormView_base_stroke_width, 1);
mBaseTextSize = a.getInteger(R.styleable.RouteeFormView_base_text_size, 12);
mHelpTextSize = a.getInteger(R.styleable.RouteeFormView_help_text_size, 14);
mHelpTextMargin = a.getInteger(R.styleable.RouteeFormView_help_text_margin, 8);
mTextMarginX = DisplayUtils.dp2px(getContext(), a.getInteger(R.styleable.RouteeFormView_text_margin_x, 4));
mTextMarginY = DisplayUtils.dp2px(getContext(), a.getInteger(R.styleable.RouteeFormView_text_margin_y, 4));
mHelpTextBgResId = a.getResourceId(R.styleable.RouteeFormView_help_text_bg_res, R.drawable.bg_routee_form_view_help_text);
mNeedDrawShader = a.getBoolean(R.styleable.RouteeFormView_shader, false);
mPointWidth = DisplayUtils.dp2px(getContext(), a.getInteger(R.styleable.RouteeFormView_point_size, 2));
mPointTouchWith = DisplayUtils.dp2px(getContext(), a.getInteger(R.styleable.RouteeFormView_point_touch_size, 10));
isStartZero = a.getBoolean(R.styleable.RouteeFormView_zero_start, false);
a.recycle();
mPaint = new Paint();
mPaint.setAntiAlias(true);
}
然后藕帜,我們需要重寫(xiě)我們的onMeasure方法,計(jì)算自定義View的大小
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == AT_MOST && heightSpecMode == AT_MOST) {
setMeasuredDimension(mMinSize, mMinSize);
} else if (widthMeasureSpec == AT_MOST) {
setMeasuredDimension(mMinSize, heightSpecSize);
} else if (heightMeasureSpec == AT_MOST) {
setMeasuredDimension(widthSpecSize, mMinSize);
}
}
在計(jì)算出View的尺寸后烫罩,我們需要開(kāi)始完成自定View最重要的一步繪制,也就是重寫(xiě)onDraw(Canvas canvas)方法,依據(jù)需求分析洽故,我們需要進(jìn)行一些列的計(jì)算再去按如下順序去繪制View的不同部分:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//當(dāng)設(shè)置的數(shù)據(jù)為空時(shí)不繪制任何UI
if (mDatas == null || mDatas.size() == 0) {
return;
}
calc();
drawText(canvas);
drawLines(canvas);
drawData(canvas);
drawHelpLine(canvas);
drawHelpText(canvas);
}
在上述的onDraw(Canvas canvas)我們發(fā)現(xiàn)嗡髓,在drawText(cavas)繪制坐標(biāo)軸文字之前我們執(zhí)行了calc()方法,該方法其實(shí)就是我們之前需求分析時(shí)提到的需要計(jì)算的一些東西收津。我們?cè)賮?lái)看看calc()都計(jì)算了哪些:
private void calc() {
calcMaxYValue(); //計(jì)算Y軸最大值
calcMinYValue(); //計(jì)算Y軸最小值
calcYSpacing(); //計(jì)算Y軸間隔大小
calcYTextList(); //計(jì)算Y軸的文字內(nèi)容列表
calcTextSize(); //計(jì)算文字占用尺寸
calcFormSize(); //計(jì)算表格區(qū)域尺寸
calcXTextList(); //計(jì)算X軸的文字內(nèi)容列表
calcBaseLines(); //計(jì)算平行于X軸的輔助線
calcData(); //計(jì)算數(shù)據(jù)對(duì)應(yīng)的位置
}
接下來(lái)就是計(jì)算這些數(shù)據(jù)的具體實(shí)現(xiàn),代碼的實(shí)現(xiàn)有很多種,以下的方法只是其中一種實(shí)現(xiàn)方式而已饿这。主要還是calc的邏輯
private void calcMaxYValue() {
double max = 0;
for (Integer color : mDatas.keySet()) {
for (Units units : mDatas.get(color)) {
max = Math.max(max, units.y);
}
}
mMaxUsefulY = max;
}
private void calcMinYValue() {
double min = 0;
for (Integer color : mDatas.keySet()) {
List<Units> list = mDatas.get(color);
for (int i = 0; i < list.size(); i++) {
if (i == 0) {
min = list.get(i).y;
}
min = Math.min(min, list.get(i).y);
}
}
mMinUsefulY = min;
}
private void calcYSpacing() {
mUsefulY = mMaxUsefulY - mMinUsefulY;
if (mUsefulY == 0) {
mMaxUsefulY = mMinUsefulY + 80;
mUsefulY = 80.0;
}
int minSpacing = (int) (mUsefulY / 6);
if (minSpacing == 0) {
int w = (mMaxUsefulY + "").length();
int spacing = w / 10;
if (spacing != 0) {
mYDataSpacing = spacing;
} else if (mMaxUsefulY == 0) {
mYDataSpacing = 20;
} else if (mMaxUsefulY <= 1) {
mYDataSpacing = 1;
} else {
mYDataSpacing = 2;
}
return;
}
String s = minSpacing + "";
int length = s.length() - 1 > 0 ? s.length() - 1 : 0;
int unit = (int) (1 * Math.pow(10, length));
for (int i = 1; i <= 10; i += 1) {
if (mUsefulY / (i * unit) < 6) {
mYDataSpacing = i * unit;
return;
}
}
}
private void calcYTextList() {
mYTexts = new ArrayList<>();
if (mYDataSpacing == 1) {
mMaxUsefulY = 1.0;
}
double remainder = mMaxUsefulY % mYDataSpacing;
for (double i = mMaxUsefulY - remainder + mYDataSpacing; i >= mMinUsefulY - mYDataSpacing && i >= 0; i -= mYDataSpacing) {
mYTexts.add((int) i + "");
}
String maxY = mYTexts.get(0);
mMaxYValue = Double.parseDouble(maxY);
String minY = mYTexts.get(mYTexts.size() - 1);
mMinYValue = Double.parseDouble(minY);
}
private void calcTextSize() {
String xMax = "";
for (Integer integer : mDatas.keySet()) {
List<Units> units = mDatas.get(integer);
for (Units unit : units) {
xMax = unit.x.length() > xMax.length() ? unit.x : xMax;
}
}
mPaint.setTextSize(DisplayUtils.dp2px(getContext(), mBaseTextSize));
Rect bounds = new Rect();
mPaint.getTextBounds(xMax, 0, xMax.length(), bounds);
mMaxXTextHeight = bounds.height();
mMaxXTextWidth = bounds.width();
mPaint.getTextBounds(mYTexts.get(0), 0, mYTexts.get(0).length(), bounds);
mMaxYTextHeight = bounds.height();
mMaxYTextWidth = bounds.width();
mMaxXTextHeight = Math.max(mMaxXTextHeight, mMaxYTextHeight);
mMaxYTextHeight = Math.max(mMaxXTextHeight, mMaxYTextHeight);
mMaxYTextWidth = Math.max(mMaxYTextWidth, mMaxXTextWidth / 2 - mTextMarginX);
}
private void calcFormSize() {
mFormWidth = getWidth() - mTextMarginX - mMaxYTextWidth - mMaxXTextWidth / 2 - 1;
mFormHeight = getHeight() - mTextMarginY - mMaxXTextHeight - mMaxYTextHeight;
}
private void calcXTextList() {
mXTexts.clear();
mXSpacingCount = 1;
Iterator<Integer> it = mDatas.keySet().iterator();
if (it.hasNext()) {
Integer next = it.next();
List<Units> units = mDatas.get(next);
while ((units.size() / mXSpacingCount + 1) * mMaxXTextWidth > mFormWidth * 2 / 3) {
mXSpacingCount++;
}
for (int i = 0; i < units.size(); i++) {
mXTexts.add(units.get(i).x + "");
}
return;
}
}
private void calcBaseLines() {
mLineSpacingCount = (mYTexts.size() - 1) / 2;
if (mLineSpacingCount == 0) {
mLineSpacingCount = 1;
}
mLineSpacingCountRemainer = (mYTexts.size() - 1) % mLineSpacingCount;
}
private void calcData() {
Iterator<Integer> it = mDatas.keySet().iterator();
int size = mXTexts.size();
while (it.hasNext()) {
List<Point> listPoint = new ArrayList<>();
List<Rect> listRect = new ArrayList<>();
Integer color = it.next();
List<Units> units = mDatas.get(color);
for (int i = 0; i < units.size(); i++) {
float x = i * mFormWidth / (size - 1) + mMaxYTextWidth + mTextMarginY;
float y = (float) ((mMaxYValue - units.get(i).y) * mFormHeight / (mMaxYValue - mMinYValue) + mMaxYTextHeight);
listPoint.add(new Point((int) x, (int) y));
listRect.add(new Rect((int) (x - mPointTouchWith), (int) (y - mPointTouchWith), (int) (x + mPointTouchWith), (int) (y + mPointTouchWith)));
}
mDataPoints.put(color, listPoint);
mDataRects.put(color, listRect);
}
}
以上,該計(jì)算的都計(jì)算了,onDraw方法此時(shí)已經(jīng)可以將我們的數(shù)據(jù)繪制出來(lái)了,但是產(chǎn)品的需求是在我們點(diǎn)擊touch的時(shí)候還需要繪制輔助線并顯示輔助文本,因此,我們還需要去重寫(xiě)onTouchEvent方法:
public boolean onTouchEvent(MotionEvent event) {
int pointerCount = event.getPointerCount();
if (pointerCount > 1) {
getParent().requestDisallowInterceptTouchEvent(false);
return false;
}
mXPosition = event.getX();
mYPosition = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownEventMills = Calendar.getInstance().getTimeInMillis();
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
mUpEventMills = Calendar.getInstance().getTimeInMillis();
break;
default:
break;
}
if (mUpEventMills - mDownEventMills < 100 && mUpEventMills > mDownEventMills) {
mPreRect = null;
}
getParent().requestDisallowInterceptTouchEvent(true);
invalidate();
return true;
}
注意:因?yàn)槲覀兊膄ormView要添加touch事件,所以當(dāng)formView被用在可以滾動(dòng)的ViewGroup中時(shí)撞秋,我們touch時(shí)可能會(huì)消耗掉touchEvent长捧,如果單獨(dú)處理將滑動(dòng)事件透?jìng)髦罺iewGroup,可能并不是我們想要的效果吻贿。所以我們添加了event.getPointer判斷串结,當(dāng)多點(diǎn)觸控時(shí),我們不消耗滑動(dòng)事件。這樣就能平滑的操作formView了肌割。
最后卧蜓,就是我們的drawText(canvas);drawLines(canvas);drawData(canvas);drawHelpLine(canvas);drawHelpText(canvas);在這里,大家可以在gitHub中查看代碼把敞,我們只漸變效果及輔助文本是如何被繪制的弥奸。
private void drawData(Canvas canvas) {
Iterator<Integer> it = mDataPoints.keySet().iterator();
while (it.hasNext()) {
Path path = new Path();
Integer color = it.next();
List list = mDataPoints.get(color);
for (int i = 0; i < list.size(); i++) {
Point o = (Point) list.get(i);
if (i == 0) {
path.moveTo(o.x, o.y);
} else {
path.lineTo(o.x, o.y);
}
}
mPaint.setColor(color);
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawPath(path, mPaint);
//繪制漸變效果
if (mNeedDrawShader) {
path.lineTo(((Point) list.get(list.size() - 1)).x, mFormHeight + mMaxYTextHeight);
path.lineTo(mMaxYTextWidth + mTextMarginX, mFormHeight + mMaxYTextHeight);
path.lineTo(((Point) list.get(0)).x, ((Point) list.get(0)).y);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(Color.WHITE);
canvas.drawPath(path, mPaint);
Shader shder = new LinearGradient(getWidth() / 2, 0, getWidth() / 2, getHeight()
, color & Color.parseColor("#44ffffff")
, color & Color.parseColor("#11ffffff"), Shader.TileMode.CLAMP);
mPaint.setShader(shder);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(color);
canvas.drawPath(path, mPaint);
mPaint.setShader(null);
}
}
}
因?yàn)檩o助文本是自定義的,所以我們需要對(duì)外提供一個(gè)借口奋早,用來(lái)設(shè)置輔助文本內(nèi)容盛霎。文本的model包含兩個(gè)屬性int color;String text;
public static class TextUnit {
int color;
String text;
public TextUnit(int color, String text) {
this.color = color;
this.text = text;
}
}
最終使用List<List<TextUnit>> mDataTexts來(lái)保存需要展示的文本耽装。
private void drawHelpText(Canvas canvas) {
if (!calcHelpTextSize()) {
return;
}
Drawable drawable = ContextCompat.getDrawable(getContext(), mHelpTextBgResId);
Rect rect = calcHelpRect();
if (rect == null) {
return;
}
drawable.setBounds(rect);
drawable.draw(canvas);
Rect bounds = new Rect();
int margin = DisplayUtils.dp2px(getContext(), mHelpTextMargin);
int height = (mMaxHelpTextHeight - margin * 2 - (mDataTexts.size() - 1) * DisplayUtils.dp2px(getContext(), 4)) / mDataTexts.size();
mPaint.setTextSize(DisplayUtils.dp2px(getContext(), mHelpTextSize));
for (int i = 0; i < mDataTexts.size(); i++) {
int width = 0;
for (TextUnit unit : mDataTexts.get(i)) {
mPaint.setColor(unit.color);
mPaint.getTextBounds(unit.text, 0, unit.text.length(), bounds);
canvas.drawText(unit.text, rect.left + margin + width, rect.top + height + margin + i * (height + DisplayUtils.dp2px(getContext(), 4)), mPaint);
width += bounds.width();
}
}
}
最后愤炸,展示一下實(shí)際效果圖