android记账本折线图_Android自定义View - 仿支付宝月账单折线图

本文介绍了如何在Android中实现一个仿支付宝月账单的自定义折线图View。通过分析折线图的组成部分,如月份文字、折线、数据点和消费金额文字,然后利用Canvas的drawText、drawPath和drawCircle方法来绘制。文章详细阐述了计算各部分坐标的过程,并提到了绘制细节,如实心和空心圆的区别以及虚线的绘制方法。
摘要由CSDN通过智能技术生成

前言

支付宝有个查看月账单的功能,最近一直在学习自定义View,于是就尝试着自己实现了一个类似的折线图。

下面是支付宝消费分析功能截图和自己实现的折线效果截图:

6b5383c9a186

支付宝消费分析折线图.jpg

6b5383c9a186

效果1.jpg

6b5383c9a186

效果2.jpg

6b5383c9a186

效果3.jpg

确定绘制内容

在绘制折线图之前首先要分析折线图需要绘制哪些部分以及如何绘制这些部分,确定了各绘制部分及绘制方法之后再开始具体的绘制。

6b5383c9a186

支付宝消费分析折线图_v10.0.15.jpg

1.确定需要绘制的区域

将折线图分为四个绘制区域:

月份文字

折线的边

折线数据点处的圆

消费金额文字

2.如何绘制相应的区域

月份文字和消费金额文字

Android 系统提供了 Canvas.drawText 方法用于绘制文字,我们只需要使用该方法绘制文字即可。关于该方法可以参考 Android中canvas.drawText参数的介绍以及绘制一个文本居中的案例这篇文章 ,网上还有很多介绍该方法的文章,感兴趣的同学请自行搜索。

public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint)

折线的边

折线的边使用 Canvas.drawPath方法绘制,如果有不了解这个方法中第二个参数Path含义和用法可以参考这篇文章 Path从懵逼到精通——基本操作,读完这篇文章基本可以掌握Path的相关操作。

public void drawPath(@NonNull Path path, @NonNull Paint paint)

折线数据点处的圆

Android提供下面的方法用于绘制圆形,如果有不了解这个方法的同学还请自行搜索学习。

public void drawCircle(float cx, float cy, float radius, @NonNull Paint paint)

现在已经知道了折线需要绘制哪些部分以及各部分的绘制方法,接下来要做的就是确定绘制折线各部分所需要的点的坐标。

计算绘制点坐标

1.确定折线数据点x坐标

从下面绘制分析图可以直观地看到月份文字中间点x坐标,数据点x坐标,数据点处圆的圆心x坐标以及消费金额文字中间点x坐标的值是相同的,所以只需要确定数据点的x坐标就行了。

6b5383c9a186

支付宝折线图绘制分析.png

观察支付宝的折线图可以看到第一个数据点距离屏幕左侧和最后一个数据点距离屏幕右侧的宽度是相等的,我们将这个宽度定义为基准宽度mBaseWidth。

将相邻两个数据点之间的宽度定义为mItemWidth,仔细观察可以看出这个宽度大概为 3*mBaseWidth,因为折线图固定显示5个月份的数据,所以mBaseWidth值就确定了:

// mWidth为当前整个View的显示宽度

// mWidth = 2 * mBaseWidth + 4 * mItemWidth = 14 * mBaseWidth;

mBaseWidth = mWidth / 14;

mItemWidth = mBaseWidth * 3;

得到mBaseWidth值后各个数据点的x坐标也就确定了:

for (int i = 0; i < pointCount; i++) {

float dx = mBaseWidth + mItemWidth * i;

}

2.确定各绘制点y坐标

从支付宝折线图上可以看到月份的y坐标是固定的,数据点的y坐标是通过消费金额确定的,圆心y坐标和消费金额文字y坐标都依赖于数据点的y坐标。

下面我们就来分别确定月份的y坐标和数据点的y坐标。在绘制分析图中定义了几个参考变量,各变量的含义如下:

/**

* 数据点最低y轴坐标

*/

private float mMinLineHeight;

/**

* 数据点最高y轴坐标

*/

private float mMaxLineHeight;

/**

* 折线最大可占用空间的高度,即折线波峰和波谷的差值,用来控制折线的陡峭程度

*/

private float mMaxLineSpace;

月份文字y坐标

从绘制分析图可以看到月份的y坐标范围在区间 [3/4*mHeight, mHeight] 之间,只要以 3/4*mHeight 为基准值加上一个偏移量就可得到月份的y轴坐标,那么如何确定这个偏移量的大小呢?

首先通过下面的方法计算月份文字的高度:

Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();

float fontHeight = fontMetrics.descent - fontMetrics.ascent;

......

float dy = mMaxLineHeight + fontHeight * 3 / 2;

计算出文字高度后选取 fontHeight * 3 / 2 作为偏移量,让月份文字的顶部距离 3/4*mWidth 基准线的高度刚好为 fontHeight。这个偏移量的值不是固定的,如果对最后绘制效果不满意,可以调整该值到自己满意的效果。

数据点y坐标

依然先定义一些控制参数:

/**

* 已出账单月份中最低消费金额

*/

private double mMinBillValue;

/**

* 已出账单月份中最高消费金额

*/

private double mMaxBillValue;

/**

* 消费金额的极差

*/

private double mBillRange;

mMaxBillValue = Utils.getMaxBillValue(mBillValues);

mMinBillValue = Utils.getMinBillValue(mBillValues);

mBillRange = mMaxBillValue - mMinBillValue;

mBillRange = (mBillRange == 0 ? 1 : mBillRange);

先说数据点y坐标的确定方法,接下来再给出分析:

float dy = (float) (mMaxLineHeight - (mBillValues[i] - mMinBillValue) / mBillRange * mMaxLineSpace);

从计算公式可以看出数据点y坐标是通过mMaxLineHeight减去一个偏移量得到的,只需要确定偏移量的计算方法就可以了。

首先计算出当前月消费金额和最低消费金额的差值,然后用这个差值除以消费金额的极差mBillRange得到一个比例值,再用这个比例值去乘以mMaxLineSpace就是偏移值,然后用mMaxLineHeight减去偏移值就是数据点的y轴坐标。

数据点y坐标确定之后其它几个点的坐标也就确定了。圆心的y坐标就是数据点y坐标,消费金额文字y坐标通过数据点y坐标减去消费金额文字的 fontHeight * 3 / 2得到。

上面得到的仅仅是在账单金额已经出来情况下数据点的y坐标,那么月份的消费金额数还没出来时数据点y坐标该如何确定呢?因为只是为了演示效果,所以我采用下面的计算方法来得到未出账单月份的数据点y坐标。

for (int i = pointCount; i < totalMonths; i++) {

float dx = mBaseWidth + mItemWidth * i;

float dy = mMinLineHeight + mMaxLineSpace * (i - pointCount + 1) / (totalMonths - pointCount);

mPath.lineTo(dx, dy);

}

绘制细节

确定绘制点的坐标之后,剩下的就是一些需要注意的细节,比如已出账单月份数据点处的圆是实心的,未出账单月份数据点处的圆是空心的。还有数据点之间的连线有虚线和实线之分,实线的颜色也有不同,绘制的时候注意这些细节就可以了。

顺带介绍一下Android中虚线绘制是通过设置画笔的路径效果来实现的:

DashPathEffect dashPathEffect = new DashPathEffect(new float[]{20, 20}, 1);

mLinePaint.setPathEffect(dashPathEffect);

总结

折线图的目的是学习绘制自定义View,跟支付宝的实际逻辑肯定是完全不同的。另外目前项目并没有并没有在onMeasure进行特殊的绘制处理,等以后再优化改进。

更新

2017-06-20 增加onMeasure测量过程的处理 diff

package com.yzxy.draw; 002 003 import java.util.ArrayList; 004 import java.util.HashMap; 005 import java.util.Iterator; 006 import java.util.Map; 007 import java.util.Set; 008 009 import android.annotation.SuppressLint; 010 import android.content.Context; 011 import android.graphics.Canvas; 012 import android.graphics.Color; 013 import android.graphics.Paint; 014 import android.graphics.Path; 015 import android.graphics.Point; 016 import android.graphics.RectF; 017 import android.graphics.Paint.Style; 018 import android.graphics.Typeface; 019 import android.util.AttributeSet; 020 import android.view.MotionEvent; 021 import android.view.View; 022 023 /** 024 * 025 * @author zhangyayun1@gmail.com 026 * 027 */ 028 @SuppressLint("ViewConstructor") 029 class MyChartView extends View{ 030 031 public static final int RECT_SIZE = 10; 032 private Point mSelectedPoint; 033 034 //枚举实现坐标桌面的样式风格 035 public static enum Mstyle 036 { 037 Line,Curve 038 } 039 040 private Mstyle mstyle=Mstyle.Line; 041 private Point[] mPoints = new Point[100]; 042 043 Context context; 044 int bheight=0; 045 HashMap<Double, Double> map; 046 ArrayList<Double> dlk; 047 int totalvalue=30; 048 int pjvalue=5; 049 String xstr,ystr="";//横纵坐标的属性 050 int margint=15; 051 int marginb=40; 052 int c=0; 053 int resid=0; 054 Boolean isylineshow; 055 056 /** 057 * @param map 需要的数据,虽然key是double,但是只用于排序和显示,与横向距离无关 058 * @param totalvalue Y轴的最大值 059 * @param pjvalue Y平均值 060 * @param xstr X轴的单位 061 * @param ystr Y轴的单位 062 * @param isylineshow 是否显示纵向网格 063 * @return 064 */ 065 public void SetTuView(HashMap<Double, Double> map,int totalvalue,int pjvalue,String xstr,String ystr,Boolean isylineshow) 066 { 067 this.map=map; 068 this.totalvalue=totalvalue; 069 this.pjvalue=pjvalue; 070 this.xstr=xstr; 071 this.ystr=ystr; 072 this.isylineshow=isylineshow; 073 //屏幕横向 074 // act.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); 075 } 076 077 public MyChartView(Context ct) 078 { 079 super(ct); 080 this.context=ct; 081 } 082 083 public MyChartView(Context ct, AttributeSet attrs) 084 { 085 super( ct, attrs ); 086 this.context=ct; 087 } 088 089 public MyChartView(Context ct, AttributeSet attrs, int defStyle) 090 { 091 super( ct, attrs, defStyle ); 092 this.context=ct; 093 } 094 095 @SuppressLint("DrawAllocation") 096 @Override 097 protected void onDraw(Canvas canvas) { 098 super.onDraw(canvas); 099 if(c!=0) 100 this.setbg(c); 101 if(resid!=0) 102 this.setBackgroundResource(resid); 103 dlk=getintfrommap(map); 104 int height=getHeight(); 105 if(bheight==0) 106 bheight=height-marginb; 107 108 int width=getWidth(); 109 int blwidh=dip2px(context,50); 110 int pjsize=totalvalue/pjvalue;//界面布局的尺寸的比例 111 // set up paint 112 Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); 113 paint.setColor(Color.GRAY); 114 paint.setStrokeWidth(1); 115 paint.setStyle(Style.STROKE); 116 for(int i=0;i<pjsize+1;i++)//将顶点的线变为红色的 警戒线 117 { 118 if(i==pjsize) 119 paint.setColor(Color.RED); 120 canvas.drawLine(blwidh,bheight-(bheight/pjsize)*i+margint,width,bheight-(bheight/pjsize)*i+margint, paint);//Y坐标 121 drawline(pjvalue*i+ystr, blwidh/2, bheight-(bheight/pjsize)*i+margint, canvas); 122 } 123 ArrayList<Integer> xlist=new ArrayList<Integer>();//记录每个x的值 124 //画直线(纵向) 125 paint.setColor(Color.GRAY); 126 if(dlk==null) 127 return; 128 for(int i=0;i<dlk.size();i++) 129 { 130 xlist.add(blwidh+(width-blwidh)/dlk.size()*i); 131 if(isylineshow) 132 { 133 canvas.drawLine(blwidh+(width-blwidh)/dlk.size()*i,margint,blwidh+(width-blwidh)/dlk.size()*i,bheight+margint, paint); 134 } 135 drawline(dlk.get(i)+xstr, blwidh+(width-blwidh)/dlk.size()*i, bheight+40, canvas);//X坐标 136 } 137 138 //点的操作设置 139 mPoints=getpoints(dlk, map, xlist, totalvalue, bheight); 140 // Point[] ps=getpoints(dlk, map, xlist, totalvalue, bheight); 141 // mPoints=ps; 142 143 paint.setColor(Color.GREEN); 144 paint.setStyle(Style.STROKE); 145 paint.setStrokeWidth(0); 146 147 if(mstyle==Mstyle.Curve) 148 drawscrollline(mPoints, canvas, paint); 149 else 150 drawline(mPoints, canvas, paint); 151 152 paint.setColor(Color.RED); 153 paint.setStyle(Style.FILL); 154 for (int i=0; i<mPoints.length; i++) 155 { 156 canvas.drawRect(pointToRect(mPoints[i]),paint); 157 } 158 } 159 160 @Override 161 public boolean onTouchEvent(MotionEvent event) 162 { 163 switch (event.getAction()) 164 { 165 case MotionEvent.ACTION_DOWN: 166 for (int i=0; i<mPoints.length; i++) 167 { 168 if (pointToRect(mPoints[i]).contains(event.getX(),event.getY())) 169 { 170 System.out.println("-yes-"+i); 171 mSelectedPoint = mPoints[i]; 172 } 173 } 174 break; 175 case MotionEvent.ACTION_MOVE: 176 if ( null != mSelectedPoint) 177 { 178 // mSelectedPoint.x = (int) event.getX(); 179 mSelectedPoint.y = (int) event.getY(); 180 // invalidate(); 181 } 182 break; 183 case MotionEvent.ACTION_UP: 184 mSelectedPoint = null; 185 break; 186 default: 187 break; 188 } 189 return true; 190 191 } 192 193 194 private RectF pointToRect(Point p) 195 { 196 return new RectF(p.x -RECT_SIZE/2, p.y - RECT_SIZE/2,p.x + RECT_SIZE/2, p.y + RECT_SIZE/2); 197 } 198 199 200 private void drawscrollline(Point[] ps,Canvas canvas,Paint paint) 201 { 202 Point startp=new Point(); 203 Point endp=new Point(); 204 for(int i=0;i<ps.length-1;i++) 205 { 206 startp=ps[i]; 207 endp=ps[i+1]; 208 int wt=(startp.x+endp.x)/2; 209 Point p3=new Point(); 210 Point p4=new Point(); 211 p3.y=startp.y; 212 p3.x=wt; 213 p4.y=endp.y; 214 p4.x=wt; 215 216 Path path = new Path(); 217 path.moveTo(startp.x,startp.y); 218 path.cubicTo(p3.x, p3.y, p4.x, p4.y,endp.x, endp.y); 219 canvas.drawPath(path, paint); 220 221 } 222 } 223 224 225 private void drawline(Point[] ps,Canvas canvas,Paint paint) 226 { 227 Point startp=new Point(); 228 Point endp=new Point(); 229 for(int i=0;i<ps.length-1;i++) 230 { 231 startp=ps[i]; 232 endp=ps[i+1]; 233 canvas.drawLine(startp.x,startp.y,endp.x,endp.y, paint); 234 } 235 } 236 237 238 private Point[] getpoints(ArrayList<Double> dlk,HashMap<Double, Double> map,ArrayList<Integer> xlist,int max,int h) 239 { 240 Point[] points=new Point[dlk.size()]; 241 for(int i=0;i<dlk.size();i++) 242 { 243 int ph=h-(int)(h*(map.get(dlk.get(i))/max)); 244 245 points[i]=new Point(xlist.get(i),ph+margint); 246 } 247 return points; 248 } 249 250 251 private void drawline(String text,int x,int y,Canvas canvas) 252 { 253 Paint p = new Paint(); 254 p.setAlpha(0x0000ff); 255 p.setTextSize(20); 256 String familyName = "宋体"; 257 Typeface font = Typeface.create(familyName,Typeface.ITALIC); 258 p.setTypeface(font); 259 p.setTextAlign(Paint.Align.CENTER); 260 canvas.drawText(text, x, y, p); 261 } 262 263 264 public int dip2px(Context context, float dpValue) 265 { 266 final float scale = context.getResources().getDisplayMetrics().density; 267 return (int) (dpValue * scale + 0.5f); 268 } 269 270 271 public int px2dip(Context context, float pxValue) 272 { 273 final float scale = context.getResources().getDisplayMetrics().density; 274 return (int) (pxValue / scale + 0.5f); 275 } 276 277 278 279 @SuppressWarnings("rawtypes") 280 public ArrayList<Double> getintfrommap(HashMap<Double, Double> map) 281 { 282 ArrayList<Double> dlk=new ArrayList<Double>(); 283 int position=0; 284 if(map==null) 285 return null; 286 Set set= map.entrySet(); 287 Iterator iterator = set.iterator(); 288 289 while(iterator.hasNext()) 290 { 291 @SuppressWarnings("rawtypes") 292 Map.Entry mapentry = (Map.Entry)iterator.next(); 293 dlk.add((Double)mapentry.getKey()); 294 } 295 for(int i=0;i<dlk.size();i++) 296 { 297 int j=i+1; 298 position=i; 299 Double temp=dlk.get(i); 300 for(;j<dlk.size();j++) 301 { 302 if(dlk.get(j)<temp) 303 { 304 temp=dlk.get(j); 305 position=j; 306 } 307 } 308 309 dlk.set(position,dlk.get(i)); 310 dlk.set(i,temp); 311 } 312 return dlk; 313 314 } 315 316 317 318 319 public void setbg(int c) 320 { 321 this.setBackgroundColor(c); 322 } 323 324 public HashMap<Double, Double> getMap() { 325 return map; 326 } 327 328 public void setMap(HashMap<Double, Double> map) { 329 this.map = map; 330 } 331 332 public int getTotalvalue() { 333 return totalvalue; 334 } 335 336 public void setTotalvalue(int totalvalue) { 337 this.totalvalue = totalvalue; 338 } 339 340 public int getPjvalue() { 341 return pjvalue; 342 } 343 344 public void setPjvalue(int pjvalue) { 345 this.pjvalue = pjvalue; 346 } 347 348 public String getXstr() { 349 return xstr; 350 } 351 352 public void setXstr(String xstr) { 353 this.xstr = xstr; 354 } 355 356 public String getYstr() { 357 return ystr; 358 } 359 360 public void setYstr(String ystr) { 361 this.ystr = ystr; 362 } 363 364 public int getMargint() { 365 return margint; 366 } 367 368 public void setMargint(int margint) { 369 this.margint = margint; 370 } 371 372 public Boolean getIsylineshow() { 373 return isylineshow; 374 } 375 376 public void setIsylineshow(Boolean isylineshow) { 377 this.isylineshow = isylineshow; 378 } 379 380 public int getMarginb() { 381 return marginb; 382 } 383 384 public void setMarginb(int marginb) { 385 this.marginb = marginb; 386 } 387 388 public Mstyle getMstyle() { 389 return mstyle; 390 } 391 392 public void setMstyle(Mstyle mstyle) { 393 this.mstyle = mstyle; 394 } 395 396 public int getBheight() { 397 return bheight; 398 } 399 400 public void setBheight(int bheight) { 401 this.bheight = bheight; 402 } 403 404 public int getC() { 405 return c; 406 } 407 408 public void setC(int c) { 409 this.c = c; 410 } 411 412 public int getResid() { 413 return resid; 414 } 415 416 public void setResid(int resid) { 417 this.resid = resid; 418 } 419 420 421 422 }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值