Android MPAndroidChart自適應Markerview
前言
Android裡面只要用過圖表的應該都知道MPAndroidChart這個庫。這個庫在iOS裡面也有對應圖表,所以一般移動端做圖表,Android和iOS兩端都要實現同樣的效果,他們是不錯的一個選擇。
但是,對於圖表這種包含的情況非常複雜的東西,很難滿足大家各種各樣的需求,所以很多都需要自定義。下面就是給大家分享一下自己寫的自適應MarkerView。
但是,對於圖表這種包含的情況非常複雜的東西,很難滿足大家各種各樣的需求,所以很多都需要自定義。下面就是給大家分享一下自己寫的自適應MarkerView。
圖片:
超過上邊界
超過右邊界
超過左邊界
正文
要實現這樣的效果,大家先想想怎麼做?
這裡的邏輯步驟分為:
- 創建新類繼承自MarkerView
- inflate layout 進去
- 重寫getOffsetForDrawingAtPoint,分別處理各種邊界情況的偏移
- 重寫繪製,繪製底色,根據不同的情況,繪製帶箭頭的對話框
直接上代碼
public class XYMarkerView extends MarkerView {
public static final int ARROW_SIZE = 40; // 箭头的大小
private static final float CIRCLE_OFFSET = 10;//因为我这里的折点是圆圈,所以要偏移,防止直接指向了圆心
private static final float STOKE_WIDTH = 5;//这里对于stroke_width的宽度也要做一定偏移
private final TextView tvContent;
private final RoundImageView avatar;
private final TextView name;
private final List<StepListModel> stepListModels;
private int index;
private int oldIndex = -1;
public XYMarkerView(Context context, List<StepListModel> stepListModels) {
super(context, R.layout.custom_marker_view);
tvContent = (TextView) findViewById(R.id.tvContent);
avatar = (RoundImageView) findViewById(R.id.avatar);
name = (TextView) findViewById(R.id.name);
this.stepListModels = stepListModels;
}
@Override
public void refreshContent(Entry e, Highlight highlight) {
super.refreshContent(e, highlight);
index = highlight.getDataSetIndex();//这个方法用于获得折线是哪根
tvContent.setText((int) e.getY() + "");
// StepListModel stepListModel = stepListModels.get(highlight.getDataSetIndex() % Constants.battleUsersCount);
// name.setText(stepListModel.getNickNm());
// Glide.with(getContext())
// .load(GlideUtil.getGlideUrl(stepListModel.getIconUrl()))
// .into(avatar);
Glide.with(getContext())
.asBitmap()
.load(avatars[index % avatars.length])
.listener(new RequestListener<Bitmap>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Bitmap> target, boolean isFirstResource) {
return false;
}
@Override
public boolean onResourceReady(Bitmap resource, Object model, Target<Bitmap> target, DataSource dataSource, boolean isFirstResource) {
if (resource != null) {
if (oldIndex != index) {
XYMarkerView.this.getChartView().invalidate();
oldIndex = index;
}
avatar.setImageBitmap(resource);
}
return false;
}
})
.into(avatar);
name.setText(highlight.getDataSetIndex() + "");
tvContent.setTextColor(getResources().getColor(ColorUtil.colors[highlight.getDataSetIndex() % ColorUtil.colors.length]));
LogUtil.m("getDataSetIndex" + highlight.getDataSetIndex());
}
@Override
public MPPointF getOffsetForDrawingAtPoint(float posX, float posY) {
MPPointF offset = getOffset();
Chart chart = getChartView();
float width = getWidth();
float height = getHeight();
// posY \posX 指的是markerView左上角点在图表上面的位置
//处理Y方向
if (posY <= height + ARROW_SIZE) {// 如果点y坐标小于markerView的高度,如果不处理会超出上边界,处理了之后这时候箭头是向上的,我们需要把图标下移一个箭头的大小
offset.y = ARROW_SIZE;
} else {//否则属于正常情况,因为我们默认是箭头朝下,然后正常偏移就是,需要向上偏移markerView高度和arrow size,再加一个stroke的宽度,因为你需要看到对话框的上面的边框
offset.y = -height - ARROW_SIZE - STOKE_WIDTH; // 40 arrow height 5 stroke width
}
//处理X方向,分为3种情况,1、在图表左边 2、在图表中间 3、在图表右边
//
if (posX > chart.getWidth() - width) {//如果超过右边界,则向左偏移markerView的宽度
offset.x = -width;
} else {//默认情况,不偏移(因为是点是在左上角)
offset.x = 0;
if (posX > width / 2) {//如果大于markerView的一半,说明箭头在中间,所以向右偏移一半宽度
offset.x = -(width / 2);
}
}
return offset;
}
@Override
public void draw(Canvas canvas, float posX, float posY) {
Paint paint = new Paint();//绘制边框的画笔
paint.setStrokeWidth(STOKE_WIDTH);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setColor(getResources().getColor(ColorUtil.colors[index % ColorUtil.colors.length]));
Paint whitePaint = new Paint();//绘制底色白色的画笔
whitePaint.setStyle(Paint.Style.FILL);
whitePaint.setColor(Color.WHITE);
Chart chart = getChartView();
float width = getWidth();
float height = getHeight();
MPPointF offset = getOffsetForDrawingAtPoint(posX, posY);
int saveId = canvas.save();
Path path = new Path();
if (posY < height + ARROW_SIZE) {//处理超过上边界
path = new Path();
path.moveTo(0, 0);
if (posX > chart.getWidth() - width) {//超过右边界
path.lineTo(width - ARROW_SIZE, 0);
path.lineTo(width, -ARROW_SIZE + CIRCLE_OFFSET);
path.lineTo(width, 0);
} else {
if (posX > width / 2) {//在图表中间
path.lineTo(width / 2 - ARROW_SIZE / 2, 0);
path.lineTo(width / 2, -ARROW_SIZE + CIRCLE_OFFSET);
path.lineTo(width / 2 + ARROW_SIZE / 2, 0);
} else {//超过左边界
path.lineTo(0, -ARROW_SIZE + CIRCLE_OFFSET);
path.lineTo(0 + ARROW_SIZE, 0);
}
}
path.lineTo(0 + width, 0);
path.lineTo(0 + width, 0 + height);
path.lineTo(0, 0 + height);
path.lineTo(0, 0);
path.offset(posX + offset.x, posY + offset.y);
} else {//没有超过上边界
path = new Path();
path.moveTo(0, 0);
path.lineTo(0 + width, 0);
path.lineTo(0 + width, 0 + height);
if (posX > chart.getWidth() - width) {
path.lineTo(width, height + ARROW_SIZE - CIRCLE_OFFSET);
path.lineTo(width - ARROW_SIZE, 0 + height);
path.lineTo(0, 0 + height);
} else {
if (posX > width / 2) {
path.lineTo(width / 2 + ARROW_SIZE / 2, 0 + height);
path.lineTo(width / 2, height + ARROW_SIZE - CIRCLE_OFFSET);
path.lineTo(width / 2 - ARROW_SIZE / 2, 0 + height);
path.lineTo(0, 0 + height);
} else {
path.lineTo(0 + ARROW_SIZE, 0 + height);
path.lineTo(0, height + ARROW_SIZE - CIRCLE_OFFSET);
path.lineTo(0, 0 + height);
}
}
path.lineTo(0, 0);
path.offset(posX + offset.x, posY + offset.y);
}
// translate to the correct position and draw
canvas.drawPath(path, whitePaint);
canvas.drawPath(path, paint);
canvas.translate(posX + offset.x, posY + offset.y);
draw(canvas);
canvas.restoreToCount(saveId);
}
}
詳細
不同顏色,頭像等處理
我這裡的需求是根據不同的折線,顯示不同的人物頭像,名字和對應的值,並且對話框的顏色,字體都要對應折線的顏色。
所以,這裡我根據
所以,這裡我根據
highlight.getDataSetIndex()
就能處理不同顏色,頭像等信息的情況。
//类似这个
name.setText(highlight.getDataSetIndex() + "");
tvContent.setTextColor(getResources().getColor(ColorUtil.colors[highlight.getDataSetIndex() % ColorUtil.colors.length]));
注意:
這裡有個地方要注意一下,這裡如果你使用Glide來加載圖片到ImageView裡去,會出現第一次加載不出來頭像,第二次點擊的時候就加載出來了。
原因:是因為其實Glide已經加載出來了,然後只是異步加載出來之後,已經是在佈局繪製完成之後了,並沒有進行無效的刷新。所以直到第二次的時候,其實滑行已經加載過了,有緩存,所以直接就顯示了,發生在繪製方法之前,因為refreshContent就在抽籤之前調用
這裡有個地方要注意一下,這裡如果你使用Glide來加載圖片到ImageView裡去,會出現第一次加載不出來頭像,第二次點擊的時候就加載出來了。
原因:是因為其實Glide已經加載出來了,然後只是異步加載出來之後,已經是在佈局繪製完成之後了,並沒有進行無效的刷新。所以直到第二次的時候,其實滑行已經加載過了,有緩存,所以直接就顯示了,發生在繪製方法之前,因為refreshContent就在抽籤之前調用
// callbacks to update the content
mMarker.refreshContent(e, highlight);
// draw the marker
mMarker.draw(canvas, pos[0], pos[1]);
所以,這裡使用了一個oldIndex是否等於指數來判斷是否是已經無效,這樣的話就只有點擊不同的折線的時候會無效一次,之後就不會了。
private int index;
private int oldIndex = -1;
...
Glide.with(getContext())
.asBitmap()
.load(avatars[index % avatars.length])
.listener(new RequestListener<Bitmap>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Bitmap> target, boolean isFirstResource) {
return false;
}
@Override
public boolean onResourceReady(Bitmap resource, Object model, Target<Bitmap> target, DataSource dataSource, boolean isFirstResource) {
if (resource != null) {
if (oldIndex != index) {
XYMarkerView.this.getChartView().invalidate();
oldIndex = index;
}
avatar.setImageBitmap(resource);
}
return false;
}
})
.into(avatar);
詳細說明在下方連結
轉自Jafir的簡書
留言
張貼留言