美文网首页Android自定义View
自定义View_撸一个多层折线图

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

作者: BraveJoy | 来源:发表于2018-09-17 17:02 被阅读186次

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

    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传送门:源码

    相关文章

      网友评论

      本文标题:自定义View_撸一个多层折线图

      本文链接:https://www.haomeiwen.com/subject/lbfknftx.html