android 多个折线图 最佳视野,自定义View_撸一个多层折线图
看到这个标题,可能有点发懵,啥叫多层折线图啊?这个是我自己取的名字,是因为那天我遇到了这样一个需求。
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_撸一个多层折线图相关推荐
- 呆呆带你手撸一个思维导图-基础篇
希沃ENOW大前端 公司官网:CVTE(广州视源股份) 团队:CVTE旗下未来教育希沃软件平台中心enow团队 「本文作者:」 前言 你盼世界,我盼望你无bug.Hello 大家好,我是霖呆呆! 哈哈 ...
- html轮播图原理,30_用js实现一个轮播图效果,简单说下原理
一.原理 将一些图片在一行中平铺,然后计算偏移量再利用定时器实现定时轮播. 步骤一:建立html基本布局 如下所示: 轮播图 1 2 3 4 5 < > 只有五张图片,却使用7张来轮播,这 ...
- android canvas绘制圆角_Android自定义View撸一个渐变的温度指示器(TmepView)
秦子帅明确目标,每天进步一点点..... 作者 | andy 地址 | blog.csdn.net/Andy_l1/article/details/82910061 1.概述 自定义View对需要 ...
- iMeta | 复杂热图(ComplexHeatmap)可视化文章最新版,画热图就引它
点击蓝字 关注我们 复杂热图可视化 https://doi.org/10.1002/imt2.43 PROTOCOL ●2022年8月,德国癌症研究中心顾祖光在iMeta在线发表了题为"Co ...
- .NET手撸绘制TypeScript类图——上篇
.NET手撸绘制TypeScript类图--上篇 近年来随着交互界面的精细化, TypeScript越来越流行,前端的设计也越来复杂,而 类图正是用简单的箭头和方块,反映对象与对象之间关系/依赖的好方 ...
- typescript get方法_.NET手撸绘制TypeScript类图——上篇
.NET手撸绘制TypeScript类图--上篇 近年来随着交互界面的精细化,TypeScript越来越流行,前端的设计也越来复杂,而类图正是用简单的箭头和方块,反映对象与对象之间关系/依赖的好方式. ...
- UML图 | 让你快速学会使用 Visio 绘制时序图(顺序、序列),再也不用担心文档画图问题啦!!
上一次写过一篇 UML | 类图 相关的文章,平时规范开发会用的上,或者是写什么文档,就还是需要画图,就像毕业设计就是如此.希望能够帮助到大家. 注:本文中所用画图软件为 Microsoft Visi ...
- UML图详解(七)——交互图(时序图与协作图)
一.概念 交互图描述对象之间的动态合作关系以及合作过程中的行为次序. 交互图常常用来描述一个用例的行为,显示该用例中所涉及的对象以及这些对象之间的消息传递情况,即一个用例的实现过程. 交互图有顺序图和 ...
- android 轨迹生成图,Android自定义View实现公交成轨迹图
本文实例为大家分享了Android自定义View实现公交成轨迹图的具体代码,供大家参考,具体内容如下 总体分析下:水平方向recyclewview,item包含定位点,站台位置和站台名称. 下面看实现 ...
最新文章
- nginx怎么部署php项目,nginx怎么正确部署前端项目
- 数据中心成投资新宠 今年或再创历史纪录
- 计算机四年级下册教案泰山版,泰山版信息技术四年级下册4、制作作息时间表教案设计...
- python缩进注意事项_python注意事项
- to_csvread_csvisnullisnanisna
- 关于《ASP.NET MVC企业级实战》
- 比较好的论坛(个人认为)
- ubuntu16.04 设置动态ip和静态ip及route命令的使用
- linux保险箱软件,手机加密App哪个好?手机加密软件推荐
- valgrind:内存泄漏 memory leak 调试教程
- Web安全深度剖析第三章读书笔记
- android sepolicy 最新小结
- 解决vue项目路由出现message: “Navigating to current location (XXX) is not allowed“的问题
- tomcat轻量级服务器
- 仿真(Simulation)
- 用计算机算法拼拼图,算法 – “拼图拼图”拼图
- 图说卡尔曼滤波(C++实现)
- 唤起公众号关注页面内部_外部H5页面内实现一键唤起微信添加好友OR关注公众号...
- 学子随感——遇见长郡浏阳(2)
- 写给在校生——听师兄传的IT之道