Beginners learn to customize View series (1) Demo

Brief description:

  1. Why write another Demo blog?

    The last blog gave an example of a line chart at the end. I remember that the next blog gave its source code, but then I thought about it again. Since we are a custom View of the novice series, the content needs to be detailed. Clear and understandable. If you throw a bunch of source code at random, it is too embarrassing for a novice. Every one of our old drivers has come from a novice. I believe that everyone is very helpless about throwing a bunch of code bloggers at every turn. Today we will briefly analyze this demo.

  2. Answering the question from the previous blog?

    The last blog talked about how to implement click and touch feedback on a specific area in a custom View. We all know that the only thing that can be set to click on the View independent single component is to set the click event on the entire View. Can click touch feedback also be implemented for a certain area of ​​a View control? The answer is yes, the histogram of the example in this issue is only clicked on the rectangle to trigger the event, not the entire View control. This is actually a Region area that uses a special API. It can frame a specific area of ​​a VIew, and then in the View's OnTouchEvent event, monitor whether the finger pressed and lifted points fall into the corresponding Region area, and if it falls, a callback will be given, it's that simple and rude .

  3. Custom View histogram idea analysis?

    1. First of all, from the perspective of the entire control, any control implementation must include two aspects: data interface definition and UI rendering.

    2. For the data interface definition, we can see from this control that there are two data factors, one is the name corresponding to the type (String), and the other is the proportion of the maximum value of the corresponding type (float), which more intuitively reflects the height of the drawing. Two data factors, many people think to write a class package, I personally feel that the simpler the data type is for the direct rendering data of the View layer, the stronger its versatility. So when I think of it, I can pass in a List < Pair < ​​String, float > > from the outside.

    3. For UI rendering, consider two aspects: static drawing and click interaction.

    4. For static drawing, it can be divided into pure histogram drawing and line graph drawing.

    5. The drawing of the histogram includes coordinate system drawing, text collection drawing, and rectangle collection drawing.

    6. For coordinate system drawing, in order to calculate the drawing coordinates conveniently, the original default is the coordinate origin of the upper left corner of the View, and the canvas is shifted by translate to just move to the origin of the histogram coordinates. The drawing of text is described in the previous blog, but the text collection is drawn through a loop, the rectangle collection is drawn using the loop + path.addRect method, and finally the entire Path is drawn.

    7. The drawing of the line chart includes the drawing of the collection of points and the collection of line segments.

    8. The point collection drawing uses drawPoints to draw multiple points, and the line segment collection drawing uses drawLines to draw multiple lines.

    9. For click interaction, it is more troublesome. The idea is to define a Region with the same size as the View control canvas, let’s define it as globalRegion. Then define a Path and Region for each rectangle. Add each rectangle to the corresponding Path through path.addRect , the path area and the clip area are intersected by region.setPath(path, globalRegion) to capture the coordinate range of the region region corresponding to the path. Finally, each path is added to the global mPath, and each region is added to the mRegionList collection.

    10. By rewriting onTouchEvent, the coordinates of the clicked point are obtained, and by traversing the mRegionList collection, it is judged whether the touch point falls into the position of the Region of the corresponding area. Then redraw it again, and draw a background rectangle of different colors for the corresponding position in the onDraw method.

    11. The last pit is coordinate conversion, because when drawing, we simply move the origin of the coordinate system to the origin of the histogram coordinates in order to calculate the coordinates, but the touch coordinate system in onTouchEvent is still the coordinate system with the upper left corner of the View as the origin, so we need to have The transformation from the touch point coordinate system to the drawing coordinate system. Take advantage of the knowledge of the inverse matrix in the Matrix matrix.

  4. What core knowledge points does this blog contain?

    1. Drawing rectangles in Canvas (already mentioned in the previous blog)

    2. Canvas draw multiple line segments (as mentioned in the previous blog)

    3. Canvas draw multiple points (as mentioned in the previous blog)

    4. Canvas draw text (already mentioned in the previous blog)

    5. The geometric transformation of Canvas uses translate to move the drawing coordinate system (later blog will explain in detail, you can remember it first, it will not be too unfamiliar next time)

    6. The drawing path of Canvas (the next blog will explain in detail, you can remember it first, it will not be too unfamiliar when you talk next time)

    7. The combination of Path and Region in Canvas uses a cropped rectangular area. (Difficulty, will be explained in depth in the next issue)

    8. The use of the Region area judges whether the touch point falls into the range area of ​​the bar graph and gives a callback for touch feedback. (Difficulty, will be explained in depth in the next issue)

    9. After the coordinate system is transformed, the inverse matrix of the Matrix class is used to convert the coordinates of the touch coordinate system to the coordinates of the drawing coordinates. (Difficulty, will be explained in depth in the next issue)

1. Data interface definition List < Pair < ​​String, float > > type

  • Using basic data types is conducive to improving the versatility of View
List<Pair<String,Float>> pairList = new ArrayList<>();      pairList.add(new Pair<>("Java",0.71f));
pairList.add(new Pair<>("Swift",0.61f));
pairList.add(new Pair<>("C",0.26f));
pairList.add(new Pair<>("C++",0.37f));
pairList.add(new Pair<>("Python",0.84f));
pairList.add(new Pair<>("Go",0.6f));
setContentView(new RectChart(this, pairList));//直接给contentView设置自定义View

2. Drawing of coordinate axes

  • For the convenience of drawing and coordinate calculation, the translate displacement canvas is used to move the origin of the upper left corner of the original View to the origin of the histogram (the position of the intersection of the two coordinate axes in the control)
//初始化坐标系画布
    private void initCoordinateCanvas(Canvas canvas) {

        canvas.drawColor(Color.parseColor("#eeeeee"));//绘制画布纯色背景
        canvas.translate(100, mHeight - 100);//平移坐标系,原来View左上角原点移动到直方图原点
        // 获取测量矩阵(逆矩阵) 由于绘制画布的View坐标系被平移了,
        // 但是所处的触摸坐标系并没有改变,需要利用Matrix矩阵实现触摸坐标系向绘制坐标系转化
        if (mMatrix.isIdentity()) {
            canvas.getMatrix().invert(mMatrix);
        }

        float[] opts = {0, 0, mWidth - 200, 0, 0, 0, 0, -(mHeight - 200)};//两条线8个点坐标数据
        canvas.drawLines(opts, mCoordinatePaint);//绘制两条线

        mCoordinatePaint.setStrokeCap(Paint.Cap.ROUND);//绘制点为圆点
        mCoordinatePaint.setStrokeWidth(20f);//设置点的大小
        canvas.drawPoint(0, 0, mCoordinatePaint);//绘制两条线段
    }

3. Drawing of text collection

  • Text drawing uses paint.setTextBounds to get the size of the text
        //初始化文字画笔
        mTextPaint = new Paint();
        mTextPaint.setAntiAlias(true);
        mTextPaint.setColor(Color.parseColor("#C2185B"));
        mTextPaint.setTextSize(40);
        if (mDataList == null || mDataList.isEmpty()) {
            return;
        }
        //先取出数据中所有的文字测量每个文字尺寸,并把每个文字尺寸信息记录在对应的rect中,通过paint.getTextBounds(text,start,end,rect),
        // 最后把每个文字的rect保存在集合中
        for (Pair<String, Float> pair : mDataList) {
            Rect textBound = new Rect();
            mTextPaint.getTextBounds(pair.first, 0, pair.first.length(), textBound);
            mTextBounds.add(textBound);
        }
        //绘制对应矩形居中的文字,通过每个矩形region中的left,left + (矩形宽度 - 文字宽度) / 2 作为文字绘制的起点x, y 取文字高度。因为文字绘制起点很怪异是第一个字的左下角还要向左偏移一点距离。
            private void drawTextList(Canvas canvas) {
        for (int i = 0; i < mRegionList.size(); i++) {
            canvas.drawText(mDataList.get(i).first, mRegionList.get(i).getBounds().left + (mRectWidth / 2 - mTextBounds.get(i).width() / 2), mTextBounds.get(i).height() + 20F, mTextPaint);
        }
    }

3. Drawing of a rectangular set

  • It is more troublesome to draw a rectangular set, you need to crop the region of each rect, and add the corresponding path
mGlobalRegion = new Region(-w, -h, w, h);//创建全局的region

        mPointList.clear();//重置折线图中点集合
        mRegionList.clear();//重置region集合
        for (int i = 0; i < mDataList.size(); i++) {
            //根据每个矩形需要添加一个间隔距离,第一个矩形left就是间隔,第二个就是第一个矩形的left + 间隔
            //反推第N个就是第 N-1 个矩形的left + 间隔。那么这个left =  mGap * (i + 1) + mRectWidth * i
            float left = mGap * (i + 1) + mRectWidth * i;
            //top就是按比例算,负数因为绘制处于Y的负半轴,mHeight-200是为了留出坐标轴顶点距离控件底部距离为200.
            float top = -mDataList.get(i).second * (mHeight - 200);
            float right = left + mRectWidth;
            //-mCoordinatePaint.getStrokeWidth()一个细微处理为了防止矩形绘制,会盖住底部坐标轴
            float bottom = -mCoordinatePaint.getStrokeWidth();

            //创建折线图中每个点,每个点位置也正处于矩形宽度中点位置
            Point point = new Point(left + mRectWidth / 2, top);
            mPointList.add(point);

            //为每个矩形创建一个path
            Path path = new Path();
            //向每个path中添加一个矩形
            path.addRect(left, top, right, bottom, Path.Direction.CW);
            //创建一个region
            Region region = new Region();
            //在全局的mGlobalRegion中裁剪出对应path的矩形范围,并把相应的范围信息保存在region
            region.setPath(path, mGlobalRegion);
            //把每个path添加全局的mPath中
            mPath.addPath(path);
            //保存每个region信息到集合中
            mRegionList.add(region);
        }
    //绘制直方图
    private void drawHistogram(Canvas canvas) {
        canvas.drawPath(mPath, mRectPaint);//绘制最后mPath
        if (mClickPosition != -1) {//判断点击mClickPosition,根据位置重新绘制点击色的矩形
            mRectPaint.setColor(mPressColor);
            canvas.drawRect(mRegionList.get(mClickPosition).getBounds(), mRectPaint);
            mClickPosition = -1;//重置ClickPosition
        }
    }

4. Drawing of a line chart

  • The drawing of a line chart is relatively simple. It is to draw a set of points and a set of line segments. The key is the calculation of coordinates. We calculate the information of the points along with the calculation of the position of the rectangle, and finally save it in the pointList collection, which can be used directly. Can.

//计算点的坐标,创建折线图中每个点,每个点位置也正处于矩形宽度中点位置
            Point point = new Point(left + mRectWidth / 2, top);
            mPointList.add(point);

//绘制折线图
    private void drawPolyline(Canvas canvas) {
        for (int i = 0; i < mPointList.size(); i++) {
            //绘制点交点
            mCoordinatePaint.setStrokeWidth(20f);
            canvas.drawPoint(mPointList.get(i).x, mPointList.get(i).y, mCoordinatePaint);
            //绘制连线
            if (i < mPointList.size() - 1) {
                mCoordinatePaint.setStrokeWidth(5f);
                canvas.drawLine(mPointList.get(i).x, mPointList.get(i).y, mPointList.get(i + 1).x, mPointList.get(i + 1).y, mCoordinatePaint);
            }
        }
    }

5. Implementation of click interaction

  • The core of its implementation is that a region.contain(x, y) method is provided in the region to determine whether the coordinates of the incoming point fall within the current region and return true or false
@Override
    public boolean onTouchEvent(MotionEvent event) {
        float[] pts = new float[2];
        pts[0] = event.getX();
        pts[1] = event.getY();
        mMatrix.mapPoints(pts);//利用Matrix矩阵实现触摸坐标系向绘制坐标系转化

        int touchX = (int) pts[0];
        int touchY = (int) pts[1];

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_UP:
                mClickPosition = findTouchPoint(touchX, touchY);
                if (mClickPosition != -1) {
                    invalidate();
                    Toast.makeText(getContext(), String.format(Locale.US, "当前选中: %s 数据为: %f", mDataList.get(mClickPosition).first, mDataList.get(mClickPosition).second), Toast.LENGTH_SHORT).show();
                }
                break;
        }
        return super.onTouchEvent(event);
    }

    //根据触摸点的坐标找到对应的position
    private int findTouchPoint(int touchX, int touchY) {
        int position = -1;
        for (int i = 0; i < mRegionList.size(); i++) {
            Region region = mRegionList.get(i);
            if (region.contains(touchX, touchY)) {//region中提供一个非常不错的API,region.contains(x,y),可以判断传入点的坐标是否落入当前的region内,返回true or false
                position = i;//触摸点落入对应region也就找到对应region所在position
                return position;
            }
        }
        return position;
    }

6. The pit of coordinate conversion

  • Since the drawing coordinate system was changed for the convenience of drawing at the beginning, but the touch coordinates cannot be changed, the coordinate system touched by the finger and the canvas coordinate system are not unified, which may cause the touch position of the finger and the drawing position to be inconsistent, and only the coordinates of the touch can be converted. into the drawing coordinate system. Matrix knowledge is needed here. One of the biggest functions of Matrix is ​​coordinate mapping and numerical conversion. Later, you will learn about Matrix.
canvas.translate(100, mHeight - 100);//平移坐标系
        // 获取测量矩阵(逆矩阵) 由于绘制画布的View坐标系被平移了,
        // 但是所处的触摸坐标系并没有改变,需要利用Matrix矩阵实现触摸坐标系向绘制坐标系转化
        if (mMatrix.isIdentity()) {
            canvas.getMatrix().invert(mMatrix);
        }

mMatrix.mapPoints(pts);//利用Matrix矩阵实现触摸坐标系向绘制坐标系转化

7. Finally attach all the source code

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.Region;
import android.util.AttributeSet;
import android.util.Pair;
import android.view.MotionEvent;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

public class RectChart extends CanvasView {

    private int mNormalColor;
    private int mPressColor;

    private Paint mRectPaint;//绘制矩形的画笔
    private Paint mTextPaint;//绘制文字的画笔
    private Paint mCoordinatePaint;//绘制坐标轴的画笔

    private Matrix mMatrix;//用于坐标转换的矩阵Matrix的对象
    private Path mPath = new Path();//定义全局所有矩形path集合,最后所有矩形的Path会加入到mPath中
    private Region mGlobalRegion;//定义全局的Region

    private List<Rect> mTextBounds = new ArrayList<>();//为了绘制文字的集合,每个文字都用rect框住,便于拿到文字的宽度和高度。
    private List<Point> mPointList = new ArrayList<>();//为了绘制折线图中点集合,每个point对象包含了x,y坐标
    private List<Region> mRegionList = new ArrayList<>();//为每个矩形集合定义的region集合

    private int mWidth;//控件的宽度
    private int mHeight;//控件的高度
    private float mGap = 40f;//每个矩形之间间隔大小
    private float mRectWidth = 80f;//每个矩形宽度大小
    private boolean isShowHistogram = true;//是否绘制矩形图
    private boolean isShowPolyline = true;//是否绘制折线图
    private int mClickPosition = -1;//用于记录刷选出点击落入对应的region的position

    private List<Pair<String, Float>> mDataList;//数据pair集合,pair对象第一个是用于底部文字绘制的内容,第二个是比例对应所画实际矩形高度。

    public RectChart(Context context, List<Pair<String, Float>> mDataList) {
        super(context);
        this.mDataList = mDataList;
    }

    @Override
    protected void initDrawTools() {
        //初始化矩形画笔
        mRectPaint = new Paint();
        mRectPaint.setAntiAlias(true);
        mRectPaint.setColor(mNormalColor);
        mRectPaint.setStyle(Paint.Style.FILL);

        //初始化坐标系画笔
        mCoordinatePaint = new Paint();
        mCoordinatePaint.setAntiAlias(true);
        mCoordinatePaint.setColor(Color.RED);
        mCoordinatePaint.setStyle(Paint.Style.STROKE);
        mCoordinatePaint.setStrokeWidth(5f);

        //初始化文字画笔
        mTextPaint = new Paint();
        mTextPaint.setAntiAlias(true);
        mTextPaint.setColor(Color.parseColor("#C2185B"));
        mTextPaint.setTextSize(40);
        if (mDataList == null || mDataList.isEmpty()) {
            return;
        }
        //先取出数据中所有的文字测量每个文字尺寸,并把每个文字尺寸信息记录在对应的rect中,通过paint.getTextBounds(text,start,end,rect),
        // 最后把每个文字的rect保存在集合中
        for (Pair<String, Float> pair : mDataList) {
            Rect textBound = new Rect();
            mTextPaint.getTextBounds(pair.first, 0, pair.first.length(), textBound);
            mTextBounds.add(textBound);
        }
        //创建矩阵对象
        mMatrix = new Matrix();
    }

    @Override
    protected void fetchDefAttrValues(Context context, AttributeSet attrs, int defStyleAttr) {
        //该方法用于接收自定义属性的值,本例子还没来得及添加自定义属性
        mNormalColor = Color.parseColor("#ff9900");
        mPressColor = Color.parseColor("#ff0000");
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//      super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(1080, 1000);//测量这里指定大小尺寸的画布即控件大小
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (mMatrix != null) {
            mMatrix.reset();
        }

        mWidth = w;
        mHeight = h;

        mGlobalRegion = new Region(-w, -h, w, h);//创建全局的region

        mPointList.clear();//重置折线图中点集合
        mRegionList.clear();//重置region集合
        for (int i = 0; i < mDataList.size(); i++) {
            //根据每个矩形需要添加一个间隔距离,第一个矩形left就是间隔,第二个就是第一个矩形的left + 间隔
            //反推第N个就是第 N-1 个矩形的left + 间隔。那么这个left =  mGap * (i + 1) + mRectWidth * i
            float left = mGap * (i + 1) + mRectWidth * i;
            //top就是按比例算,负数因为绘制处于Y的负半轴,mHeight-200是为了留出坐标轴顶点距离控件底部距离为200.
            float top = -mDataList.get(i).second * (mHeight - 200);
            float right = left + mRectWidth;
            //-mCoordinatePaint.getStrokeWidth()一个细微处理为了防止矩形绘制,会盖住底部坐标轴
            float bottom = -mCoordinatePaint.getStrokeWidth();

            //创建折线图中每个点,每个点位置也正处于矩形宽度中点位置
            Point point = new Point(left + mRectWidth / 2, top);
            mPointList.add(point);

            //为每个矩形创建一个path
            Path path = new Path();
            //向每个path中添加一个矩形
            path.addRect(left, top, right, bottom, Path.Direction.CW);
            //创建一个region
            Region region = new Region();
            //在全局的mGlobalRegion中裁剪出对应path的矩形范围,并把相应的范围信息保存在region
            region.setPath(path, mGlobalRegion);
            //把每个path添加全局的mPath中
            mPath.addPath(path);
            //保存每个region信息到集合中
            mRegionList.add(region);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        initDrawTools();//初始化绘制工具
        initCoordinateCanvas(canvas);//初始化画布坐标轴

        drawTextList(canvas);//绘制文字集合
        if (isShowHistogram) {//绘制直方图
            drawHistogram(canvas);
        }

        if (isShowPolyline) {//绘制折线图
            drawPolyline(canvas);
        }

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float[] pts = new float[2];
        pts[0] = event.getX();
        pts[1] = event.getY();
        mMatrix.mapPoints(pts);//利用Matrix矩阵实现触摸坐标系向绘制坐标系转化

        int touchX = (int) pts[0];
        int touchY = (int) pts[1];

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_UP:
                mClickPosition = findTouchPoint(touchX, touchY);
                if (mClickPosition != -1) {
                    invalidate();
                    Toast.makeText(getContext(), String.format(Locale.US, "当前选中: %s 数据为: %f", mDataList.get(mClickPosition).first, mDataList.get(mClickPosition).second), Toast.LENGTH_SHORT).show();
                }
                break;
        }
        return super.onTouchEvent(event);
    }

    //根据触摸点的坐标找到对应的position
    private int findTouchPoint(int touchX, int touchY) {
        int position = -1;
        for (int i = 0; i < mRegionList.size(); i++) {
            Region region = mRegionList.get(i);
            if (region.contains(touchX, touchY)) {//region中提供一个非常不错的API,region.contains(x,y),可以判断传入点的坐标是否落入当前的region内,返回true or false
                position = i;//触摸点落入对应region也就找到对应region所在position
                return position;
            }
        }
        return position;
    }

    private void drawTextList(Canvas canvas) {
        for (int i = 0; i < mRegionList.size(); i++) {
            canvas.drawText(mDataList.get(i).first, mRegionList.get(i).getBounds().left + (mRectWidth / 2 - mTextBounds.get(i).width() / 2), mTextBounds.get(i).height() + 20F, mTextPaint);
        }
    }

    //初始化坐标系画布
    private void initCoordinateCanvas(Canvas canvas) {
        canvas.drawColor(Color.parseColor("#eeeeee"));
        canvas.translate(100, mHeight - 100);//平移坐标系
        // 获取测量矩阵(逆矩阵) 由于绘制画布的View坐标系被平移了,
        // 但是所处的触摸坐标系并没有改变,需要利用Matrix矩阵实现触摸坐标系向绘制坐标系转化
        if (mMatrix.isIdentity()) {
            canvas.getMatrix().invert(mMatrix);
        }

        float[] opts = {0, 0, mWidth - 200, 0, 0, 0, 0, -(mHeight - 200)};
        canvas.drawLines(opts, mCoordinatePaint);//绘制两条线

        mCoordinatePaint.setStrokeCap(Paint.Cap.ROUND);
        mCoordinatePaint.setStrokeWidth(20f);
        canvas.drawPoint(0, 0, mCoordinatePaint);
    }

    //绘制直方图
    private void drawHistogram(Canvas canvas) {
        canvas.drawPath(mPath, mRectPaint);//绘制最后mPath
        if (mClickPosition != -1) {//判断点击mClickPosition,根据位置重新绘制点击色的矩形
            mRectPaint.setColor(mPressColor);
            canvas.drawRect(mRegionList.get(mClickPosition).getBounds(), mRectPaint);
            mClickPosition = -1;//重置ClickPosition
        }
    }

    //绘制折线图
    private void drawPolyline(Canvas canvas) {
        for (int i = 0; i < mPointList.size(); i++) {
            //绘制点交点
            mCoordinatePaint.setStrokeWidth(20f);
            canvas.drawPoint(mPointList.get(i).x, mPointList.get(i).y, mCoordinatePaint);
            //绘制连线
            if (i < mPointList.size() - 1) {
                mCoordinatePaint.setStrokeWidth(5f);
                canvas.drawLine(mPointList.get(i).x, mPointList.get(i).y, mPointList.get(i + 1).x, mPointList.get(i + 1).y, mCoordinatePaint);
            }
        }
    }

    class Point {
        private float x;
        private float y;

        public Point(float x, float y) {
            this.x = x;
            this.y = y;
        }
    }

}


end

Even if this example is finished, let's talk about Path related content in the next blog. So much water all at once, so thirsty to drink water. For those who do not know very much knowledge, you can remember it first.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325370640&siteId=291194637