看到这个标题,可能有点发懵,啥叫多层折线图啊?这个是我自己取的名字,是因为那天我遇到了这样一个需求。

UI图.png

呐!这还是一个宝塔型的折线图,根据常识,很容易就知道这里面的交互逻辑:一指多控。曾经有一个华丽的需求摆在我的面前,我没有珍惜,后来出了bug被客户怼我才追悔莫及,如果上天能再给我一次机会的话,我一定要自己写一个出来。于是,就有了下面的效果。

效果图.gif

如果gif加载失败,请看这里~

折线图.jpg

这里面全部都是使用canvas绘制的,比如画折线canvas.drawPath,画圆点drawCircle,画坐标线canvas.drawLine,画文字canvas.drawText等等。代码注释写的也比较详细,就不一一介绍了。直接上代码:

import android.content.Context;

import android.graphics.Canvas;

import android.graphics.Color;

import android.graphics.Paint;

import android.graphics.Path;

import android.graphics.Point;

import android.support.annotation.Nullable;

import android.support.v4.content.ContextCompat;

import android.util.AttributeSet;

import android.util.Log;

import android.view.MotionEvent;

import android.view.View;

import java.util.ArrayList;

import java.util.Collections;

import java.util.Comparator;

import java.util.List;

/**

* 多层折线图控件

* Created by zhuyong on 2018/8/30.

*/

public class MyChatView extends View {

private Context mContext;

private Paint mPaintLine;//折线图

private Paint mPaintCircle;//圆的外边框

private Paint mPaintPoint;//圆内填充

private Paint mPaintBottomLine;//底部X轴

private Paint mPaintLimit;//指示线

private Paint mPaintText;//底部X坐标文字

private int mBottomTextHeight = 50;//底部X轴文字所占总高度,单位dp

private int mSingleLineHeight = 100;//单个折线图的高度,单位dp

private int mPaddingTB = 10;//折线图上下的偏移量,单位dp

private int mLineColor;//折线图的颜色

protected int[] mColors;//几种颜色

private List> mListAll = new ArrayList<>();//数据源

private int mViewWidth;//控件宽高

private int mViewHeight;//控件宽高

public MyChatView(Context context) {

this(context, null);

}

public MyChatView(Context context, @Nullable AttributeSet attrs) {

this(context, attrs, 0);

}

public MyChatView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {

super(context, attrs, defStyleAttr);

mContext = context;

initView();

}

/**

* 赋值

*

* @param list

*/

public void setData(List> list) {

if (list == null || list.size() == 0) {

return;

}

this.mListAll = list;

invalidate();

}

/**

* 设置折线图颜色

*

* @param position

*/

private void setLineColor(int position) {

mLineColor = mColors[position % mColors.length];

mPaintLine.setColor(mLineColor);

mPaintCircle.setColor(mLineColor);

}

private void initView() {

mColors = new int[]{ContextCompat.getColor(mContext, R.color.colorAccent)

, ContextCompat.getColor(mContext, R.color.colorPrimary)};

mPaintLine = new Paint();

mPaintLine.setStyle(Paint.Style.STROKE);

mPaintLine.setStrokeWidth(2);

mPaintLine.setAntiAlias(true);

mPaintCircle = new Paint();

mPaintCircle.setStyle(Paint.Style.STROKE);

mPaintCircle.setStrokeWidth(3);

mPaintCircle.setAntiAlias(true);

mPaintPoint = new Paint();

mPaintPoint.setStyle(Paint.Style.FILL);

mPaintPoint.setColor(Color.WHITE);

mPaintPoint.setAntiAlias(true);

mPaintBottomLine = new Paint();

mPaintBottomLine.setStyle(Paint.Style.STROKE);

mPaintBottomLine.setStrokeWidth(3);

mPaintBottomLine.setColor(Color.parseColor("#999999"));

mPaintBottomLine.setAntiAlias(true);

mPaintLimit = new Paint();

mPaintLimit.setStyle(Paint.Style.FILL);

mPaintLimit.setStrokeWidth(2);

mPaintLimit.setColor(Color.parseColor("#000000"));

mPaintLimit.setAntiAlias(true);

//画笔->绘制字体

mPaintText = new Paint();

mPaintText.setAntiAlias(true);

mPaintText.setStyle(Paint.Style.FILL);

mPaintText.setColor(Color.parseColor("#666666"));

mPaintText.setTextSize(sp2px(mContext, 14));

}

@Override

protected void onDraw(Canvas canvas) {

super.onDraw(canvas);

for (int jjj = 0; jjj < mListAll.size(); jjj++) {

List itemList = mListAll.get(jjj);

if (itemList != null && itemList.size() > 0) {

float mMaxVal = Collections.max(itemList, new MyComparator()).getVal();

Log.i("TAG", "最大值:" + mMaxVal);

setLineColor(jjj);

Path path = new Path();

List pointList = new ArrayList<>();

for (int i = 0; i < itemList.size(); i++) {

int xDiv = 0;

if (itemList.size() > 1) {

xDiv = (mViewWidth - getPaddingLeft() - getPaddingRight()) / (itemList.size() - 1);

}

MyModel item = itemList.get(i);

float x = i * xDiv;

float y = item.getVal() * (dip2px(mContext, mSingleLineHeight - mPaddingTB * 2)) / mMaxVal;

y = ((dip2px(mContext, mSingleLineHeight)) * (jjj + 1)) - dip2px(mContext, mPaddingTB * 2) - y;

if (i == 0) {

path.moveTo(x + getPaddingLeft(), y + dip2px(mContext, mPaddingTB));

} else {

path.lineTo(x + getPaddingLeft(), y + dip2px(mContext, mPaddingTB));

}

/**

* 这里记录一下xy坐标,用于后面绘制小球

*/

Point point = new Point();

point.x = (int) x;

point.y = (int) y;

pointList.add(point);

}

//画折线

canvas.drawPath(path, mPaintLine);

//画小圆球

drawCircle(canvas, pointList, jjj);

//画文字

if (jjj == mListAll.size() - 1) {

drawText(canvas, pointList);

}

}

}

/**

* 画竖线,指示线

*/

if (mLineX > 0) {

canvas.drawLine(mLineX, 0, mLineX, mViewHeight - dip2px(mContext, mBottomTextHeight), mPaintLimit);

}

}

/**

* 画圆和底部X轴

*

* @param canvas

* @param pointList

*/

private void drawCircle(Canvas canvas, List pointList, int jjj) {

for (int i = 0; i < pointList.size(); i++) {

Point point = pointList.get(i);

//画圆圈

canvas.drawCircle(point.x + getPaddingLeft(), point.y + dip2px(mContext, mPaddingTB), 10, mPaintCircle);

if (position == i && mLineX > 0) {

mPaintPoint.setColor(mLineColor);

} else {

mPaintPoint.setColor(Color.WHITE);

}

//填充圆内空间

canvas.drawCircle(point.x + getPaddingLeft(), point.y + dip2px(mContext, mPaddingTB), 9, mPaintPoint);

//画X轴间隔线

canvas.drawLine(point.x + getPaddingLeft(), dip2px(mContext, mSingleLineHeight) * (jjj + 1), point.x + getPaddingLeft(), dip2px(mContext, mSingleLineHeight) * (jjj + 1) - dip2px(mContext, 5), mPaintBottomLine);

}

//底部X轴

canvas.drawLine(0, dip2px(mContext, mSingleLineHeight) * (jjj + 1), mViewWidth, dip2px(mContext, mSingleLineHeight) * (jjj + 1), mPaintBottomLine);

}

/**

* 画文字

*

* @param canvas

* @param pointList

*/

private void drawText(Canvas canvas, List pointList) {

for (int i = 0; i < pointList.size(); i++) {

Point point = pointList.get(i);

//画底部文字

String text = (i + 1) + "";

//获取文字宽度

float textWidth = mPaintText.measureText(text, 0, text.length());

float dx = point.x + getPaddingLeft() - textWidth / 2;

Paint.FontMetricsInt fontMetricsInt = mPaintText.getFontMetricsInt();

float dy = (fontMetricsInt.bottom - fontMetricsInt.top) / 2 - fontMetricsInt.bottom;

float baseLine = dip2px(mContext, mSingleLineHeight) * mListAll.size() + dip2px(mContext, mBottomTextHeight / 2) + dy;

canvas.drawText(text, dx, baseLine, mPaintText);

}

}

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

super.onMeasure(widthMeasureSpec, heightMeasureSpec);

/**

* 这里根据数据有多少组来动态计算整个view的高度,然后重新设置尺寸

*/

mViewHeight = dip2px(mContext, mSingleLineHeight) * mListAll.size() + dip2px(mContext, mBottomTextHeight);

mViewWidth = MeasureSpec.getSize(widthMeasureSpec);

setMeasuredDimension(mViewWidth, mViewHeight);

}

@Override

public boolean onTouchEvent(MotionEvent event) {

switch (event.getAction()) {

case MotionEvent.ACTION_DOWN:

case MotionEvent.ACTION_MOVE:

getPointLine(event.getX());

}

return true;

}

private float mLineX = 0;

private int position = 0;

/**

* 判断触摸的坐标距离哪个点最近

*

* @param mRawX

*/

private void getPointLine(float mRawX) {

if (mListAll == null || mListAll.size() == 0) {

return;

}

float newLineX = 0;

//触摸在折线区域

if (mRawX <= mViewWidth - getPaddingRight() && mRawX >= getPaddingLeft()) {

if (mListAll.get(0).size() == 1) {

newLineX = getPaddingLeft();

position = 0;

} else {

for (int i = 0; i < mListAll.get(0).size(); i++) {

int xDiv = 0;

if (mListAll.get(0).size() > 1) {

xDiv = (mViewWidth - getPaddingLeft() - getPaddingRight()) / (mListAll.get(0).size() - 1);

}

float x1 = i * xDiv + getPaddingLeft();

float x2 = (i + 1) * xDiv + getPaddingLeft();

//判断触摸在两个点之间时,离谁更近一些

if (mRawX > x1 && mRawX < x2) {

float cneterX = x1 + (x2 - x1) / 2;

if (mRawX > cneterX) {

newLineX = x2;

position = i + 1;

if (position == mListAll.get(0).size()) {

position = i;

}

} else {

newLineX = x1;

position = i;

}

break;

}

}

}

} else if (mRawX < getPaddingLeft()) {//触摸在折线左边

newLineX = getPaddingLeft();

position = 0;

} else {//触摸在折线右边

if (mListAll.get(0).size() == 1) {

newLineX = getPaddingLeft();

position = 0;

} else {

newLineX = mViewWidth - getPaddingRight();

position = mListAll.get(0).size() - 1;

}

}

/**

* 这里判断如果跟上次的触摸结果一样,则不处理

*/

if (mLineX == newLineX) {

return;

}

mLineX = newLineX;

notifyUI(mLineX);

}

/**

* 选中某一组

*

* @param position

*/

public void setPosition(int position) {

try {

this.position = position;

int xDiv = (mViewWidth - getPaddingLeft() - getPaddingRight()) / (mListAll.get(0).size() - 1);

mLineX = position * xDiv + getPaddingLeft();

notifyUI(mLineX);

} catch (Exception e) {

e.printStackTrace();

Log.i("MyChatView", "Exception:" + e);

}

}

private void notifyUI(float mLineX) {

this.mLineX = mLineX;

if (onClickListener != null) {

onClickListener.click(position);

}

invalidate();

}

private OnClickListener onClickListener;

public void setOnClickListener(OnClickListener listener) {

this.onClickListener = listener;

}

public interface OnClickListener {

void click(int position);

}

public static int dip2px(Context context, float dpValue) {

final float scale = context.getResources().getDisplayMetrics().density;

return (int) (dpValue * scale + 0.5f);

}

public static int sp2px(Context context, float spValue) {

final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;

return (int) (spValue * fontScale + 0.5f);

}

private class MyComparator implements Comparator {

public int compare(MyModel o1, MyModel o2) {

return (o1.getVal() < o2.getVal() ? -1 : (o1.getVal() == o2.getVal() ? 0 : 1));

}

}

}

使用:

public class MainActivity extends AppCompatActivity {

private MyChatView view1;

private TextView tv_text;

private List> mListAll = new ArrayList<>();

/**

* 获取随机数

*

* @param range

* @param startsfrom

* @return

*/

protected float getRandom(float range, float startsfrom) {

return (float) (Math.random() * range) + startsfrom;

}

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

view1 = (MyChatView) findViewById(R.id.view1);

tv_text = (TextView) findViewById(R.id.tv_text);

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

List item = new ArrayList<>();

for (int i1 = 0; i1 < 15; i1++) {

item.add(new MyModel(i1, getRandom(1000, 500)));

}

mListAll.add(item);

}

view1.setData(mListAll);

view1.setOnClickListener(new MyChatView.OnClickListener() {

@Override

public void click(int position) {

update(position);

}

});

findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

/**

* 设置默认选中第几组数据

*/

view1.setPosition(new Random().nextInt(15));

}

});

}

private void update(int position) {

tv_text.setText("");

tv_text.append("第" + (position + 1) + "组:\n");

for (int i = 0; i < mListAll.size(); i++) {

tv_text.append("第" + i + "个数据:" + mListAll.get(i).get(position).getVal() + "\n");

}

}

}

GitHub传送门:源码

android 多个折线图 最佳视野,自定义View_撸一个多层折线图相关推荐

  1. 呆呆带你手撸一个思维导图-基础篇

    希沃ENOW大前端 公司官网:CVTE(广州视源股份) 团队:CVTE旗下未来教育希沃软件平台中心enow团队 「本文作者:」 前言 你盼世界,我盼望你无bug.Hello 大家好,我是霖呆呆! 哈哈 ...

  2. html轮播图原理,30_用js实现一个轮播图效果,简单说下原理

    一.原理 将一些图片在一行中平铺,然后计算偏移量再利用定时器实现定时轮播. 步骤一:建立html基本布局 如下所示: 轮播图 1 2 3 4 5 < > 只有五张图片,却使用7张来轮播,这 ...

  3. android canvas绘制圆角_Android自定义View撸一个渐变的温度指示器(TmepView)

    秦子帅明确目标,每天进步一点点..... 作者 |  andy 地址 |  blog.csdn.net/Andy_l1/article/details/82910061 1.概述 自定义View对需要 ...

  4. iMeta | 复杂热图(ComplexHeatmap)可视化文章最新版,画热图就引它

    点击蓝字 关注我们 复杂热图可视化 https://doi.org/10.1002/imt2.43 PROTOCOL ●2022年8月,德国癌症研究中心顾祖光在iMeta在线发表了题为"Co ...

  5. .NET手撸绘制TypeScript类图——上篇

    .NET手撸绘制TypeScript类图--上篇 近年来随着交互界面的精细化, TypeScript越来越流行,前端的设计也越来复杂,而 类图正是用简单的箭头和方块,反映对象与对象之间关系/依赖的好方 ...

  6. typescript get方法_.NET手撸绘制TypeScript类图——上篇

    .NET手撸绘制TypeScript类图--上篇 近年来随着交互界面的精细化,TypeScript越来越流行,前端的设计也越来复杂,而类图正是用简单的箭头和方块,反映对象与对象之间关系/依赖的好方式. ...

  7. UML图 | 让你快速学会使用 Visio 绘制时序图(顺序、序列),再也不用担心文档画图问题啦!!

    上一次写过一篇 UML | 类图 相关的文章,平时规范开发会用的上,或者是写什么文档,就还是需要画图,就像毕业设计就是如此.希望能够帮助到大家. 注:本文中所用画图软件为 Microsoft Visi ...

  8. UML图详解(七)——交互图(时序图与协作图)

    一.概念 交互图描述对象之间的动态合作关系以及合作过程中的行为次序. 交互图常常用来描述一个用例的行为,显示该用例中所涉及的对象以及这些对象之间的消息传递情况,即一个用例的实现过程. 交互图有顺序图和 ...

  9. android 轨迹生成图,Android自定义View实现公交成轨迹图

    本文实例为大家分享了Android自定义View实现公交成轨迹图的具体代码,供大家参考,具体内容如下 总体分析下:水平方向recyclewview,item包含定位点,站台位置和站台名称. 下面看实现 ...

最新文章

  1. nginx怎么部署php项目,nginx怎么正确部署前端项目
  2. 数据中心成投资新宠 今年或再创历史纪录
  3. 计算机四年级下册教案泰山版,泰山版信息技术四年级下册4、制作作息时间表教案设计...
  4. python缩进注意事项_python注意事项
  5. to_csvread_csvisnullisnanisna
  6. 关于《ASP.NET MVC企业级实战》
  7. 比较好的论坛(个人认为)
  8. ubuntu16.04 设置动态ip和静态ip及route命令的使用
  9. linux保险箱软件,手机加密App哪个好?手机加密软件推荐
  10. valgrind:内存泄漏 memory leak 调试教程
  11. Web安全深度剖析第三章读书笔记
  12. android sepolicy 最新小结
  13. 解决vue项目路由出现message: “Navigating to current location (XXX) is not allowed“的问题
  14. tomcat轻量级服务器
  15. 仿真(Simulation)
  16. 用计算机算法拼拼图,算法 – “拼图拼图”拼图
  17. 图说卡尔曼滤波(C++实现)
  18. 唤起公众号关注页面内部_外部H5页面内实现一键唤起微信添加好友OR关注公众号...
  19. 学子随感——遇见长郡浏阳(2)
  20. 写给在校生——听师兄传的IT之道

热门文章

  1. 电脑版QQ个人资料无法设置问题
  2. 叠罗汉I LeetCode中等题
  3. Redesign Your App for iOS 7 之 页面布局
  4. 后端实现发送短信接口
  5. 行业分析报告-全球与中国净推荐值软件市场现状及未来发展趋势
  6. Adobe Photoshop CS6快捷键大全
  7. 家庭关系代码 GB/T 4761-2008
  8. python中update是啥意思,python中update的基本使用方法详解
  9. 什么是HIS、PACS、LIS、RIS、EMR
  10. Android-Studio的登录页面设计