*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
金融类自定义View(一)–仿蚂蚁财富基金收益曲线图
该自定义View模仿了蚂蚁金服的基金收益曲线图走势。大致包含以下功能:x轴绘制等分时间坐标;y轴绘制等分收益值;中间绘制等分虚线;收益变化折线图;上方默认显示最后收益值;长按显示当前距离最近按下点的时间坐标以及对应收益值的十字线,同时上方显示对应时间与收益。看似简单的一个收益曲线图,包含很多细节技术点需要处理。虽然该自定义View比较简单,但是和金融类复杂的K线图、蜡烛图等以及相应复杂的交互本质上是一样的,会提供很好的解决思路。本文不会讲解自定义View入门知识。enjoy it~
效果图
- 原蚂蚁金服收益View操作GIF
- 仿操作GIF
- 截图
中间虚线的绘制
要绘制横轴那五条虚线,我们首先需要知道该控件的大小和位置,由调用者在使用时设置,控件在onMeasure()的时候获取,特别注意处理一下
AT_MOST
的情况。另外,我们需要设置上下左右的起始位置,也就是给该控件设置好Padding值,如下://上下左右padding float mPaddingTop = 100; float mPaddingBottom = 70; float mPaddingLeft = 50; float mPaddingRight = 50;
知道了绘制的起始和结束位置之后,我们需要初始化一个绘制虚线的画笔,用来在Canves上绘制虚线
//初始化绘制虚线的画笔 private void initInnerXPaint() { mInnerXPaint = new Paint(); mInnerXPaint.setColor(getColor(R.color.color_fundView_xLineColor)); mInnerXPaint.setStrokeWidth(convertDp2Px(mInnerXStrokeWidth)); mInnerXPaint.setStyle(Paint.Style.STROKE); setLayerType(LAYER_TYPE_SOFTWARE, null);//禁用硬件加速 PathEffect effects = new DashPathEffect(new float[]{5, 5, 5, 5}, 1); mInnerXPaint.setPathEffect(effects); }
其实知道位置和初始化画笔之后,就可以直接绘制了。我们很容易知道最上面的那一条的起始点和结束点是
(mPaddingLeft, mPaddingTop)
,(mWidth - mPaddingRight, mPaddingTop)
,就是左右上边界减去左右和上的Padding值。同理,下方的位置也很好确定。至于中间那三条线的确定,因为是上下平移,所以主要是确定y周的坐标。我们可以根据最下面线的y周坐标减去最上面的y轴值取均分值即可:private void drawInnerXPaint(Canvas canvas) { //画5条横轴的虚线 //首先确定最大值和最小值的位置 float perHight = (mHeight - mPaddingBottom - mPaddingTop) / 4; canvas.drawLine(0 + mPaddingLeft, mPaddingTop, mWidth - mPaddingRight, mPaddingTop, mInnerXPaint);//最上面的那一条 canvas.drawLine(0 + mPaddingLeft, mPaddingTop + perHight * 1, mWidth - mPaddingRight, mPaddingTop + perHight * 1, mInnerXPaint);//2 canvas.drawLine(0 + mPaddingLeft, mPaddingTop + perHight * 2, mWidth - mPaddingRight, mPaddingTop + perHight * 2, mInnerXPaint);//3 canvas.drawLine(0 + mPaddingLeft, mPaddingTop + perHight * 3, mWidth - mPaddingRight, mPaddingTop + perHight * 3, mInnerXPaint);//4 canvas.drawLine(0 + mPaddingLeft, mHeight - mPaddingBottom, mWidth - mPaddingRight, mHeight - mPaddingBottom, mInnerXPaint);//最下面的那一条 }
按照上述步骤执行完毕之后,
invalidate()
刷新界面即可。其实虚线的绘制还是比较简单的,主要是定位、定分y周轴。
x轴时间文字和y轴收益文字的定位和绘制
首先定义绘制x\y轴文字的画笔、字体大小颜色等属性。当然,如果要细分x轴和y轴可以定义两套Paint,这里简单处理,只用了一个Paint:
//外围X、Y轴线文字 Paint mXYPaint; //x、y轴指示文字字体的大小 final float mXYTextSize = 14; //左侧文字距离左边线线的距离 final float mLeftTxtPadding = 16; //底部文字距离底部线的距离 final float mBottomTxtPadding = 20;
对于x轴的处理,我们只需要获取列表中第一个Model的时间值、结束位置的时间值和中间值寄存下来,然后在固定位置显示即可。对于第一个时间的位置,和绘制最下面一条虚线定位一样。中间时间文字的显示,x轴的坐标是:总宽度减去左右pading值,然后取半再加上左边pdding值。当然,你如果仔细观察,这个时候x并不是居中显示的,因为是从中间点开始显示的,所有会错一个一半的时间文字宽度的值,这个时候应该再减去txt.lnegth()/2。同理,最后一个时间点的x轴开始位置也是需要减去文字的宽度的,不然就会绘制出范围。特别注意,为了好看,我们让文字向下偏移了一定的距离:
float hight = mHeight - mPaddingBottom + mBottomTxtPadding;
。//找到最大时间、最小时间和中间时间显示即可 private void drawXPaint(Canvas canvas) { long beginTime = mFundModeList.get(0).datetime; long midTime = mFundModeList.get((mFundModeList.size() - 1) / 2).datetime; long endTime = mFundModeList.get(mFundModeList.size() - 1).datetime; String bengin = processDateTime(beginTime); String mid = processDateTime(midTime); String end = processDateTime(endTime); //x轴文字的高度 float hight = mHeight - mPaddingBottom + mBottomTxtPadding; canvas.drawText(bengin, mPaddingLeft, hight, mXYPaint); canvas.drawText(mid, mPaddingLeft + (mWidth - mPaddingLeft - mPaddingRight) / 2, hight, mXYPaint); canvas.drawText(end, mWidth - mPaddingRight - mXYPaint.measureText(end), hight, mXYPaint);//特别注意x轴的处理:- mXYPaint.measureText(end) }
对于y轴文字的处理,我们并不知道最小值和最大值,所以我们需要遍历记录下最小值和最大值。然后均分取间距,在左侧绘制即可。也需要特别注意文字绘制起始点的问题。
折线的绘制
对于折线的处理就比较麻烦了。按照上述思路,x轴可以直接取有效总宽度除以数据总个数,计算出每一个点的宽度即可。对于y轴,我们可以考虑一下,y轴的收益可能小到1分,或者大到100元,怎么确定每一点的y轴位置,如何保证绘制的曲线不出有效范围?我们可以用y轴有效的总高度除以收益的最大最小区间,获取每个单位的高度。当绘制时,根据对应时间点的收益乘以单位收益,即可以计算出需要的高度:
//获取单个数据X/y轴的大小 mPerX = (mWidth - mPaddingLeft - mPaddingRight) / mFundModeList.size(); mPerY = ((mHeight - mPaddingTop - mPaddingBottom) / (mMaxFundMode.dataY - mMinFundMode.dataY)); Log.e(TAG, "setDataList: " + mMinFundMode + "," + mMaxFundMode + "..." + mPerX + "," + mPerY);
根据上述计算出来单位高度,绘制y轴文字信息
private void drawYPaint(Canvas canvas) { //现将最小值、最大值画好 //draw min float txtWigth = mXYPaint.measureText(mMinFundMode.originDataY) + mLeftTxtPadding; canvas.drawText(mMinFundMode.originDataY + "", mPaddingLeft - txtWigth, mHeight - mPaddingBottom, mXYPaint); //draw max canvas.drawText(mMaxFundMode.dataY + "", mPaddingLeft - txtWigth, mPaddingTop, mXYPaint); //因为横线是均分的,所以只要取到最大值最小值的差值,均分即可。 float perYValues = (mMaxFundMode.dataY - mMinFundMode.dataY) / 4; float perYWidth = (mHeight - mPaddingBottom - mPaddingTop) / 4; //从下到上依次画 for (int i = 1; i <= 3; i++) { canvas.drawText(mMinFundMode.dataY + perYValues * i + "", mPaddingLeft - txtWigth, mHeight - mPaddingBottom - perYWidth * i, mXYPaint); } }
不长按上面最终收益文字的绘制
- 我们观察可以发现,默认文字包括:绿色的小圆点、提示文字、收益金额。看似简单的几个字,也需要一个一个处理,准备不同的画笔(当然也可以共用一个画笔)。代码不再给出。
长按上面最终收益文字的绘制
这一块因为需要监测用户的长按状态,会稍微复杂一点。首先需要在
onTouchEvent(MotionEvent event)
获取长按监听:@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mPressTime = event.getDownTime(); break; case MotionEvent.ACTION_MOVE: if (event.getEventTime() - mPressTime > DEF_LONGPRESS_LENGTH) { Log.e(TAG, "onTouchEvent: 长按了。。。"); mPressX = event.getX(); mPressY = event.getY(); //处理长按后的逻辑 showLongPressView(); } break; case MotionEvent.ACTION_UP: //处理松手后的逻辑 hiddenLongPressView(); break; default: break; } return true; }
至于长按后的处理,其实并不是在
showLongPressView();
中处理,这里只是去通知onCanves()方法去刷新界面。同时用一个标记位处理是否绘制十字:private void showLongPressView() { mDrawLongPressPaint = true; invalidate(); }
这里有一个问题,仔细观看GIF图,十字的中间位置并不一定是用户手指长按的位置,而是距离按下最近的有效时间点和对应的收益坐标的位置。这样描述可能比较难以理解,这么说吧,比如你点击的是(4,5),但是这一点并没有刚好对应的那一天(因为一天是一个区间,而坐标是一个点),而最近的一天的坐标是(4.1,5.1)。不仅仅x轴不能根据按下的位置确定,并且y轴也不能确认!需要确认到距离按下的最近的有效位置。这里的处理思路是:获取按下的x轴的位置坐标,然后遍历集合中的x轴的时间点所对应的坐标,找到间距最小的有效点,该点的x轴即有效的x轴点。为了简单处理y轴的值,这里保存的是对应点的Model,不用再去处理y轴的有效点。至于绘制十字就可以根据该有效点的x、y轴的坐标去处理:
/** * 这里处理画十字的逻辑:这里的十字不是手指按下的位置,这样没有意义。 * 而是当前按下的距离x轴最近的时间(注意:并不一定按下对应的x轴就是有时间的,如果没有取最近的)。 * 当取到x轴的值,之后算出来对应的y轴的值,这个才是十字对应的位置坐标。 * 如何获取x轴最近的时间?我们可以在FundMode中定义x\y的位置参数,遍历对比找到最小即可。 * (see: drawBrokenPaint(canvas);) * * @param canvas */ private void drawLongPress(Canvas canvas) { if (!mDrawLongPressPaint) return; //获取距离最近按下的位置的model float pressX = mPressX; //循环遍历,找到距离最短的x轴的mode FundMode finalFundMode = mFundModeList.get(0); float minXLen = Integer.MAX_VALUE; for (int i = 0; i < mFundModeList.size(); i++) { FundMode currFunMode = mFundModeList.get(i); float abs = Math.abs(pressX - currFunMode.floatX); if (abs < minXLen) { finalFundMode = currFunMode; minXLen = abs; } } //x canvas.drawLine(mPaddingLeft, finalFundMode.floatY, mWidth - mPaddingRight, finalFundMode.floatY, mLongPressPaint); //y canvas.drawLine(finalFundMode.floatX, mPaddingTop, finalFundMode.floatX, mWidth - mPaddingBottom, mLongPressPaint); //开始处理按下之后top的文字信息 //先画背景 float hight = mPaddingTop - 30; Paint bgColor = new Paint(Paint.ANTI_ALIAS_FLAG); bgColor.setColor(getColor(R.color.color_fundView_pressIncomeTxtBg)); canvas.drawRect(0, 0, mWidth, hight, bgColor); //开始画按下之后左边的日期文字 Paint timePaint = new Paint(Paint.ANTI_ALIAS_FLAG); timePaint.setTextSize(mLongPressTextSize); timePaint.setColor(getColor(R.color.color_fundView_xyTxtColor)); canvas.drawText(processDateTime(finalFundMode.datetime) + "", 10, hight / 2 + getFontHeight(mLoadingTextSize, timePaint) / 2, timePaint); //右边红色收益文字 canvas.drawText(finalFundMode.dataY + "", mWidth - mPaddingRight - mLongPressPaint.measureText(finalFundMode.dataY + ""), hight / 2 + getFontHeight(mLoadingTextSize, timePaint) / 2, mLongPressPaint); //右边的左边的提示文字 Paint hintPaint = new Paint(Paint.ANTI_ALIAS_FLAG); hintPaint.setTextSize(mLongPressTextSize); hintPaint.setColor(getColor(R.color.color_fundView_xyTxtColor)); canvas.drawText(getString(R.string.string_fundView_pressHintTxt), mWidth - mPaddingRight - mLongPressPaint.measureText(finalFundMode.dataY + "") - hintPaint.measureText(getString(R.string.string_fundView_pressHintTxt)), hight / 2 + getFontHeight(mLoadingTextSize, timePaint) / 2, hintPaint); }
如何实现蚂蚁金服基金收益的长按后十字延迟消失?这里延迟消失,其实是很好的体验,省的按下的位置不能及时看不到。其实只需要设置一个标记位,当标记为为false时,不执行绘制十字的操作即可:
if (!mDrawLongPressPaint) return;
private void hiddenLongPressView() { //实现蚂蚁金服延迟消失十字线 postDelayed(new Runnable() { @Override public void run() { mDrawLongPressPaint = false; invalidate(); } }, 1000); }
正在加载中文字处理
- 这里只需要在数据过来之前居中显示一个Text即可:
canvas.drawText(mLoadingText, mWidth / 2 - mLoadingPaint.measureText(mLoadingText) / 2, mHeight / 2, mLoadingPaint);
- 那么如何在数据来之后取消这个显示呢?其实所谓取消,直接不执行这个绘制即可(因为重新刷新了界面)。其实和上面十字消失是一样的道理,加一个标记位。
code
https://github.com/scsfwgy/FinancialCustomerView
- 注:该项目会一直维护,不断加入新的关于金融类的各种自定义View。最终的目标是绘制出复杂多变的K线图~