Android自定义View

自定义View_撸一个多层折线图

2018-09-17  本文已影响186人  BraveJoy

看到这个标题,可能有点发懵,啥叫多层折线图啊?这个是我自己取的名字,是因为那天我遇到了这样一个需求。

UI图.png

呐!这还是一个宝塔型的折线图,根据常识,很容易就知道这里面的交互逻辑:一指多控。曾经有一个华丽的需求摆在我的面前,我没有珍惜,后来出了bug被客户怼我才追悔莫及,如果上天能再给我一次机会的话,我一定要自己写一个出来。于是,就有了下面的效果。

效果图.gif

如果gif加载失败,请看这里~

折线图.jpg

这里面全部都是使用canvas绘制的,比如画折线canvas.drawPath,画圆点drawCircle,画坐标线canvas.drawLine,画文字canvas.drawText等等。代码注释写的也比较详细,就不一一介绍了。直接上代码:

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
 * 多层折线图控件
 * Created by zhuyong on 2018/8/30.
 */

public class MyChatView extends View {

    private Context mContext;

    private Paint mPaintLine;//折线图
    private Paint mPaintCircle;//圆的外边框
    private Paint mPaintPoint;//圆内填充
    private Paint mPaintBottomLine;//底部X轴
    private Paint mPaintLimit;//指示线
    private Paint mPaintText;//底部X坐标文字
    private int mBottomTextHeight = 50;//底部X轴文字所占总高度,单位dp
    private int mSingleLineHeight = 100;//单个折线图的高度,单位dp
    private int mPaddingTB = 10;//折线图上下的偏移量,单位dp
    private int mLineColor;//折线图的颜色
    protected int[] mColors;//几种颜色
    private List<List<MyModel>> mListAll = new ArrayList<>();//数据源
    private int mViewWidth;//控件宽高
    private int mViewHeight;//控件宽高

    public MyChatView(Context context) {
        this(context, null);
    }

    public MyChatView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyChatView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
        initView();
    }

    /**
     * 赋值
     *
     * @param list
     */
    public void setData(List<List<MyModel>> list) {
        if (list == null || list.size() == 0) {
            return;
        }
        this.mListAll = list;
        invalidate();
    }

    /**
     * 设置折线图颜色
     *
     * @param position
     */
    private void setLineColor(int position) {
        mLineColor = mColors[position % mColors.length];
        mPaintLine.setColor(mLineColor);
        mPaintCircle.setColor(mLineColor);
    }

    private void initView() {
        mColors = new int[]{ContextCompat.getColor(mContext, R.color.colorAccent)
                , ContextCompat.getColor(mContext, R.color.colorPrimary)};

        mPaintLine = new Paint();
        mPaintLine.setStyle(Paint.Style.STROKE);
        mPaintLine.setStrokeWidth(2);
        mPaintLine.setAntiAlias(true);

        mPaintCircle = new Paint();
        mPaintCircle.setStyle(Paint.Style.STROKE);
        mPaintCircle.setStrokeWidth(3);
        mPaintCircle.setAntiAlias(true);

        mPaintPoint = new Paint();
        mPaintPoint.setStyle(Paint.Style.FILL);
        mPaintPoint.setColor(Color.WHITE);
        mPaintPoint.setAntiAlias(true);

        mPaintBottomLine = new Paint();
        mPaintBottomLine.setStyle(Paint.Style.STROKE);
        mPaintBottomLine.setStrokeWidth(3);
        mPaintBottomLine.setColor(Color.parseColor("#999999"));
        mPaintBottomLine.setAntiAlias(true);

        mPaintLimit = new Paint();
        mPaintLimit.setStyle(Paint.Style.FILL);
        mPaintLimit.setStrokeWidth(2);
        mPaintLimit.setColor(Color.parseColor("#000000"));
        mPaintLimit.setAntiAlias(true);

        //画笔->绘制字体
        mPaintText = new Paint();
        mPaintText.setAntiAlias(true);
        mPaintText.setStyle(Paint.Style.FILL);
        mPaintText.setColor(Color.parseColor("#666666"));
        mPaintText.setTextSize(sp2px(mContext, 14));

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        for (int jjj = 0; jjj < mListAll.size(); jjj++) {
            List<MyModel> itemList = mListAll.get(jjj);
            if (itemList != null && itemList.size() > 0) {
                float mMaxVal = Collections.max(itemList, new MyComparator()).getVal();
                Log.i("TAG", "最大值:" + mMaxVal);
                setLineColor(jjj);
                Path path = new Path();
                List<Point> pointList = new ArrayList<>();
                for (int i = 0; i < itemList.size(); i++) {
                    int xDiv = 0;
                    if (itemList.size() > 1) {
                        xDiv = (mViewWidth - getPaddingLeft() - getPaddingRight()) / (itemList.size() - 1);
                    }
                    MyModel item = itemList.get(i);
                    float x = i * xDiv;
                    float y = item.getVal() * (dip2px(mContext, mSingleLineHeight - mPaddingTB * 2)) / mMaxVal;

                    y = ((dip2px(mContext, mSingleLineHeight)) * (jjj + 1)) - dip2px(mContext, mPaddingTB * 2) - y;

                    if (i == 0) {
                        path.moveTo(x + getPaddingLeft(), y + dip2px(mContext, mPaddingTB));
                    } else {
                        path.lineTo(x + getPaddingLeft(), y + dip2px(mContext, mPaddingTB));
                    }
                    /**
                     * 这里记录一下xy坐标,用于后面绘制小球
                     */
                    Point point = new Point();
                    point.x = (int) x;
                    point.y = (int) y;
                    pointList.add(point);
                }
                //画折线
                canvas.drawPath(path, mPaintLine);
                //画小圆球
                drawCircle(canvas, pointList, jjj);
                //画文字
                if (jjj == mListAll.size() - 1) {
                    drawText(canvas, pointList);
                }
            }
        }

        /**
         * 画竖线,指示线
         */
        if (mLineX > 0) {
            canvas.drawLine(mLineX, 0, mLineX, mViewHeight - dip2px(mContext, mBottomTextHeight), mPaintLimit);
        }
    }

    /**
     * 画圆和底部X轴
     *
     * @param canvas
     * @param pointList
     */
    private void drawCircle(Canvas canvas, List<Point> pointList, int jjj) {
        for (int i = 0; i < pointList.size(); i++) {
            Point point = pointList.get(i);
            //画圆圈
            canvas.drawCircle(point.x + getPaddingLeft(), point.y + dip2px(mContext, mPaddingTB), 10, mPaintCircle);
            if (position == i && mLineX > 0) {
                mPaintPoint.setColor(mLineColor);
            } else {
                mPaintPoint.setColor(Color.WHITE);
            }
            //填充圆内空间
            canvas.drawCircle(point.x + getPaddingLeft(), point.y + dip2px(mContext, mPaddingTB), 9, mPaintPoint);
            //画X轴间隔线
            canvas.drawLine(point.x + getPaddingLeft(), dip2px(mContext, mSingleLineHeight) * (jjj + 1), point.x + getPaddingLeft(), dip2px(mContext, mSingleLineHeight) * (jjj + 1) - dip2px(mContext, 5), mPaintBottomLine);
        }

        //底部X轴
        canvas.drawLine(0, dip2px(mContext, mSingleLineHeight) * (jjj + 1), mViewWidth, dip2px(mContext, mSingleLineHeight) * (jjj + 1), mPaintBottomLine);

    }

    /**
     * 画文字
     *
     * @param canvas
     * @param pointList
     */
    private void drawText(Canvas canvas, List<Point> pointList) {
        for (int i = 0; i < pointList.size(); i++) {
            Point point = pointList.get(i);
            //画底部文字
            String text = (i + 1) + "";
            //获取文字宽度
            float textWidth = mPaintText.measureText(text, 0, text.length());
            float dx = point.x + getPaddingLeft() - textWidth / 2;
            Paint.FontMetricsInt fontMetricsInt = mPaintText.getFontMetricsInt();
            float dy = (fontMetricsInt.bottom - fontMetricsInt.top) / 2 - fontMetricsInt.bottom;
            float baseLine = dip2px(mContext, mSingleLineHeight) * mListAll.size() + dip2px(mContext, mBottomTextHeight / 2) + dy;
            canvas.drawText(text, dx, baseLine, mPaintText);
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        /**
         * 这里根据数据有多少组来动态计算整个view的高度,然后重新设置尺寸
         */
        mViewHeight = dip2px(mContext, mSingleLineHeight) * mListAll.size() + dip2px(mContext, mBottomTextHeight);
        mViewWidth = MeasureSpec.getSize(widthMeasureSpec);
        setMeasuredDimension(mViewWidth, mViewHeight);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
                getPointLine(event.getX());
        }

        return true;
    }

    private float mLineX = 0;
    private int position = 0;

    /**
     * 判断触摸的坐标距离哪个点最近
     *
     * @param mRawX
     */
    private void getPointLine(float mRawX) {
        if (mListAll == null || mListAll.size() == 0) {
            return;
        }
        float newLineX = 0;
        //触摸在折线区域
        if (mRawX <= mViewWidth - getPaddingRight() && mRawX >= getPaddingLeft()) {
            if (mListAll.get(0).size() == 1) {
                newLineX = getPaddingLeft();
                position = 0;
            } else {
                for (int i = 0; i < mListAll.get(0).size(); i++) {
                    int xDiv = 0;
                    if (mListAll.get(0).size() > 1) {
                        xDiv = (mViewWidth - getPaddingLeft() - getPaddingRight()) / (mListAll.get(0).size() - 1);
                    }

                    float x1 = i * xDiv + getPaddingLeft();
                    float x2 = (i + 1) * xDiv + getPaddingLeft();
                    //判断触摸在两个点之间时,离谁更近一些
                    if (mRawX > x1 && mRawX < x2) {
                        float cneterX = x1 + (x2 - x1) / 2;
                        if (mRawX > cneterX) {
                            newLineX = x2;
                            position = i + 1;
                            if (position == mListAll.get(0).size()) {
                                position = i;
                            }
                        } else {
                            newLineX = x1;
                            position = i;
                        }
                        break;
                    }
                }
            }
        } else if (mRawX < getPaddingLeft()) {//触摸在折线左边
            newLineX = getPaddingLeft();
            position = 0;
        } else {//触摸在折线右边
            if (mListAll.get(0).size() == 1) {
                newLineX = getPaddingLeft();
                position = 0;
            } else {
                newLineX = mViewWidth - getPaddingRight();
                position = mListAll.get(0).size() - 1;
            }
        }
        /**
         * 这里判断如果跟上次的触摸结果一样,则不处理
         */
        if (mLineX == newLineX) {
            return;
        }
        mLineX = newLineX;

        notifyUI(mLineX);

    }

    /**
     * 选中某一组
     *
     * @param position
     */
    public void setPosition(int position) {
        try {
            this.position = position;
            int xDiv = (mViewWidth - getPaddingLeft() - getPaddingRight()) / (mListAll.get(0).size() - 1);
            mLineX = position * xDiv + getPaddingLeft();

            notifyUI(mLineX);
        } catch (Exception e) {
            e.printStackTrace();
            Log.i("MyChatView", "Exception:" + e);
        }
    }

    private void notifyUI(float mLineX) {
        this.mLineX = mLineX;
        if (onClickListener != null) {
            onClickListener.click(position);
        }
        invalidate();
    }

    private OnClickListener onClickListener;

    public void setOnClickListener(OnClickListener listener) {
        this.onClickListener = listener;
    }


    public interface OnClickListener {
        void click(int position);
    }

    public static int dip2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

    public static int sp2px(Context context, float spValue) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (spValue * fontScale + 0.5f);
    }

    private class MyComparator implements Comparator<MyModel> {
        public int compare(MyModel o1, MyModel o2) {
            return (o1.getVal() < o2.getVal() ? -1 : (o1.getVal() == o2.getVal() ? 0 : 1));
        }
    }

}

使用:

public class MainActivity extends AppCompatActivity {

    private MyChatView view1;
    private TextView tv_text;
    private List<List<MyModel>> mListAll = new ArrayList<>();

    /**
     * 获取随机数
     *
     * @param range
     * @param startsfrom
     * @return
     */
    protected float getRandom(float range, float startsfrom) {
        return (float) (Math.random() * range) + startsfrom;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        view1 = (MyChatView) findViewById(R.id.view1);
        tv_text = (TextView) findViewById(R.id.tv_text);

        for (int i = 0; i < 3; i++) {
            List<MyModel> item = new ArrayList<>();
            for (int i1 = 0; i1 < 15; i1++) {
                item.add(new MyModel(i1, getRandom(1000, 500)));
            }
            mListAll.add(item);
        }

        view1.setData(mListAll);

        view1.setOnClickListener(new MyChatView.OnClickListener() {
            @Override
            public void click(int position) {
                update(position);
            }
        });

        findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                /**
                 * 设置默认选中第几组数据
                 */
                view1.setPosition(new Random().nextInt(15));
            }
        });
    }

    private void update(int position) {
        tv_text.setText("");
        tv_text.append("第" + (position + 1) + "组:\n");
        for (int i = 0; i < mListAll.size(); i++) {
            tv_text.append("第" + i + "个数据:" + mListAll.get(i).get(position).getVal() + "\n");
        }

    }

}

GitHub传送门:源码

上一篇下一篇

猜你喜欢

热点阅读