使用
inflate.findViewById(R.id.lineChart);
Random random = new Random();//生成随机数
for (int i = 0; i < 80; i++) {
data[i][0] =random.nextInt(19)+1;
data[i][1] =random.nextInt(29)+1;
}
lineChart.setData(data);
lineChart.setSegmentX(10);
lineChart.setSegmentY(10);
lineChart.start();
布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:background="#333888"
android:layout_height="match_parent">
<com.example.ocean.charts.line.LineChart
android:id="@+id/lineChart"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="400dp"
app:graphTitle = "骑行记录"
app:xAxisNmae = "第几天"
app:yAxisNmae = "骑行公里数"
app:axisTextSize = "4sp"
android:layout_marginBottom="30dp"/>
</RelativeLayout>
折线图实现类 LineChart.java
package com.example.ocean.charts.line;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.Rect;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.view.MotionEvent;
import com.example.ocean.R;
import org.jetbrains.annotations.NotNull;
import androidx.annotation.Nullable;
public class LineChart extends BaseLineChart {
protected final Context mContext;
public int[][] data ;//传入的数据
private Paint linePaint;//折线画笔
private int defaultOriginalX ;//设置固定的X轴起点位置,X轴触摸可以滑动,用于防止Y轴滑动
private Paint vaguePaint;//阴影画笔
private float moveX;//当前滑动到的位置
public LineChart(Context context) {
this(context,null);
}
public LineChart(Context context, @Nullable AttributeSet attrs) {
this(context,attrs,0);
}
public LineChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr,0);
}
public LineChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mContext = context;
initPaint();
}
/** Explain : 初始化折线画笔
* @author LiXiang */
private void initPaint() {
linePaint = new Paint(basePaint);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setColor(getResources().getColor(R.color.blue_rgba_24_261_255));
linePaint.setStrokeWidth(2f);
vaguePaint = new Paint(basePaint);
vaguePaint.setStyle(Paint.Style.FILL);
vaguePaint.setStrokeWidth(dip2px(4));
vaguePaint.setColor(getResources().getColor(R.color.blue_sharder));
}
public void setData(int[][] data) {
this.data = data;
}
/** Explain : 设置X轴刻度距离
* @author LiXiang */
@Override
public void setSegmentX(int x) { segmentX = dip2px(x); }
/** Explain : 设置Y轴刻度距离
* @author LiXiang */
@Override
public void setSegmentY(int y) { segmentY = dip2px(y); }
/** Explain : Y轴长度,默认为视图的高度
* @author LiXiang */
@Override
protected int getSpaceY() {
return getDefaultSpaceY();
}
/** Explain : X轴的长度,由刻度距离和数据量决定
* @author LiXiang */
@Override
protected int getSpaceX() {
return defaultOriginalX + segmentX*data.length;
}
/** Explain : 绘制折线
* @author LiXiang */
@Override
protected void drawLine(Canvas canvas, Paint basePaint) {
//每次绘制阴影之前都需重新设置渐变熟悉
vaguePaint.setShader(new LinearGradient(//填充方向是以Y轴为基准 若是自上而下 就是 0,0,0,getMeasuredHeight()
defaultOriginalX, endY, defaultOriginalX, originalY,//从上到下逐渐变淡
new int[]{
getResources().getColor(R.color.blue_sharder),
getResources().getColor(android.R.color.transparent)},
null, Shader.TileMode.CLAMP)
);
//先绘制折线阴影部分,以防止折线被覆盖
canvas.drawPath(drawSegmentVague(drawVague()),vaguePaint);
//绘制折线
canvas.drawPath(drawSegment(drawLineReal()),linePaint);
}
/** Explain : 获取折线整体的path
* @author LiXiang */
private Path drawLineReal() {
Path path = new Path();
path.moveTo(originalX+segmentX,originalY-data[0][1]*segmentY);//移动到第一个点的位置
for (int i = 0; i < data.length; i++) {
path.lineTo(originalX+segmentX*(i+1),originalY-data[i][1]*segmentY);
}
return path;
}
/** Explain : 获取折线阴影整体的path
* @author LiXiang */
private Path drawVague() {
Path pathVague = new Path();
pathVague.moveTo(originalX+segmentX,originalY);//移动到第一个点的X轴
pathVague.lineTo(originalX+segmentX,originalY-data[0][1]*segmentY);//与第一个点的X轴连成线
for (int i = 0; i < data.length; i++) {//将后面的每一个点遍历入path
pathVague.lineTo(originalX+segmentX*(i+1),originalY-data[i][1]*segmentY);
}
return pathVague;
}
/** Explain : 依照动画进程获取阴影片段
* @author LiXiang */
@NotNull
private Path drawSegmentVague(Path path) {
Path pathVagueSegment = drawSegment(path);
//添加当前X轴绘制到的点,形参封闭区域
pathVagueSegment.lineTo(originalX+segmentX*mAnimatedValue * data.length,originalY);
pathVagueSegment.close();//形参封闭区域
return pathVagueSegment;
}
/** Explain : 依照动画进程获取折线片段
* @author LiXiang */
@NotNull
private Path drawSegment(Path path) {
Path pathVagueSegment = new Path();
PathMeasure pathMeasureCover = new PathMeasure(path, false);
float curveLength = pathMeasureCover.getLength();
//取当前path的某一段长度
pathMeasureCover.getSegment(0, curveLength * ( mAnimatedValue), pathVagueSegment, true);
return pathVagueSegment;
}
/** Explain : 绘制Y轴刻度值
* @author LiXiang */
@Override
protected void drawAxisScaleValuveY(Canvas canvas, Paint basePaint) {
int scaleSize = (originalY - endY) / segmentY;
for (int i = 1; i < scaleSize +1; i++) {
canvas.drawText(i+"",//显示的数字
defaultOriginalX-dip2px(10),//往左偏移10dp,防止遮住Y轴
(originalY-i*segmentY)+getDimenssion(i).height()/2,//使数字在刻度的中间
basePaint);
}
}
/** Explain : 计算数字的宽高
* @author LiXiang */
@NotNull
private Rect getDimenssion(int intNumber) {
String number = intNumber+"";
//获取数字宽高尺寸
Rect rect= new Rect();
basePaint.getTextBounds(number,0,number.length(),rect);
return rect;
}
/** Explain : 绘制X轴刻度值
* @author LiXiang */
@Override
protected void drawAxisScaleValuveX(Canvas canvas, Paint basePaint) {
//获取刻度的数量,当长度超过屏幕的时候选择对应数据量的长度,小于的时候选屏幕的宽度
int scaleSize = (getSpaceX()>getDefaultSpaceX()? data.length : getDefaultSpaceX()/ segmentX) ;
for (int i = 1; i < scaleSize +1; i++) {
canvas.drawText(i+"",//显示的数字
originalX+i*segmentX-getDimenssion(i).width()/2,//往下偏移10dp,防止遮住X轴
originalY+dip2px(15),//使数字在刻度的中间
basePaint);
}
}
/** Explain : 绘制Y轴刻度
* @author LiXiang */
@Override
protected void drawAxisScaleY(Canvas canvas, Paint basePaint) {
int scaleSize = (originalY - endY)/ segmentY;
for (int i = 0; i < scaleSize; i++) {
canvas.drawLine(defaultOriginalX,originalY-i*segmentX,defaultOriginalX+dip2px(2),originalY-i*segmentX,basePaint);
}
}
/** Explain : 绘制X轴
* @author LiXiang */
@Override
protected void drawAxisScaleX(Canvas canvas, Paint basePaint) {
//获取刻度的数量,当长度超过屏幕的时候选择对应数据量的长度,小于的时候选屏幕的宽度
int scaleSize = (getSpaceX()>getDefaultSpaceX()? data.length : getDefaultSpaceX()/ segmentX) ;
for (int i = 0; i < scaleSize; i++) {
canvas.drawLine(originalX+i*segmentX,originalY,originalX+i*segmentX,originalY-dip2px(2),basePaint);
}
}
/** Explain : 绘制Y轴
* @author LiXiang */
@Override
protected void drawAxisY(Canvas canvas, Paint basePaint) {
canvas.drawLine(defaultOriginalX,originalY,defaultOriginalX,endY,basePaint);
}
/** Explain : 绘制X轴
* @author LiXiang */
@Override
protected void drawAxisX(Canvas canvas, Paint basePaint) {
canvas.drawLine(originalX,originalY,endX+originalX,originalY,basePaint);
}
/** Explain : 绘制X轴箭头
* @author LiXiang */
protected void drawAxisArrowX(Canvas canvas, Paint basePaint) {
Path path = new Path();
path.moveTo(endX+dip2px(2)+originalX,originalY);
path.lineTo(endX+originalX,originalY + dip2px(2));
path.lineTo(endX+originalX,originalY - dip2px(2));
path.close();
canvas.drawPath(path,basePaint);
canvas.drawText(xAxisNmae,endX+dip2px(16)+originalX,originalY + dip2px(2),basePaint);
}
/** Explain : 绘制Y轴箭头
* @author LiXiang */
public void drawAxisArrowY(Canvas canvas, Paint basePaint) {
Path path = new Path();
path.moveTo(defaultOriginalX,endY - dip2px(2));
path.lineTo(defaultOriginalX+ dip2px(2),endY );
path.lineTo(defaultOriginalX- dip2px(2),endY );
path.close();
canvas.drawPath(path,basePaint);
canvas.drawText(yAxisNmae,defaultOriginalX,endY - dip2px(8),basePaint);
}
private float minOriginalX;
private float maxOriginalX;
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
defaultOriginalX = getPaddingLeft() + defaultPadding;
minOriginalX = getWidth() - segmentX * (data.length+4);//起点可以移动到的最小值
maxOriginalX = defaultOriginalX;//起点的最大值就是最初的位置
}
/** Explain : 动画每进行一次刷新都重置X轴起点位置
* @author LiXiang */
@Override
protected void invalidateOtherData() {
super.invalidateOtherData();
originalX = (int) ( defaultOriginalX
+ (minOriginalX-defaultOriginalX)*mAnimatedValue);//当前X起点可以移动的总距离(X轴起点的正值+X轴起点的最小负值)*当前动画的进度
}
/** Explain : 监听当前的滑动事件
* @author LiXiang */
@Override
public boolean onTouchEvent(MotionEvent event) {
this.getParent().requestDisallowInterceptTouchEvent(true);//当该view获得点击事件,就请求父控件不拦截事件
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
moveX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
if (segmentX * data.length > getWidth() - defaultOriginalX) {//当期的宽度不足以呈现全部数据的时候才可以滑动
float dis = event.getX() - moveX;//获取当前滑动的距离
moveX = event.getX();//记录当前滑动的位置
//设置滑动到两端的极限值
if (originalX + dis < minOriginalX) {
originalX = (int) minOriginalX;
} else if (originalX + dis > maxOriginalX) {
originalX = (int) maxOriginalX;
} else {
originalX = (int) (originalX + dis);
}
invalidate();//刷新数据
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
this.getParent().requestDisallowInterceptTouchEvent(false);
break;
}
return true;
}
}
基础类 BaseLineChart.java
package com.example.ocean.charts.line;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Typeface;
import android.text.TextUtils;
import android.util.AttributeSet;
import com.example.ocean.R;
import com.example.ocean.charts.BaseChart;
import androidx.annotation.Nullable;
public abstract class BaseLineChart extends BaseChart {
protected int axisTextColor;//刻度值颜色
protected int axisTextSize;//刻度值字体大小
protected String xAxisNmae;//X轴名字
protected String yAxisNmae;//Y轴名字
protected String graphTitle;//图表名字
protected int segmentX;//轴间距
protected int segmentY;//轴间距
public BaseLineChart(Context context) {
this(context,null);
}
public BaseLineChart(Context context, @Nullable AttributeSet attrs) {
this(context,attrs,0);
}
public BaseLineChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr,0);
}
public BaseLineChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
/** Explain : 获取样式
* @author LiXiang */
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.GraphStyle);
graphTitle = typedArray.getString(R.styleable.GraphStyle_graphTitle);
xAxisNmae = typedArray.getString(R.styleable.GraphStyle_xAxisNmae);
yAxisNmae = typedArray.getString(R.styleable.GraphStyle_yAxisNmae);
axisTextColor = typedArray.getColor(R.styleable.GraphStyle_axisTextColor,Color.GRAY);
axisTextSize= (int) typedArray.getDimension(R.styleable.GraphStyle_axisTextSize,4);
axisTextSize = dip2px(axisTextSize);
if (typedArray != null) {
typedArray.recycle();
}
// Typeface font0 = Typeface.create(Typeface.SANS_SERIF, Typeface.DEFAULT_BOLD.getStyle());
// basePaint.setTypeface(font0);
//初始化基础画笔
basePaint.setTextSize(dip2px(8));//设置标题大小
basePaint.setTextAlign(Paint.Align.CENTER);//标题对其格式
basePaint.setStrokeWidth(dip2px(0.1f));//画笔粗细
basePaint.setStrokeCap(Paint.Cap.ROUND);//设置圆角
basePaint.setColor(Color.GRAY);//设置画笔颜色
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawTitle(canvas,basePaint);//绘制标题
drawAxisArrowX(canvas,basePaint);//绘制X轴箭头
drawAxisArrowY(canvas,basePaint);//绘制Y轴箭头
//以下类交给子类处理
drawAxisX(canvas,basePaint);//绘制X轴
drawAxisY(canvas,basePaint);//绘制Y轴
drawAxisScaleX(canvas,basePaint);//绘制X轴刻度
drawAxisScaleY(canvas,basePaint);//绘制Y轴刻度
drawAxisScaleValuveX(canvas,basePaint);//绘制X轴刻度值
drawAxisScaleXValuveY(canvas,basePaint);//绘制Y轴刻度值
drawLine(canvas,basePaint);//绘制折线
}
/** Explain : 绘制X轴箭头
* @author LiXiang */
protected void drawAxisArrowX(Canvas canvas, Paint basePaint) {
Path path = new Path();
path.moveTo(endX+dip2px(2),originalY);
path.lineTo(endX,originalY + dip2px(2));
path.lineTo(endX,originalY - dip2px(2));
path.close();
canvas.drawPath(path,basePaint);
canvas.drawText(xAxisNmae,endX+dip2px(16),originalY + dip2px(2),basePaint);
}
/** Explain : 绘制Y轴箭头
* @author LiXiang */
protected void drawAxisArrowY(Canvas canvas, Paint basePaint) {
Path path = new Path();
path.moveTo(originalX,endY - dip2px(2));
path.lineTo(originalX+ dip2px(2),endY );
path.lineTo(originalX- dip2px(2),endY );
path.close();
canvas.drawPath(path,basePaint);
canvas.drawText(yAxisNmae,originalX,endY - dip2px(8),basePaint);
}
/** Explain : 设置标题
* @author LiXiang
* @param canvas
* @param basePaint */
private void drawTitle(Canvas canvas, Paint basePaint) {
if (!TextUtils.isEmpty(graphTitle)) {
Paint titlePaint = new Paint(basePaint);
titlePaint.setFakeBoldText(true);//设置字体加粗
titlePaint.setTextSize(axisTextSize);
titlePaint.setColor(axisTextColor);
canvas.drawText(graphTitle,getWidth() / 2,originalY+dip2px(35),titlePaint);
}
}
protected abstract void setSegmentX(int x);//X轴长度
protected abstract void setSegmentY(int y);//Y轴长度
protected abstract void drawLine(Canvas canvas, Paint basePaint);
protected abstract void drawAxisScaleXValuveY(Canvas canvas, Paint basePaint);
protected abstract void drawAxisScaleValuveX(Canvas canvas, Paint basePaint);
protected abstract void drawAxisScaleY(Canvas canvas, Paint basePaint);
protected abstract void drawAxisScaleX(Canvas canvas, Paint basePaint);
protected abstract void drawAxisY(Canvas canvas, Paint basePaint);
protected abstract void drawAxisX(Canvas canvas, Paint basePaint);
}
基础类 BaseChart.java
package com.example.ocean.charts;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Paint;
import android.os.Build;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.View;
import com.example.ocean.R;
import androidx.annotation.Nullable;
public abstract class BaseChart extends View {
protected final Context mContext;
protected Paint basePaint;//基础画笔
protected int originalX ;//X轴起点
protected int originalY ;//Y轴起点
protected int endX ;//X轴终点
protected int endY ;//Y轴终点
protected int defaultPadding = dip2px(30);//默认内边距
protected ValueAnimator valueAnimator;//动画
protected boolean starting = false;//是否正在执行动画
protected float mAnimatedValueMax = 1;//动画最大值
protected float mAnimatedValue = 0;//动画当前值
protected boolean onTouch = false;//是否正在触摸
public BaseChart(Context context) {
this(context,null);
}
public BaseChart(Context context, @Nullable AttributeSet attrs) {
this(context,attrs,0);
}
public BaseChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr,0);
}
public BaseChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mContext = context;
initBasePaint();
}
/** Explain : 初始化基础画笔
* @author LiXiang */
private void initBasePaint() {
if (basePaint == null) {
basePaint = new Paint();
basePaint.setAntiAlias(true);//抗锯齿
basePaint.setDither(true);//防抖动
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
originalX = getPaddingLeft() + defaultPadding;
originalY = getMeasuredHeight() - getPaddingBottom() - 2*defaultPadding;
endX = getSpaceX()>getDefaultSpaceX()?getSpaceX():getDefaultSpaceX();
endY = getSpaceY()>getDefaultSpaceY()?getSpaceY():getDefaultSpaceY();
}
protected int getDefaultSpaceY() { return getPaddingTop() + defaultPadding; }
protected int getDefaultSpaceX() { return getMeasuredWidth() - getPaddingRight() - defaultPadding; }
protected abstract int getSpaceY();
abstract protected int getSpaceX();
protected int dip2px(float dipValue) {
final float scale = Resources.getSystem().getDisplayMetrics().density;
return (int) (dipValue * scale + 0.5f);
}
public void start() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
startAnimator();
} else {
this.post(new Runnable() {//可以避免页面未初始化完成造成的 空白
@Override
public void run() {
startAnimator();
}
});
}
}
private void startAnimator() {
if ( starting) {//只能绘制一次 或者正在绘制过程中的话不能再次绘制
return;
}
starting = true;
valueAnimator = ValueAnimator.ofFloat(0, mAnimatedValueMax).setDuration(5000);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mAnimatedValue = (float) valueAnimator.getAnimatedValue();
if (starting) {
System.out.println("mAnimatedValue:" + mAnimatedValue);
invalidateOtherData();
invalidate();
}
}
});
valueAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
starting = false;
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
valueAnimator.start();
}
protected void invalidateOtherData(){};
public void postDelayedInvalidate() {
onTouch = false;//置为响应触摸操作的绘制
getParent().requestDisallowInterceptTouchEvent(false);//离开绘制区域,拦截触摸事件
handler.postDelayed(new Runnable() {
@Override
public void run() {
invalidate();
}
}, 1000);
}
@SuppressLint("HandlerLeak")
private Handler handler = new Handler();
}
style样式属性
<!-- 折线图图表样式-->
<declare-styleable name="GraphStyle">
<attr name="graphTitle" format="string"/>
<attr name="xAxisNmae" format="string"/>
<attr name="yAxisNmae" format="string"/>
<attr name="axisTextSize" format="dimension|integer"/>
<attr name="axisTextColor" format="color|integer"/>
<attr name="axisDevidedSizeX" format="integer"/>
<attr name="axisDevidedSizeY" format="integer"/>
</declare-styleable>