Android实现轻量线性与百分比图表的方法

前言

经常要用到图表统计数据,在WEB开发中图表绘制是一件简单的事情,因为有比较多的开源方案。但在Android中开源方案并不多。但目前github上有多个关于图表的框架,比如MPAndroidChart很好,但是很大,没必要因为一个小的图标让工程项目扩大很多,另外有些轻量级的框架,但是个人感觉都很难满足自己的需求,再者就算很好的框架,那也是别人的,只有自己动手写起来,了解前前后后的坑,自己才能成长,而且在写的过程,我们能发现更多的细节,比如绘制的时候内存分配的问题,Canvas直接绘制和通过Bitmap绘制等等,所以这篇文章的目的:

1.是给大家提供自定义view绘制的思路

2.滑动自定义view的部分区域怎么实现

3.path动画绘制的实现

4.熟悉canvas的api,总之能直接动手了,那就自定义view就通关了,所以就写这篇文章主要是鼓励大家多去实现。

效果图

线性图表实现的思路:

线性表是最基本、最简单、也是最常用的一种数据结构。线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的,注意,这句话只适用大部分线性表,而不是全部。

由于屏幕的宽度有限,所以我们一屏经过计算,最好显示的7个点,所以我们首先需要对我们的view宽度进行计算,首先拿到屏幕的宽度,然后再进行/7,得到每个间隔的宽度,然后乘以我们x的坐标点的个数,其中的onMeasure的方法:

 int widthParentMeasureMode = MeasureSpec.getMode(widthMeasureSpec);
 int widthParentMeasureSize = MeasureSpec.getSize(widthMeasureSpec);
 int heightParentMeasureMode = MeasureSpec.getMode(heightMeasureSpec);
 int heightParentMeasureSize = MeasureSpec.getSize(heightMeasureSpec);
 int resultWidthSize = 0;
 int resultHeightSize = 0;
 int resultWidthMode = MeasureSpec.EXACTLY;//用来对childView进行计算的
 int resultHeightMode = MeasureSpec.EXACTLY;
 int paddingWidth = getPaddingLeft() + getPaddingRight();
 int paddingHeight = getPaddingTop() + getPaddingBottom();
 ViewGroup.LayoutParams thisLp = getLayoutParams();
 switch (widthParentMeasureMode) {
  //父类不加限制给子类
  case MeasureSpec.UNSPECIFIED:
   //这个代表在布局写死了宽度
   if (thisLp.width > 0) {
    resultWidthSize = thisLp.width;
    resultWidthMode = MeasureSpec.EXACTLY;
   } else {
    resultWidthSize = (int) (getYMaxTextWidth() + mXinterval * mXdots.length);
    resultWidthMode = MeasureSpec.UNSPECIFIED;
   }
   break;
  case MeasureSpec.AT_MOST:
   //这个代表在布局写死了宽度
   if (thisLp.width > 0) {
    resultWidthSize = thisLp.width;
    resultWidthMode = MeasureSpec.EXACTLY;
   } else if (thisLp.width == ViewGroup.LayoutParams.MATCH_PARENT) {
    resultWidthSize = Math.max(0, widthParentMeasureSize - paddingWidth);
    resultWidthMode = MeasureSpec.AT_MOST;
   } else if (thisLp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
    resultWidthSize = (int) (getYMaxTextWidth() + mXinterval * mXdots.length);
    resultWidthMode = MeasureSpec.AT_MOST;
   }
   break;
  case MeasureSpec.EXACTLY:
   //这个代表在布局写死了宽度
   if (thisLp.width > 0) {
    resultWidthSize = Math.min(widthParentMeasureSize, thisLp.width);
    resultWidthMode = MeasureSpec.EXACTLY;
   } else if (thisLp.width == ViewGroup.LayoutParams.MATCH_PARENT) {
    resultWidthSize = widthParentMeasureSize;
    resultWidthMode = MeasureSpec.EXACTLY;
   } else if (thisLp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
    resultWidthSize = (int) (getYMaxTextWidth() + mXinterval * mXdots.length);
    resultWidthMode = MeasureSpec.AT_MOST;
   }
   break;
 }
 switch (heightParentMeasureMode) {
  //父view不加限制
  case MeasureSpec.UNSPECIFIED:
   //这个代表在布局写死了宽度
   if (thisLp.height > 0) {
    resultHeightSize = thisLp.height;
    resultHeightMode = MeasureSpec.EXACTLY;
   } else {
    resultHeightSize = (int) (getYMaxTextHeight() + mYvisibleNum * mYinterval + getXMaxTextHeight());
    resultHeightMode = MeasureSpec.UNSPECIFIED;
   }
   break;
  case MeasureSpec.AT_MOST:
   if (thisLp.height > 0) {
    resultHeightSize = heightParentMeasureSize;
    resultHeightMode = MeasureSpec.EXACTLY;
   } else if (thisLp.height == ViewGroup.LayoutParams.MATCH_PARENT) {
    resultHeightSize = Math.max(0, heightParentMeasureSize - paddingHeight);
    resultHeightMode = MeasureSpec.AT_MOST;
   } else if (thisLp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
    resultHeightSize = (int) (getYMaxTextHeight() + mYvisibleNum * mYinterval + getXMaxTextHeight());
    resultHeightMode = MeasureSpec.UNSPECIFIED;
   }
   break;
  case MeasureSpec.EXACTLY:
   //这个代表在布局写死了宽度
   if (thisLp.height > 0) {
    resultHeightSize = Math.min(heightParentMeasureSize, getMeasuredWidth());
    resultHeightMode = MeasureSpec.EXACTLY;
   } else if (thisLp.width == ViewGroup.LayoutParams.MATCH_PARENT) {
    resultHeightSize = heightParentMeasureSize;
    resultHeightMode = MeasureSpec.EXACTLY;
   } else if (thisLp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
    resultHeightSize = (int) (getYMaxTextHeight() + mYvisibleNum * mYinterval + getXMaxTextHeight());
    resultHeightMode = MeasureSpec.AT_MOST;
   }
   break;
 }
 setMeasuredDimension(MeasureSpec.makeMeasureSpec(resultWidthSize, resultWidthMode),
   MeasureSpec.makeMeasureSpec(resultHeightSize, resultHeightMode));

设置好了尺寸,我们就可以绘制界面,这里我们onDraw的时候,就依次绘制横线和竖线,在绘制横线的时候,将Y坐标的数字一起绘制上去,同理绘制竖线的时候,把x坐标的数字绘制上去,折线的画根据数字计算出坐标点,然后创建一个path,首先moveTo(firstX,firstY) ,然后lineTo下面的点就可以了,最后绘制上path,然而这样的话,我们在滑动的时候,会发现这个view都会跟着一起滚动了,那么我们怎样才能实现view的部分pinned呢?在这个时候,我们就需要先创建一个bitmap,将需要滑动的部分绘制到这个bitmap上去,然后bitmap在绘制到这个canvas上的时候,保持固定的位置就行了,好了再说就懵逼了,还是上代码吧:

 float tempTableLeftPadding = getYMaxTextWidth();
 if (mBitmap == null || mYNumCanvas == null) {
  mBitmap = Bitmap.createBitmap((int) (getMeasuredWidth() - getYMaxTextWidth()), getMeasuredHeight(), Bitmap.Config.ARGB_8888);
  mYNumCanvas = new Canvas(mBitmap);
 }
 mYNumCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
 mYNumCanvas.translate(mScrollPosX,0);//这段代码就是来实现滑动的操作

 //绘制横线
 for (int y = 0, size = mYdots.length; y < size; y++) {
  String tempText = String.valueOf(mYdots[mYdots.length - 1 - y]);
  mYNumCanvas.drawLine(0, (float) (mYinterval * y), (float) (mXdots.length * mXinterval), (float) (mYinterval * y), mXlinePaint);
  canvas.drawText(tempText, getYMaxTextWidth() - mYNumPaint.measureText(tempText), getYMaxTextHeight() + (float) (mYinterval * y), mYNumPaint);
 }
 //绘制竖线
 for (int x = 0, size = mXdots.length; x <= size; x++) {
  mYNumCanvas.drawLine((float) (mXinterval * x), 0, (float) (mXinterval * x), (float) (mYinterval * mYvisibleNum), mXlinePaint);
  if (x >= 1) {
   String tempText = mXdots[x - 1];
   mYNumCanvas.drawText(tempText, (float) (mXinterval * x) - mYNumPaint.measureText(tempText) / 2, (float) (mYvisibleNum * mYinterval + getYMaxTextHeight()), mYNumPaint);
  }
 }
 if (isAnimationOpen)//是否需要开启动画绘制,这个后面会解释实现方式
  mYNumCanvas.drawPath(mLineDrawPath, mLinePaint);
 else
  mYNumCanvas.drawPath(mLinePath, mLinePaint);
 canvas.drawBitmap(mBitmap, tempTableLeftPadding, getYMaxTextHeight() / 2, null);

上面的mScrollPosX是根据手势监听类GestureDetector来获取的:

@Override
public boolean onTouchEvent(MotionEvent event) {
 if (!isAnimationOpen || isDrawOver)
  return mGestureDetector.onTouchEvent(event);
 return super.onTouchEvent(event);
}

然而绘制了,我们感觉还缺少了什么,嗯,没错就是动画效果,这里我们用到通过的path绘制实现动画的方案,就是先通过PathMeasure得到path的长度,然后根据动画时间,通过ValueAnimator计算它在某个时刻的坐标,然后重新进行绘制path路径:

private void startPathAnim(long duration) {
 ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, mLineLength);
 valueAnimator.setDuration(duration);
 valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
  @Override
  public void onAnimationUpdate(ValueAnimator animation) {
   float value = (Float) animation.getAnimatedValue();
   // 获取当前点坐标封装到mCurrentPosition
   mPathMeasure.getPosTan(value, mCurrentPosition, null);
   mLineDrawPath.lineTo(mCurrentPosition[0], mCurrentPosition[1]);
   invalidate();
  }
 });
 valueAnimator.start();
}

百分比圆形图表实现

其实这个的实现,相比上一个少了很多,大多是集中在onDraw方法里面,关键点是在百分比的数字,怎么横向显示在扇形区域,这里我就主要这个计算规则提出来:

private void drawText(Canvas canvas, float sweepAngle, float startAngle, ArcVo temp) {
 float middleAngle;
 middleAngle = startAngle + sweepAngle / 2;
 float startX;
 float startY;
 float endX;
 float endY;
 String drawText = temp.getPercentInCircle() * 100 + "%";
 if (middleAngle <= 90) {
  //在第四象限
  double angle = middleAngle;
  angle = Math.toRadians(angle);
  startY = endY = (float) (Math.sin(angle) * mRaduis + mRaduis);
  endX = (float) (mRaduis + Math.cos(angle) * mRaduis);
  startX = endX - UiUtils.getTextWidth(drawText, mTextPaint);
 } else if (middleAngle <= 180) {
  //在第三象限
  double angle = 180 - middleAngle;
  angle = Math.toRadians(angle);
  startY = endY = (float) (Math.sin(angle) * mRaduis + mRaduis);
  startX = (float) (mRaduis - Math.cos(angle) * mRaduis);
  endX = startX + UiUtils.getTextWidth(drawText, mTextPaint);
 } else if (middleAngle <= 270) {
  //在第二象限
  double angle = 270 - middleAngle;
  angle = Math.toRadians(angle);
  startY = endY = (float) (mRaduis - Math.cos(angle) * mRaduis);
  startX = (float) (mRaduis - Math.sin(angle) * mRaduis);
  endX = startX + UiUtils.getTextWidth(drawText, mTextPaint);
 } else {
  //在第一象限
  double angle = 360 - middleAngle;
  angle = Math.toRadians(angle);
  startY = endY = (float) (mRaduis - Math.sin(angle) * mRaduis);
  endX = (float) (mRaduis + Math.cos(angle) * mRaduis);
  startX = endX - UiUtils.getTextWidth(drawText, mTextPaint);
 }

 mTextPath.reset();
 mTextPath.moveTo(startX, startY);
 mTextPath.lineTo(endX, endY);
 if (middleAngle > 180) {
  canvas.drawTextOnPath(drawText, mTextPath, 0, UiUtils.getTextHeight(drawText, mTextPaint), mTextPaint);
 } else {
  canvas.drawTextOnPath(drawText, mTextPath, 0, -UiUtils.getTextHeight(drawText, mTextPaint), mTextPaint);

 }
}
 @Override
protected void onDraw(Canvas canvas) {
 if (!canDraw()) return;
 float sweepAngle;
 float startAngle = 0;
 for (int i = 0, size = mDisArcList.size(); i < size; i++) {
  ArcVo temp = mDisArcList.get(i);
  mArcPaint.setColor(temp.getScanColor());
  sweepAngle = temp.getPercentInCircle() * 360;
  canvas.drawArc(mDrawCircleRect, startAngle, sweepAngle, true, mArcPaint);
  drawText(canvas, sweepAngle, startAngle, temp);
  startAngle = startAngle + sweepAngle;
 }
}

使用方式:

如果你觉得你们的项目正好要用到类似的图标,在项目的gradle文件中,增加compile 'wellijohn.org.simplelinechart:linechart:0.0.2'具体的方法,欢迎移步到github上去看,已经封装成库上传至jcenter,上面有具体的使用方法(图表地址),目前暴露的方法不多,可以留言增加

github地址:https://github.com/WelliJohn/LineChart

本地下载:http://xiazai.jb51.net/201712/yuanma/LineChart(jb51.net).rar

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • android 线性布局LinearLayout实例代码

    布局文件:res/layout/activity_my.xml 复制代码 代码如下: [html]  <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"     xmlns:tools="http://schemas.android.com/tools"     android:id="@+id/LinearLayout"     android:

  • android Activity线性布局和表格布局实例讲解

    实验中只需要编写相应的xml的代码,java代码不需要更改,因为我们这里只是练习android的界面设计. 线性布局:线性布局就是将各种控件按照行或者列依次进行排列.其中本实验用到的各控件的属性解释如下:android:layout_weight属性是指不同的控件在activity中占有体积大小的比例.android:paddingLeft指内边距左的距离,即控件内文字离控件左边边界的距离.其它的类推.android:gravity指控件内文字相对于控件本身的方向属性,长度为dip,与像素独立的

  • 一个酷炫的Android图表制作框架

    一.概述 最近项目中需要制作柱形图以及折线图,所以便在网上搜索了一下这方面的开源框架,最后找到了这个酷炫的框架,不仅支持各种各样的图形制作,包括折线图.柱形图.饼状图等,而且提供了丰富的API接口,等着你去自定义,只要花点心思便能 DIY 出你心仪的图表类型,使用起来也是相当的简单. 从效果图可以看到,这个框架是相当酷炫的啊,在这里附上该框架的github地址hellocharts-android,有兴趣的不妨去 star 一下 二.炫酷的柱形图 可以看到柱形图也是能玩出花样来的,绚丽的色彩,自

  • Android UI组件LinearLayout线性布局详解

    LinearLayout 线性布局,该布局的继承关系: 1. 什么是线性布局 通俗的说感觉起来和线有关,参照线的特点,有么是横向的,要么是竖向的. LinearLayout是线性布局控件,它包含的子控件将以横向或竖向的方式排列(通过android:orientation属性来控制),按照相对位置来排列所有的widgets或者其他的containers,超过边界时,某些控件将缺失或消失 2. 线性布局常用基本属性 - android:id - android:orientation - andro

  • Android 百分比布局详解及实例代码

    Android 百分比布局 1.引入:compile 'com.android.support:percent:24.0.0' 2.点开源码可以看到,主要有两个布局类PercentFrameLayout和PercentRelativeLayout,一个工具类PercentLayoutHelper. 3.点开布局类比如PercentRelativeLayout的源码,可以看到实现的很简单. public class PercentRelativeLayout extends RelativeLay

  • Android中使用achartengine生成图表的具体方法

    今天在做项目的时候用到了图表功能,记录下来 achartengine是google的一个开源项目,可以在https://code.google.com/p/achartengine/ 下载技术文档,jar包以及项目源代码 demo下载:https://code.google.com/p/achartengine/downloads/list 一.饼状图 新建工程,添加achartengine  jar包 PieChart.java 复制代码 代码如下: package com.meritit.f

  • Android自定义View圆形百分比控件(一)

    做一个自定义View的小练习,效果如下 只需要画一个圆.一个圆弧.一个百分比文本,添加一个点击事件,传入百分比重绘 1.在res/values文件夹下新建attrs.xml文件,编写自定义属性: <?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="CirclePercentView" > <attr nam

  • Android布局之LinearLayout线性布局

    LinearLayout是线性布局控件:要么横向排布,要么竖向排布 常用属性: android:gravity------------设置的是控件自身上面的内容位置 android:layout_gravity-----设置控件本身相对于父控件的显示位置 android:layout_weight----- 给控件分配剩余空间 先给大家展示一下导图: 知识点详解(演示效果方便组件没有设置id) (1)gravity和Layout_gravity android:gravity 属性是对该view

  • Android RecyclerView线性布局详解(1)

    RecyclerView是Android 5.0新增的控件,在android-support-v7下面.官方文档对RecycleView介绍很简洁到位,如下: A flexible view for providing a limited window into a large data set. 大概意思就是说:在有限大小的窗口里显示大量数据的一个灵活的view. 下面是ReccleView继承图: 看到这里我们自然想到了与之类似的控件ListView,RecyclerView和ListVie

  • Android实现轻量线性与百分比图表的方法

    前言 经常要用到图表统计数据,在WEB开发中图表绘制是一件简单的事情,因为有比较多的开源方案.但在Android中开源方案并不多.但目前github上有多个关于图表的框架,比如MPAndroidChart很好,但是很大,没必要因为一个小的图标让工程项目扩大很多,另外有些轻量级的框架,但是个人感觉都很难满足自己的需求,再者就算很好的框架,那也是别人的,只有自己动手写起来,了解前前后后的坑,自己才能成长,而且在写的过程,我们能发现更多的细节,比如绘制的时候内存分配的问题,Canvas直接绘制和通过B

  • Android开发中使用achartengine绘制各种图表的方法

    本文实例讲述了Android开发中使用achartengine绘制各种图表的方法.分享给大家供大家参考,具体如下: 1. ABarChart.java package com.anjoyo.achartengine; import java.util.Random; import org.achartengine.ChartFactory; import org.achartengine.chart.BarChart.Type; import org.achartengine.model.Cat

  • 详解Android轻量型数据库SQLite

    数据库是Android存储方案的核心,在Andorid中SQLite非常轻量,而且执行sql语句甚至比MySQL还要快. SQLiteDatabase 是 Android 中操作数据库的核心类之一,使用SQLiteDatabase可以打开数据库,也可以对数据库进行操作,然而,为了数据库升级以及使用更加方便,我们常用SQLiteOpenHelper的子类来完成创建,打开数据库的操作. SQLiteOpenHelper是一个抽象类,在该类中有下面两个必须实现的方法: public void onCr

  • Android中给fragment写入参数的轻量开发包FragmentArgs简介

    Android开发有时候会令人头痛.你不得不为诸如建立fragment这样简单的事情写很多代码.幸运的是java支持一个强大的工具:注释处理器(Annotation Processors). Fragment的问题是你不得不设置很多参数,从而让它正常运行.很多android开发新手通常这样写: 复制代码 代码如下: public class MyFragment extends Fragment { private int id; private String title; public sta

  • 编写轻量ajax组件01-与webform平台上的各种实现方式比较

    前言 Asp.net WebForm 和 Asp.net MVC(简称MVC) 都是基于Asp.net的web开发框架,两者有很大的区别,其中一个就是MVC更加注重http本质,而WebForm试图屏蔽http,为此提供了大量的服务器控件和ViewState机制,让开发人员可以像开发Windows Form应用程序一样,基于事件模型去编程.两者各有优缺点和适用情景,但MVC现在是许多Asp.net开发者的首选. WebForm是建立在Asp.net的基础上的,Asp.net提供了足够的扩展性,我

  • js实现基于正则表达式的轻量提示插件

    本文实例讲述了基于正则表达式的轻量提示插件,分享给大家供大家参考.具体如下: 这是一款javascript实现基于正则表达式的轻量提示插件,本插件是基于正则表达式进行文本框检测的,通用性十分强,大家可以在实例中进行使用. 运行效果图:               -------------------查看效果------------------- 小提示:浏览器中如果不能正常运行,可以尝试切换浏览模式. 关键代码: $(document).ready(function () { $("#mess

  • 深入理解Vue.js轻量高效的前端组件化方案

    Vue.js通过简洁的API提供高效的数据绑定和灵活的组件系统.在前端纷繁复杂的生态中,Vue.js有幸受到一定程度的关注,目前在GitHub上已经有5000+的star.本文将从各方面对Vue.js做一个深入的介绍. Vue.js 是我在2014年2月开源的一个前端开发库,通过简洁的 API 提供高效的数据绑定和灵活的组件系统.在前端纷繁复杂的生态中,Vue.js有幸受到一定程度的关注,目前在 GitHub上已经有5000+的star.本文将从各方面对Vue.js做一个深入的介绍. 开发初衷

  • vue实现移动端轻量日期组件不依赖第三方库的方法

    不需要依赖第三方组件的vue日期移动端组件  小轮子 轻量可复用:   https://github.com/BeckReed/datepicker-for-vue 2.用法:参见 src/view/demo.vue 文件的用法,简单易懂 <div> <h3>三列(年月日)日期弹窗示例--带标题)</h3> <button class="blue-btn" @click="togglePicker2">显示三列带标题日

随机推荐