Android开发之实现比特币走势图(仿股票走势图)
1、介绍
最近有关区块链的项目层出不穷,在项目中碰到了绘制比特币涨幅走势图的需求,在网上搜了一些案例,大致就是利用绘制贝塞尔曲线来完成。
2、思路
思路来源于绘制股票走势图,主要内容在于自定义K_View,效果图:
大致步骤:
1、绘制X、Y坐标轴并绘制坐标;
2、根据数据绘制走势图,其实就是根据返回数据的多个点连成的平滑曲线;
3、根据手势找点,然后绘制当前所在点并显示具体数据。
相关数据对象类
public class FundMode implements Serializable {
//x轴原始时间数据,ms
public Stringtime;
public Stringlast;
public float dataY;
//在自定义view:FundView中的位置坐标
public float floatX;
public float floatY;
}
public class XYEntityimplements Serializable{
public Listxlist;
public Listylist;
}
这里的内容只是让一些小白们看的更容易理解,早对象可以根据需求随意改动,各取所需。
Xml布局中代码
<XXXXX....K_VIew
android:id="@+id/k_view"
android:layout_width="match_parent"
android:layout_height="@dimen/base130dp"
app:xyTextSize="@dimen/base8sp"
app:loadingTextSize="@dimen/base12sp"
app:longPressTextSize="@dimen/base10sp"
app:loadingText="正在加载数据...."
app:xBottomTopPadding="@dimen/base8dp"
app:reactWidth="@dimen/base100dp"
app:reactHeight="@dimen/base38dp"
app:reactTextMargin_top="@dimen/base8dp"
app:reactTextMargin_bottom="@dimen/base23dp"
app:paddingTop_FV="@dimen/base10dp"
app:paddingBottom_FV="@dimen/base20dp"
app:paddingRight_FV="@dimen/base10dp"
app:yTextPadding="@dimen/base10dp"
app:yTextRightPadding="@dimen/base5dp"
android:background="#fff"/>
attrs.xml 中内容大致就是一些自定义属性,内容如下
activity 中需要执行的操作很少,从服务器获取到数据后 直接调用 k_view.setData(apiResult)即可;
下面是自定义view具体代码,所有逻辑都包括在里面了
public class K_View extends View {
//控件默认宽高
private static final float DEF_WIDTH =650;
private static final float DEF_HIGHT =400;
//数据源
ApiResult2<List< FundMode>,XYEntity>>apiResult;
//ApiResult2 服务器返回的数据,FundMode平滑曲线上的点,XYEntity X、Y对应坐标,下面贴出数据结构
//控件宽高
int mWidth;
int mHeight;
//上下左右padding
int mPaddingTop =30;
int mPaddingBottom =50;
int yTextPadding=20;
float mPaddingLeft =70;
int mPaddingRight =30;
int yTextRightPadding =5;
float textTopPadding=5;
int reactHeight=80;
int reactWidth=210;
int reactTextMargin_top=20;
int reactTextMargin_bottom=50
FundMode mMinFundMode;
FundMode mMaxFundMode;
float maxY;
float minY;
//X、Y轴每一个data对应的大小
float mPerX;
float mPerY;
//正在加载中
Paint mLoadingPaint;
int mLoadingTextSize =20;
String mLoadingText ="";
boolean mDrawLoadingPaint =true;
Paint xyPaint;
//外围X、Y轴线文字
Paint mXYPaint;
Paint longPressPaint;
//x、y轴指示文字字体的大小
Paint rectTextPaint;
private int mXYTextSize =20;
//左侧文字距离左边线线的距离
final float mLeftTxtPadding =5;
//底部文字距离底部线的距离
int xBottomTopPadding =20;
float halfTextWidth_X=22;
private ListpointData=new ArrayList<>();
private int pointCount;
//内部X轴虚线
Paint mInnerXPaint;
float mInnerXStrokeWidth =1;
//折线
Paint mBrokenPaint;
Paint rectBGPaint;
Paint alphaPaint;
//单位:dp
//长按的十字线
Paint mLongPressPaint;
Paint blueLinePaint;
boolean mDrawLongPressPaint =false;
//长按处理
long mPressTime;
//默认多长时间算长按
final long DEF_LONGPRESS_LENGTH =200;
float mPressX;
float mPressY;
//最上面默认显示累计收益金额
final float mDefAllIncomeTextSize =20;
//长按情况下x轴和y轴要显示的文字
Paint mLongPressTxtPaint;
int mLongPressTextSize =25;
public K_View(Context context) {
this(context,null);
}
public K_View(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public K_View(Context context, @Nullable AttributeSet attrs,int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.K_View);
mXYTextSize=a.getDimensionPixelSize(R.styleable.K_View_xyTextSize, mXYTextSize);
mLoadingTextSize=a.getDimensionPixelSize(R.styleable.K_View_loadingTextSize, mLoadingTextSize);
mLongPressTextSize=a.getDimensionPixelSize(R.styleable.K_View_longPressTextSize, mLongPressTextSize);
mLoadingText=a.getString(R.styleable.K_View_loadingText);
xBottomTopPadding=a.getDimensionPixelSize(R.styleable.K_View_xBottomTopPadding, xBottomTopPadding);
reactWidth=a.getDimensionPixelSize(R.styleable.K_View_reactWidth, reactWidth);
reactHeight=a.getDimensionPixelSize(R.styleable.K_View_reactHeight, reactHeight);
reactTextMargin_top=a.getDimensionPixelSize(R.styleable.K_View_reactTextMargin_top, reactTextMargin_top);
reactTextMargin_bottom=a.getDimensionPixelSize(R.styleable.K_View_reactTextMargin_bottom, reactTextMargin_bottom);
mPaddingTop=a.getDimensionPixelSize(R.styleable.K_View_paddingTop_FV, mPaddingTop);
mPaddingBottom=a.getDimensionPixelSize(R.styleable.K_View_paddingBottom_FV, mPaddingBottom);
mPaddingRight=a.getDimensionPixelSize(R.styleable.K_View_paddingRight_FV, mPaddingRight);
yTextPadding=a.getDimensionPixelSize(R.styleable.K_View_yTextPadding, yTextPadding);
yTextRightPadding =a.getDimensionPixelSize(R.styleable.K_View_yTextRightPadding, yTextRightPadding);
initAttrs();
}
@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((int) DEF_WIDTH, (int) DEF_HIGHT);
}else if (widthSpecMode == AT_MOST) {
setMeasuredDimension((int) DEF_WIDTH, heightSpecSize);
}else if (heightSpecMode == AT_MOST) {
setMeasuredDimension(widthSpecSize, (int) DEF_HIGHT);
}else {
setMeasuredDimension(widthSpecSize, heightSpecSize);
}
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
}
@Override
protected void onLayout(boolean changed,int left,int top,int right,int bottom) {
super.onLayout(changed, left, top, right, bottom);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//默认加载loading界面
showLoadingPaint(canvas);
if (apiResult ==null || apiResult.data.size() ==0)return;
drawInnerXPaint(canvas);
drawBrokenPaint(canvas);
drawXYPaint(canvas);
drawLongPress(canvas);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// mPressTime = event.getDownTime();
mPressX = event.getX();
mPressY = event.getY();
//处理长按后的逻辑
showLongPressView();
break;
case MotionEvent.ACTION_MOVE:
mPressX = event.getX();
mPressY = event.getY();
//处理长按后的逻辑
showLongPressView();
// }
break;
case MotionEvent.ACTION_UP:
//处理松手后的逻辑
hiddenLongPressView();
break;
default:
break;
}
return true;
}
private void initAttrs() {
intXYLintPaint();
initRectBGPaint();
initLoadingPaint();
initRectTextPaint();
initInnerXPaint();
initXYPaint();
initBrokenPaint();
initAlphaPaint();
initLongPressPaint();
initYLinePaint();
// initTopTxt();
}
private void intXYLintPaint() {
xyPaint =new Paint();
xyPaint.setColor(getColor(R.color.gray));
xyPaint.setStrokeWidth(mInnerXStrokeWidth);
}
private void initRectBGPaint() {
rectBGPaint = getRectBGPaint();
}
private void initLoadingPaint() {
mLoadingPaint =new Paint();
mLoadingPaint.setColor(getColor(R.color.gray));
mLoadingPaint.setTextSize(mLoadingTextSize);
mLoadingPaint.setAntiAlias(true);
}
//初始化绘制虚线的画笔
private void initInnerXPaint() {
mInnerXPaint =new Paint();
mInnerXPaint.setColor(getColor(R.color.bg_lsj));
mInnerXPaint.setStrokeWidth(mInnerXStrokeWidth);
mInnerXPaint.setAlpha(40);
mInnerXPaint.setStyle(Paint.Style.STROKE);
}
private void initXYPaint() {
mXYPaint =new Paint();
mXYPaint.setColor(getColor(R.color.text_dz));
mXYPaint.setTextSize(mXYTextSize);
mXYPaint.setAntiAlias(true);
}
private void initLongPressTextPaint() {
longPressPaint =new Paint();
longPressPaint.setColor(getColor(R.color.text_dz));
longPressPaint.setTextSize(mXYTextSize);
longPressPaint.setAntiAlias(true);
}
private void initRectTextPaint() {
rectTextPaint =new Paint();
rectTextPaint.setColor(getColor(R.color.white));
rectTextPaint.setTextSize(mXYTextSize);
rectTextPaint.setAntiAlias(true);
}
private void initBrokenPaint() {
mBrokenPaint =new Paint();
mBrokenPaint.setColor(getColor(R.color.all_k_color));
mBrokenPaint.setStyle(Paint.Style.STROKE);
mBrokenPaint.setAntiAlias(true);
mBrokenPaint.setStrokeWidth(convertDp2Px(mInnerXStrokeWidth));
}
private void initAlphaPaint() {
alphaPaint =new Paint();
alphaPaint.setColor(getResources().getColor(R.color.all_k_color));
alphaPaint.setAntiAlias(true);
alphaPaint.setAlpha(40);
alphaPaint.setStyle(Paint.Style.FILL);
}
private void initLongPressPaint() {
mLongPressPaint =new Paint();
mLongPressPaint.setColor(getColor(R.color.white));
mLongPressPaint.setStyle(Paint.Style.FILL);
mLongPressPaint.setAntiAlias(true);
mLongPressPaint.setTextSize(mLongPressTextSize);
}
private void initYLinePaint() {
blueLinePaint =new Paint();
blueLinePaint.setColor(getColor(R.color.bg_lsj));
blueLinePaint.setStrokeWidth(1);
blueLinePaint.setAntiAlias(true);
}
private void showLoadingPaint(Canvas canvas) {
if (!mDrawLoadingPaint)return;
//这里特别注意,x轴的起始点要减去文字宽度的一半
canvas.drawText(mLoadingText, mWidth /2 - mLoadingPaint.measureText(mLoadingText) /2, mHeight /2, mLoadingPaint);
}
private void drawInnerXPaint(Canvas canvas) {
//画5条横轴的虚线
//首先确定最大值和最小值的位置
float perHight = (mHeight - mPaddingBottom - mPaddingTop) /3;
canvas.drawLine(mPaddingLeft,mPaddingTop,mPaddingLeft,mHeight - mPaddingBottom,xyPaint);
canvas.drawLine(mPaddingLeft,mHeight - mPaddingBottom,mPaddingLeft+mPerX*(apiResult.data.size()-1),mHeight - mPaddingBottom,xyPaint);
canvas.drawLine(0 + mPaddingLeft, mPaddingTop,
mPaddingLeft+mPerX*(apiResult.data.size()-1), mPaddingTop, mInnerXPaint);//最上面的那一条
canvas.drawLine(0 + mPaddingLeft, mPaddingTop + perHight *1,
mPaddingLeft+mPerX*(apiResult.data.size()-1), mPaddingTop + perHight *1, mInnerXPaint);//2
canvas.drawLine(0 + mPaddingLeft, mPaddingTop + perHight *2,
mPaddingLeft+mPerX*(apiResult.data.size()-1), mPaddingTop + perHight *2, mInnerXPaint);//4
}
private void drawBrokenPaint(Canvas canvas) {
if(pointData.size()>0){//清空所有点
pointData.clear();
}
//先画第一个点
FundMode fundMode = apiResult.data.get(0);
Path path =new Path();
//这里需要说明一下,x轴的起始点,其实需要加上mPerX,但是加上之后不是从起始位置开始,不好看。
// 同理,for循环内x轴其实需要(i+1)。现在这样处理,最后会留一点空隙,其实挺好看的。
float floatY = mHeight - mPaddingBottom - mPerY * ((Float.parseFloat(fundMode.last) - minY));
fundMode.floatX = mPaddingLeft;
fundMode.floatY = floatY;
Path timeAlphaPath =new Path();
timeAlphaPath.moveTo(mPaddingLeft, mHeight - mPaddingBottom);
timeAlphaPath.lineTo(mPaddingLeft, floatY);
for (int i =0; i < apiResult.data.size(); i++) {
FundMode fm = apiResult.data.get(i);
float floatX1 = mPaddingLeft + mPerX * i;
float floatY1 = mHeight - mPaddingBottom - mPerY * ((Float.parseFloat(fm.last) - minY));
fm.floatX = floatX1;
fm.floatY = floatY1;
if(i%pointCount==0||i==apiResult.data.size()-1){
pointData.add(fm);
}
if(i
float floatX2 = mPaddingLeft + mPerX * (i+1);
float floatY2 = mHeight - mPaddingBottom - mPerY * ( (Float.parseFloat(apiResult.data.get(i+1).last) - minY));
float wt = (floatX1 + floatX2) /2;
float floatX1_last=wt;
float floatY1_last =floatY1;
float floatX2_next=wt;
float floatY2_next = floatY2;
if(i==0){
path.moveTo(floatX1, floatY1);
}
path.cubicTo(floatX1_last, floatY1_last, floatX2_next, floatY2_next, floatX2, floatY2);
timeAlphaPath.cubicTo(floatX1_last, floatY1_last, floatX2_next, floatY2_next, floatX2, floatY2);
}else {
timeAlphaPath.lineTo(floatX1 , mHeight - mPaddingBottom);
Paint paint=new Paint();
paint.setColor(getColor(R.color.bg_lsj));
paint.setAntiAlias(true);
paint.setStrokeWidth(1);
paint.setStyle(Paint.Style.STROKE);
canvas.drawPath(path, paint);
paint.setAlpha(40);
paint.setStyle(Paint.Style.FILL);
canvas.drawPath(timeAlphaPath, paint);
}
}
}
private void drawXYPaint(Canvas canvas) {
//先处理y轴方向文字
drawYPaint(canvas);
//处理x轴方向文字
drawXPaint(canvas);
}
*
* @param canvas
*/
private void drawLongPress(Canvas canvas) {
if (!mDrawLongPressPaint)return;
//获取距离最近按下的位置的model
float pressX = mPressX;
//循环遍历,找到距离最短的x轴的mode
FundMode finalFundMode = pointData.get(0);
float minXLen = Integer.MAX_VALUE;
for (int i =0; i < pointData.size(); i++) {
FundMode currFunMode = pointData.get(i);
float abs = Math.abs(pressX - currFunMode.floatX);
if (abs < minXLen) {
finalFundMode = currFunMode;
minXLen = abs;
}
}
//x
float topY=mHeight - mPaddingBottom - mPerY * (maxY - minY);
int left=(int)finalFundMode.floatX-reactWidth>=mPaddingLeft?(int)finalFundMode.floatX-reactWidth:(int)finalFundMode.floatX;
int right=(int)finalFundMode.floatX-reactWidth>=mPaddingLeft?(int)finalFundMode.floatX:reactWidth+(int)finalFundMode.floatX;
int top=(int)finalFundMode.floatY-reactHeight>=(int)topY?(int)finalFundMode.floatY-reactHeight:(int)topY;
int bottom=(int)finalFundMode.floatY-reactHeight>=(int)topY?(int)finalFundMode.floatY:(int)topY+reactHeight;
Rect topDirty =new Rect(left, top, right, bottom);
canvas.drawRect(topDirty, rectBGPaint);
canvas.drawText(finalFundMode.time +"",
left+(reactWidth-mLongPressPaint.measureText(finalFundMode.time+""))/2,top+reactTextMargin_top+getFontHeight(mLongPressTextSize, mLongPressPaint) /2 , mLongPressPaint);
canvas.drawText("¥"+AppConfig.formatNum(finalFundMode.last) ,
left+(reactWidth-mLongPressPaint.measureText(finalFundMode.time+""))/2,top+reactTextMargin_bottom+getFontHeight(mLongPressTextSize, mLongPressPaint) /2 , mLongPressPaint);
}
public Paint getRectBGPaint() {
Paint paint =new Paint();
paint.setColor(Color.BLACK);
paint.setAntiAlias(true);
paint.setStrokeWidth(mInnerXStrokeWidth);
paint.setAlpha(85);
paint.setStyle(Paint.Style.FILL);
return paint;
}
//找到最大时间、最小时间和中间时间显示即可
private void drawXPaint(Canvas canvas) {
if(apiResult.ext.xlist.size()>=6){
String firstX=apiResult.ext.xlist.get(0);
String secondX=apiResult.ext.xlist.get(1);
String thirdX=apiResult.ext.xlist.get(2);
String forthX=apiResult.ext.xlist.get(3);
String fifthX=apiResult.ext.xlist.get(4);
String sixX=apiResult.ext.xlist.get(5);
//x轴文字的高度
halfTextWidth_X=mXYPaint.measureText(firstX)/2;
float hight = mHeight - mPaddingBottom + xBottomTopPadding;
canvas.drawText(firstX,
mPaddingLeft-halfTextWidth_X,
hight+textTopPadding, mXYPaint);
canvas.drawText(secondX,
mPaddingLeft + (mWidth - mPaddingLeft - mPaddingRight) /6-halfTextWidth_X,
hight+textTopPadding, mXYPaint);
canvas.drawText(thirdX,
mPaddingLeft + (mWidth - mPaddingLeft - mPaddingRight) /3f-halfTextWidth_X,
hight+textTopPadding, mXYPaint);
canvas.drawText(forthX,
mPaddingLeft + (mWidth - mPaddingLeft - mPaddingRight) /2-halfTextWidth_X,
hight+textTopPadding, mXYPaint);
canvas.drawText(fifthX,
mPaddingLeft + (mWidth - mPaddingLeft - mPaddingRight)*2 /3-halfTextWidth_X,
hight+textTopPadding, mXYPaint);
canvas.drawText(sixX,
mPaddingLeft + (mWidth - mPaddingLeft - mPaddingRight)*5/6-halfTextWidth_X,
hight+textTopPadding, mXYPaint);
}
}
private void drawYPaint(Canvas canvas) {
//现将最小值、最大值画好
//draw min
if(apiResult.ext.ylist.size()>=4){
String firstY=apiResult.ext.ylist.get(0);
String secondY=apiResult.ext.ylist.get(1);
String thirdY=apiResult.ext.ylist.get(2);
String forthY=apiResult.ext.ylist.get(3);
float txtWigth = mXYPaint.measureText(firstY) ;
float perYWidth = (mHeight - mPaddingBottom - mPaddingTop) /3;
canvas.drawText(AppConfig.formatNum(firstY),
mPaddingLeft - txtWigth- yTextRightPadding,
mHeight - mPaddingBottom, mXYPaint);
//draw max
canvas.drawText(AppConfig.formatNum(secondY),
mPaddingLeft - txtWigth- yTextRightPadding,
mPaddingTop+perYWidth*2+getFontHeight(mXYTextSize, mXYPaint) /4, mXYPaint);
canvas.drawText(AppConfig.formatNum(thirdY),
mPaddingLeft - txtWigth- yTextRightPadding,
mPaddingTop+perYWidth+getFontHeight(mXYTextSize, mXYPaint) /4, mXYPaint);
canvas.drawText(AppConfig.formatNum(forthY),
mPaddingLeft - txtWigth- yTextRightPadding,
mPaddingTop+getFontHeight(mXYTextSize, mXYPaint) /4, mXYPaint);
}
}
private void showLongPressView() {
mDrawLongPressPaint =true;
invalidate();
}
private void hiddenLongPressView() {
//实现蚂蚁金服延迟消失十字线
postDelayed(new Runnable() {
@Override
public void run() {
mDrawLongPressPaint =false;
invalidate();
}
},200);
}
// 只需要把画笔颜色置为透明即可
private void hiddenLoadingPaint() {
mLoadingPaint.setColor(0x00000000);
mDrawLoadingPaint =false;
}
private void showLoadingPaint() {
mLoadingPaint.setColor(getColor(R.color.gray));
mDrawLoadingPaint =true;
}
private int getColor(@ColorResint colorId) {
return getResources().getColor(colorId);
}
private float convertDp2Px(float dpValue) {
final float scale = getContext().getResources().getDisplayMetrics().density;
return (dpValue * scale +0.5f);
}
public float getFontHeight(float fontSize, Paint paint) {
paint.setTextSize(fontSize);
Paint.FontMetrics fm = paint.getFontMetrics();
return (float) (Math.ceil(fm.descent - fm.top) +2);
}
/**
* 程序入口,设置数据
*/
public void setData(ApiResult2, XYEntity> apiResult) {
this.apiResult = apiResult;
if (apiResult ==null || apiResult.data.size() ==0) {
showLoadingPaint();
invalidate();
}else {
if(apiResult.ext.ylist.size()>=4){
mPaddingLeft= mXYPaint.measureText(apiResult.ext.ylist.get(3))+yTextPadding;
maxY=Float.parseFloat(apiResult.ext.ylist.get(3));
minY=Float.parseFloat(apiResult.ext.ylist.get(0));
}
mPerX = (mWidth - mPaddingLeft - mPaddingRight) / (apiResult.data.size()-1);
mPerY = ((mHeight - mPaddingTop - mPaddingBottom) / (maxY - minY));
pointCount=(apiResult.data.size()-1)/24;
//数据过来,隐藏加载更多
hiddenLoadingPaint();
//刷新界面
invalidate();
}
}
}
3、总结
自定义K_View中代码没做封装提取,理解起来相对容易。具体内容都是一些尺寸计算,看起来有点烧脑,但是静下心来慢慢咀嚼,会发现其实整个实现过程非常简单。在这基础上朋友们可以根据需求随意改动,实现更丰富多彩的效果。