Android MPAndroidChart自適應Markerview

前言


Android裡面只要用過圖表的應該都知道MPAndroidChart這個庫。這個庫在iOS裡面也有對應圖表,所以一般移動端做圖表,Android和iOS兩端都要實現同樣的效果,他們是不錯的一個選擇。
但是,對於圖表這種包含的情況非常複雜的東西,很難滿足大家各種各樣的需求,所以很多都需要自定義。下面就是給大家分享一下自己寫的自適應MarkerView。


圖片:

正常居中顯示
超過上邊界
超過右邊界
超過左邊界

正文

要實現這樣的效果,大家先想想怎麼做?
這裡的邏輯步驟分為:
  1. 創建新類繼承自MarkerView 
  2. inflate layout 進去
  3. 重寫getOffsetForDrawingAtPoint,分別處理各種邊界情況的偏移
  4. 重寫繪製,繪製底色,根據不同的情況,繪製帶箭頭的對話框

直接上代碼

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就在抽籤之前調用

 // 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的簡書

留言

這個網誌中的熱門文章

Android - 使用 adb 安装apk

Android TextView autosizing 自動調整大小

Kotlin - 實現Android中的Parcelable