各位小伙伴们大家早上好,新的一周又开始了,希望大家都能有个好心情迎接新的一周。
本篇来自 _高远 的投稿,分享了仿蚂蚁财富基金收益曲线图的实现,希望大家喜欢!
_高远 的博客地址:
http://blog.csdn.net/wgyscsf
该自定义 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 轴可以定义两套 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);
那么如何在数据来之后取消这个显示呢?其实所谓取消,直接不执行这个绘制即可(因为重新刷新了界面)。其实和上面十字消失是一样的道理,加一个标记位。
该项目会一直维护,不断加入新的关于金融类的各种自定义View,最终的目标是绘制出复杂多变的K线图。
项目 github 地址:
https://github.com/scsfwgy/FinancialCustomerView
联系客服