使用
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>