Android自定義可左右滑動(dòng)和點(diǎn)擊的折線圖
前言
前幾天有小盆友讓我寫一個(gè)折線圖,可以點(diǎn)擊,可以左右滑動(dòng)。對(duì)于折線肯定有很多項(xiàng)目都使用過,所以網(wǎng)上肯定也有很多demo,像AndroidChart、HelloChart之類的,功能相當(dāng)豐富,效果也很贊,但是太重了,其他的小demo又不符合要求,當(dāng)然了,我寫的自定義折線圖的思想也有來自這些小demo,對(duì)他們表示感謝。
效果圖
廢話不多說,先上效果圖:

效果是不是很贊,如果上圖滿足你的需求,那就繼續(xù)往下看。
自定義折線圖的步驟:
1、自定義view所需要的屬性
確定所需要的自定義view的屬性,然后在res/values目錄下,新建一個(gè)attrs.xml文件,代碼如下:
<?xml version="1.0" encoding="utf-8"?> <resources> <!-- xy坐標(biāo)軸顏色 --> <attr name="xylinecolor" format="color" /> <!-- xy坐標(biāo)軸寬度 --> <attr name="xylinewidth" format="dimension" /> <!-- xy坐標(biāo)軸文字顏色 --> <attr name="xytextcolor" format="color" /> <!-- xy坐標(biāo)軸文字大小 --> <attr name="xytextsize" format="dimension" /> <!-- 折線圖中折線的顏色 --> <attr name="linecolor" format="color" /> <!-- x軸各個(gè)坐標(biāo)點(diǎn)水平間距 --> <attr name="interval" format="dimension" /> <!-- 背景顏色 --> <attr name="bgcolor" format="color" /> <!--是否在ACTION_UP時(shí),根據(jù)速度進(jìn)行自滑動(dòng),建議關(guān)閉,過于占用GPU--> <attr name="isScroll" format="boolean" /> <declare-styleable name="chartView"> <attr name="xylinecolor" /> <attr name="xylinewidth" /> <attr name="xytextcolor" /> <attr name="xytextsize" /> <attr name="linecolor" /> <attr name="interval" /> <attr name="bgcolor" /> <attr name="isScroll" /> </declare-styleable> </resources>
2、在自定義view的構(gòu)造方法中獲取我們的自定義屬性:
public ChartView(Context context) {
this(context, null);
}
public ChartView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ChartView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr);
initPaint();
}
/**
* 初始化
*
* @param context
* @param attrs
* @param defStyleAttr
*/
private void init(Context context, AttributeSet attrs, int defStyleAttr) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.chartView, defStyleAttr, 0);
int count = array.getIndexCount();
for (int i = 0; i < count; i++) {
int attr = array.getIndex(i);
switch (attr) {
case R.styleable.chartView_xylinecolor://xy坐標(biāo)軸顏色
xylinecolor = array.getColor(attr, xylinecolor);
break;
case R.styleable.chartView_xylinewidth://xy坐標(biāo)軸寬度
xylinewidth = (int) array.getDimension(attr, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, xylinewidth, getResources().getDisplayMetrics()));
break;
case R.styleable.chartView_xytextcolor://xy坐標(biāo)軸文字顏色
xytextcolor = array.getColor(attr, xytextcolor);
break;
case R.styleable.chartView_xytextsize://xy坐標(biāo)軸文字大小
xytextsize = (int) array.getDimension(attr, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, xytextsize, getResources().getDisplayMetrics()));
break;
case R.styleable.chartView_linecolor://折線圖中折線的顏色
linecolor = array.getColor(attr, linecolor);
break;
case R.styleable.chartView_interval://x軸各個(gè)坐標(biāo)點(diǎn)水平間距
interval = (int) array.getDimension(attr, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, interval, getResources().getDisplayMetrics()));
break;
case R.styleable.chartView_bgcolor: //背景顏色
bgcolor = array.getColor(attr, bgcolor);
break;
case R.styleable.chartView_isScroll://是否在ACTION_UP時(shí),根據(jù)速度進(jìn)行自滑動(dòng)
isScroll = array.getBoolean(attr, isScroll);
break;
}
}
array.recycle();
}
/**
* 初始化畫筆
*/
private void initPaint() {
xyPaint = new Paint();
xyPaint.setAntiAlias(true);
xyPaint.setStrokeWidth(xylinewidth);
xyPaint.setStrokeCap(Paint.Cap.ROUND);
xyPaint.setColor(xylinecolor);
xyTextPaint = new Paint();
xyTextPaint.setAntiAlias(true);
xyTextPaint.setTextSize(xytextsize);
xyTextPaint.setStrokeCap(Paint.Cap.ROUND);
xyTextPaint.setColor(xytextcolor);
xyTextPaint.setStyle(Paint.Style.STROKE);
linePaint = new Paint();
linePaint.setAntiAlias(true);
linePaint.setStrokeWidth(xylinewidth);
linePaint.setStrokeCap(Paint.Cap.ROUND);
linePaint.setColor(linecolor);
linePaint.setStyle(Paint.Style.STROKE);
}
3、獲取一寫基本點(diǎn)
這些基本點(diǎn)包括:xy軸的原點(diǎn)坐標(biāo),第一個(gè)點(diǎn)的x軸的初始化坐標(biāo)值以及其最大值和最小值。這些參數(shù)可以在onLayout()方法里面獲取。
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (changed) {
//這里需要確定幾個(gè)基本點(diǎn),只有確定了xy軸原點(diǎn)坐標(biāo),第一個(gè)點(diǎn)的X坐標(biāo)值及其最大最小值
width = getWidth();
height = getHeight();
//Y軸文本最大寬度
float textYWdith = getTextBounds("000", xyTextPaint).width();
for (int i = 0; i < yValue.size(); i++) {//求取y軸文本最大的寬度
float temp = getTextBounds(yValue.get(i) + "", xyTextPaint).width();
if (temp > textYWdith)
textYWdith = temp;
}
int dp2 = dpToPx(2);
int dp3 = dpToPx(3);
xOri = (int) (dp2 + textYWdith + dp2 + xylinewidth);//dp2是y軸文本距離左邊,以及距離y軸的距離
// //X軸文本最大高度
xValueRect = getTextBounds("000", xyTextPaint);
float textXHeight = xValueRect.height();
for (int i = 0; i < xValue.size(); i++) {//求取x軸文本最大的高度
Rect rect = getTextBounds(xValue.get(i) + "", xyTextPaint);
if (rect.height() > textXHeight)
textXHeight = rect.height();
if (rect.width() > xValueRect.width())
xValueRect = rect;
}
yOri = (int) (height - dp2 - textXHeight - dp3 - xylinewidth);//dp3是x軸文本距離底邊,dp2是x軸文本距離x軸的距離
xInit = interval + xOri;
minXInit = width - (width - xOri) * 0.1f - interval * (xValue.size() - 1);//減去0.1f是因?yàn)樽詈笠粋€(gè)X周刻度距離右邊的長(zhǎng)度為X軸可見長(zhǎng)度的10%
maxXInit = xInit;
}
super.onLayout(changed, left, top, right, bottom);
}
4、利用ondraw()方法進(jìn)行繪制
@Override
protected void onDraw(Canvas canvas) {
// super.onDraw(canvas);
canvas.drawColor(bgcolor);
drawXY(canvas);
drawBrokenLineAndPoint(canvas);
}
/**
* 繪制折線和折線交點(diǎn)處對(duì)應(yīng)的點(diǎn)
*
* @param canvas
*/
private void drawBrokenLineAndPoint(Canvas canvas) {
if (xValue.size() <= 0)
return;
//重新開一個(gè)圖層
int layerId = canvas.saveLayer(0, 0, width, height, null, Canvas.ALL_SAVE_FLAG);
drawBrokenLine(canvas);
drawBrokenPoint(canvas);
// 將折線超出x軸坐標(biāo)的部分截取掉
linePaint.setStyle(Paint.Style.FILL);
linePaint.setColor(bgcolor);
linePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
RectF rectF = new RectF(0, 0, xOri, height);
canvas.drawRect(rectF, linePaint);
linePaint.setXfermode(null);
//保存圖層
canvas.restoreToCount(layerId);
}
/**
* 繪制折線對(duì)應(yīng)的點(diǎn)
*
* @param canvas
*/
private void drawBrokenPoint(Canvas canvas) {
float dp2 = dpToPx(2);
float dp4 = dpToPx(4);
float dp7 = dpToPx(7);
//繪制節(jié)點(diǎn)對(duì)應(yīng)的原點(diǎn)
for (int i = 0; i < xValue.size(); i++) {
float x = xInit + interval * i;
float y = yOri - yOri * (1 - 0.1f) * value.get(xValue.get(i)) / yValue.get(yValue.size() - 1);
//繪制選中的點(diǎn)
if (i == selectIndex - 1) {
linePaint.setStyle(Paint.Style.FILL);
linePaint.setColor(0xffd0f3f2);
canvas.drawCircle(x, y, dp7, linePaint);
linePaint.setColor(0xff81dddb);
canvas.drawCircle(x, y, dp4, linePaint);
drawFloatTextBox(canvas, x, y - dp7, value.get(xValue.get(i)));
}
//繪制普通的節(jié)點(diǎn)
linePaint.setStyle(Paint.Style.FILL);
linePaint.setColor(Color.WHITE);
canvas.drawCircle(x, y, dp2, linePaint);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setColor(linecolor);
canvas.drawCircle(x, y, dp2, linePaint);
}
}
/**
* 繪制顯示Y值的浮動(dòng)框
*
* @param canvas
* @param x
* @param y
* @param text
*/
private void drawFloatTextBox(Canvas canvas, float x, float y, int text) {
int dp6 = dpToPx(6);
int dp18 = dpToPx(18);
//p1
Path path = new Path();
path.moveTo(x, y);
//p2
path.lineTo(x - dp6, y - dp6);
//p3
path.lineTo(x - dp18, y - dp6);
//p4
path.lineTo(x - dp18, y - dp6 - dp18);
//p5
path.lineTo(x + dp18, y - dp6 - dp18);
//p6
path.lineTo(x + dp18, y - dp6);
//p7
path.lineTo(x + dp6, y - dp6);
//p1
path.lineTo(x, y);
canvas.drawPath(path, linePaint);
linePaint.setColor(Color.WHITE);
linePaint.setTextSize(spToPx(14));
Rect rect = getTextBounds(text + "", linePaint);
canvas.drawText(text + "", x - rect.width() / 2, y - dp6 - (dp18 - rect.height()) / 2, linePaint);
}
/**
* 繪制折線
*
* @param canvas
*/
private void drawBrokenLine(Canvas canvas) {
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setColor(linecolor);
//繪制折線
Path path = new Path();
float x = xInit + interval * 0;
float y = yOri - yOri * (1 - 0.1f) * value.get(xValue.get(0)) / yValue.get(yValue.size() - 1);
path.moveTo(x, y);
for (int i = 1; i < xValue.size(); i++) {
x = xInit + interval * i;
y = yOri - yOri * (1 - 0.1f) * value.get(xValue.get(i)) / yValue.get(yValue.size() - 1);
path.lineTo(x, y);
}
canvas.drawPath(path, linePaint);
}
/**
* 繪制XY坐標(biāo)
*
* @param canvas
*/
private void drawXY(Canvas canvas) {
int length = dpToPx(4);//刻度的長(zhǎng)度
//繪制Y坐標(biāo)
canvas.drawLine(xOri - xylinewidth / 2, 0, xOri - xylinewidth / 2, yOri, xyPaint);
//繪制y軸箭頭
xyPaint.setStyle(Paint.Style.STROKE);
Path path = new Path();
path.moveTo(xOri - xylinewidth / 2 - dpToPx(5), dpToPx(12));
path.lineTo(xOri - xylinewidth / 2, xylinewidth / 2);
path.lineTo(xOri - xylinewidth / 2 + dpToPx(5), dpToPx(12));
canvas.drawPath(path, xyPaint);
//繪制y軸刻度
int yLength = (int) (yOri * (1 - 0.1f) / (yValue.size() - 1));//y軸上面空出10%,計(jì)算出y軸刻度間距
for (int i = 0; i < yValue.size(); i++) {
//繪制Y軸刻度
canvas.drawLine(xOri, yOri - yLength * i + xylinewidth / 2, xOri + length, yOri - yLength * i + xylinewidth / 2, xyPaint);
xyTextPaint.setColor(xytextcolor);
//繪制Y軸文本
String text = yValue.get(i) + "";
Rect rect = getTextBounds(text, xyTextPaint);
canvas.drawText(text, 0, text.length(), xOri - xylinewidth - dpToPx(2) - rect.width(), yOri - yLength * i + rect.height() / 2, xyTextPaint);
}
//繪制X軸坐標(biāo)
canvas.drawLine(xOri, yOri + xylinewidth / 2, width, yOri + xylinewidth / 2, xyPaint);
//繪制x軸箭頭
xyPaint.setStyle(Paint.Style.STROKE);
path = new Path();
//整個(gè)X軸的長(zhǎng)度
float xLength = xInit + interval * (xValue.size() - 1) + (width - xOri) * 0.1f;
if (xLength < width)
xLength = width;
path.moveTo(xLength - dpToPx(12), yOri + xylinewidth / 2 - dpToPx(5));
path.lineTo(xLength - xylinewidth / 2, yOri + xylinewidth / 2);
path.lineTo(xLength - dpToPx(12), yOri + xylinewidth / 2 + dpToPx(5));
canvas.drawPath(path, xyPaint);
//繪制x軸刻度
for (int i = 0; i < xValue.size(); i++) {
float x = xInit + interval * i;
if (x >= xOri) {//只繪制從原點(diǎn)開始的區(qū)域
xyTextPaint.setColor(xytextcolor);
canvas.drawLine(x, yOri, x, yOri - length, xyPaint);
//繪制X軸文本
String text = xValue.get(i);
Rect rect = getTextBounds(text, xyTextPaint);
if (i == selectIndex - 1) {
xyTextPaint.setColor(linecolor);
canvas.drawText(text, 0, text.length(), x - rect.width() / 2, yOri + xylinewidth + dpToPx(2) + rect.height(), xyTextPaint);
canvas.drawRoundRect(x - xValueRect.width() / 2 - dpToPx(3), yOri + xylinewidth + dpToPx(1), x + xValueRect.width() / 2 + dpToPx(3), yOri + xylinewidth + dpToPx(2) + xValueRect.height() + dpToPx(2), dpToPx(2), dpToPx(2), xyTextPaint);
} else {
canvas.drawText(text, 0, text.length(), x - rect.width() / 2, yOri + xylinewidth + dpToPx(2) + rect.height(), xyTextPaint);
}
}
}
}
5、點(diǎn)擊的處理以及左右
重寫ontouchEven()方法,來處理點(diǎn)擊和滑動(dòng)
@Override
public boolean onTouchEvent(MotionEvent event) {
if (isScrolling)
return super.onTouchEvent(event);
this.getParent().requestDisallowInterceptTouchEvent(true);//當(dāng)該view獲得點(diǎn)擊事件,就請(qǐng)求父控件不攔截事件
obtainVelocityTracker(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
if (interval * xValue.size() > width - xOri) {//當(dāng)期的寬度不足以呈現(xiàn)全部數(shù)據(jù)
float dis = event.getX() - startX;
startX = event.getX();
if (xInit + dis < minXInit) {
xInit = minXInit;
} else if (xInit + dis > maxXInit) {
xInit = maxXInit;
} else {
xInit = xInit + dis;
}
invalidate();
}
break;
case MotionEvent.ACTION_UP:
clickAction(event);
scrollAfterActionUp();
this.getParent().requestDisallowInterceptTouchEvent(false);
recycleVelocityTracker();
break;
case MotionEvent.ACTION_CANCEL:
this.getParent().requestDisallowInterceptTouchEvent(false);
recycleVelocityTracker();
break;
}
return true;
}
點(diǎn)擊的處理是計(jì)算當(dāng)前點(diǎn)擊的X、Y坐標(biāo)范圍進(jìn)行判斷點(diǎn)擊的是那個(gè)點(diǎn)
/**
* 點(diǎn)擊X軸坐標(biāo)或者折線節(jié)點(diǎn)
*
* @param event
*/
private void clickAction(MotionEvent event) {
int dp8 = dpToPx(8);
float eventX = event.getX();
float eventY = event.getY();
for (int i = 0; i < xValue.size(); i++) {
//節(jié)點(diǎn)
float x = xInit + interval * i;
float y = yOri - yOri * (1 - 0.1f) * value.get(xValue.get(i)) / yValue.get(yValue.size() - 1);
if (eventX >= x - dp8 && eventX <= x + dp8 &&
eventY >= y - dp8 && eventY <= y + dp8 && selectIndex != i + 1) {//每個(gè)節(jié)點(diǎn)周圍8dp都是可點(diǎn)擊區(qū)域
selectIndex = i + 1;
invalidate();
return;
}
//X軸刻度
String text = xValue.get(i);
Rect rect = getTextBounds(text, xyTextPaint);
x = xInit + interval * i;
y = yOri + xylinewidth + dpToPx(2);
if (eventX >= x - rect.width() / 2 - dp8 && eventX <= x + rect.width() + dp8 / 2 &&
eventY >= y - dp8 && eventY <= y + rect.height() + dp8 && selectIndex != i + 1) {
selectIndex = i + 1;
invalidate();
return;
}
}
}
處理滑動(dòng)的原理,就是通過改變第一個(gè)點(diǎn)的X坐標(biāo),通過改變這個(gè)基本點(diǎn),依次改變后面的X軸的點(diǎn)的坐標(biāo)。
最后在布局里面應(yīng)用就可以啦,我就不貼代碼啦!
總結(jié):
項(xiàng)目還是有缺點(diǎn)的:
(1)左右滑動(dòng)時(shí),抬起手指仍然可以快速滑動(dòng);代碼里面給出了一種解決方案,但是太過于暫用資源,沒有特殊要求不建議使用,所以給出一個(gè)boolean類型的自定義屬性isScroll,true:?jiǎn)?dòng),反之亦然;還有一種解決方案就是外面再加一層橫向ScrollView,請(qǐng)讀者自行解決,也很簡(jiǎn)單,只需要稍作修改即可。
(2)點(diǎn)擊的時(shí)候忘記添加回調(diào),只有添加了回調(diào)在可以在activity或者fragment里面獲取點(diǎn)擊的內(nèi)容;代碼很簡(jiǎn)單,自行腦補(bǔ)。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- 詳解Android圖表 MPAndroidChart折線圖
- MPAndroidChart開源圖表庫的使用介紹之餅狀圖、折線圖和柱狀圖
- Android MPAndroidChart開源庫圖表之折線圖的實(shí)例代碼
- Android自定義View實(shí)現(xiàn)折線圖效果
- Android繪制動(dòng)態(tài)折線圖
- Android HelloChart開源庫圖表之折線圖的實(shí)例代碼
- Android開發(fā)之天氣趨勢(shì)折線圖
- Android自定義控件實(shí)現(xiàn)折線圖
- Android自定義View簡(jiǎn)易折線圖控件(二)
- Android開發(fā)RecyclerView實(shí)現(xiàn)折線圖效果
相關(guān)文章
Android 清除SharedPreferences 產(chǎn)生的數(shù)據(jù)(實(shí)例代碼)
項(xiàng)目是要保存上次文件播放的位置,我使用SharedPreferences來保存,鍵值對(duì)分別是文件路徑和當(dāng)時(shí)播放的位置2013-11-11
Android超詳細(xì)講解組件LinearLayout的使用
LinearLayout又稱作線性布局,是一種非常常用的布局。正如它的名字所描述的一樣,這個(gè)布局會(huì)將它所包含的控件在線性方向上依次排列。既然是線性排列,肯定就不僅只有一個(gè)方向,這里一般只有兩個(gè)方向:水平方向和垂直方向2022-03-03
Android實(shí)現(xiàn)文件解壓帶進(jìn)度條功能
本文通過實(shí)例代碼給大家介紹了android實(shí)現(xiàn)文件解壓帶進(jìn)度條效果,代碼簡(jiǎn)單易懂,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友參考下吧2017-08-08
Android RenderScript實(shí)現(xiàn)高斯模糊
這篇文章主要為大家詳細(xì)介紹了Android RenderScript實(shí)現(xiàn)高斯模糊的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-12-12
Android文件存儲(chǔ)SharedPreferences源碼解析
SharedPreferences是安卓平臺(tái)上一個(gè)輕量級(jí)的存儲(chǔ)類,用來保存應(yīng)用的一些常用配置,比如Activity狀態(tài),Activity暫停時(shí),將此activity的狀態(tài)保存到SharedPereferences中;當(dāng)Activity重載,系統(tǒng)回調(diào)方法onSaveInstanceState時(shí),再從SharedPreferences中將值取出2022-08-08
Android開發(fā)之設(shè)置開機(jī)自動(dòng)啟動(dòng)的幾種方法
這篇文章主要介紹了Android開發(fā)之設(shè)置開機(jī)自動(dòng)啟動(dòng)的幾種方法的相關(guān)資料,這里提供三種方法幫助大家實(shí)現(xiàn)這樣的功能,需要的朋友可以參考下2017-08-08
Windows下Flutter+Idea環(huán)境搭建及配置
這篇文章介紹了Windows下Flutter+Idea環(huán)境搭建及配置的方法,對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-12-12
Android ViewPager制作新手導(dǎo)航頁(動(dòng)態(tài)加載)
這篇文章主要為大家詳細(xì)介紹了Android ViewPager制作新手導(dǎo)航頁,了解什么是動(dòng)態(tài)加載指示器,感興趣的小伙伴們可以參考一下2016-05-05

