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

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

a7943fd329bb

UI图.png

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

a7943fd329bb

效果图.gif

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

a7943fd329bb

折线图.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传送门:源码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值