前言
前四张已经讲完,到目前为止你应该已经在Android屏幕上添加了一个view。怎么绘制呢?接下来我们要用一个例子讲起。

如果不考虑Canvas是怎么来的话,我们将用一个例子来讲Canvas、path、paint。

例子:
我们设置一个简单的数据来绘制一个走势图。
下面是效果图:

上面是一个没有子view的view,所以我们直接继承view。上图只包含一个简单曲线,最大值线,最小值线,0点坐标轴分界线,最大值文本、最小值文本需要绘制。其他复杂的绘制,我们以后再讲。
确定了要做的工作:新建一个类,继承view,因为最大值、最小值线只是数值不同,类型,颜色相同。所以需要定义4个paint,4个path。我们只在xml中引用组件,所以只写了两个参数的构造方法。初始化代码:

public class LineChartView extends View {
    private float lineWidthDP = 2f;//曲线宽度dp

    private  int textSize = 15;//字体大小
    private Paint linePaint;//曲线画笔
    private Paint xyLinePaint; //坐标轴画笔
    private Paint textPaint; //文本画笔
    private Paint dottedLinePaint;//虚线画笔


    private Path linePath;//曲线路径
    private Path yPath; //坐标轴路径
    private Path minDottedPath; //最小值路径
    private Path maxDottedPath; //最大值路径

    public LineChartView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);


        linePath = new Path();
        yPath = new Path();
        minDottedPath = new Path();
        maxDottedPath = new Path();

        linePaint = new Paint();
        linePaint.setAntiAlias(true);//抗锯齿
        linePaint.setStyle(Paint.Style.STROKE);//STROKE描边FILL填充
        linePaint.setColor(App.getInstance().getResources().getColor(R.color.lineColor));
        linePaint.setStrokeWidth(dip2px(lineWidthDP));//边框宽度
        dottedLinePaint = new Paint();
        dottedLinePaint.setStyle(Paint.Style.STROKE);
        dottedLinePaint.setAntiAlias(true);
        dottedLinePaint.setPathEffect(new DashPathEffect(new float[]{4, 4}, 0));
        dottedLinePaint.setColor(Color.BLACK);
        dottedLinePaint.setStrokeWidth(dip2px(lineWidthDP));//边框宽度

        xyLinePaint = new Paint();
        xyLinePaint.setAntiAlias(true);//抗锯齿
        xyLinePaint.setStyle(Paint.Style.STROKE);//STROKE描边FILL填充
        xyLinePaint.setColor(Color.BLACK);
        xyLinePaint.setStrokeWidth(dip2px(lineWidthDP));//边框宽度

        textPaint = new Paint();
        textPaint.setAntiAlias(true);
        textPaint.setStyle(Paint.Style.FILL);
        textPaint.setTextAlign(Paint.Align.CENTER);
        textPaint.setColor(Color.BLACK);//文本颜色
        textPaint.setTextSize(sp2px(textSize));//字体大小
    }

    private int dip2px(float dipValue) {
        final float scale = getResources().getDisplayMetrics().density;
        return (int) (dipValue * scale + 0.5f);
    }

    private int sp2px(float spValue) {
        final float fontScale = getResources().getDisplayMetrics().scaledDensity;
        return (int) (spValue * fontScale + 0.5f);
    }
}

接下来是measure 、layout、draw三剑客,因为我们是没有包含子view的派生类,所以只覆写measure 、draw。
因为子类容器的大小会受到自身布局和父类specSize的影响。也就是说子容器等尺寸总是小于等于父容器的尺寸。所以我们此时measure取值

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int height = MeasureSpec.getSize(heightMeasureSpec);//父类期望的高度
    if (MeasureSpec.EXACTLY == heightMode) {
        height = getPaddingTop() + getPaddingBottom() + height;
    }
    setMeasuredDimension(width , height);//存储宽高
}

Draw则我们只取步骤1,步骤3:画背景,画内容。
画背景好说,那么我们要画的内容是什么呢?就是在一个坐标系中顺序描述每一个点,所以需要计算出第四象限中每个点在y轴上的百分比。

private int getValueHeight(int value) {
    float valuePercent = Math.abs(value - minValue) * 100f / (Math.abs(maxValue - minValue) * 100f);//计算value所占百分比
    return (int) (getViewDrawHeight() * valuePercent + bottomSpace + 0.5f);//底部加上间隔
}

Path

The Path class encapsulates compound (multiple contour) geometric paths consisting of straight line segments, quadratic curves, and cubic curves.
It can be drawn with canvas.drawPath(path, paint), either filled or stroked (based on the paint's Style), or it can be used for clipping or to draw text on a path

封装了直线、二次曲线、三次曲线。本文中会用到贝塞尔曲线,那Path常用方法:

本次需要了解的是cubicTo三次被塞尔曲线,贝塞尔曲线原理不多做解释。任何线都是点链接而成。贝塞尔曲线中有两类点:
控制点和数据点。控制点确定曲线的弯度,数据点确定线的起止和结束位置。以三次贝塞尔曲线为例:

/**
 * Add a cubic bezier from the last point, approaching control points
 * (x1,y1) and (x2,y2), and ending at (x3,y3). If no moveTo() call has been
 * made for this contour, the first point is automatically set to (0,0).
 *
 * @param x1 The x-coordinate of the 1st control point on a cubic curve
 * @param y1 The y-coordinate of the 1st control point on a cubic curve
 * @param x2 The x-coordinate of the 2nd control point on a cubic curve
 * @param y2 The y-coordinate of the 2nd control point on a cubic curve
 * @param x3 The x-coordinate of the end point on a cubic curve
 * @param y3 The y-coordinate of the end point on a cubic curve
 */
public void cubicTo(float x1, float y1, float x2, float y2,
                    float x3, float y3) {
    isSimplePath = false;
    nCubicTo(mNativePath, x1, y1, x2, y2, x3, y3);
}

以最后一个点作为开始点,以(x1,y1)、(x2,y2)为控制点、(x3,y3)为结束点。
所以为linePath设置第一个点,作为控制点。

int stepTemp = getTableStart() + textStartPadding + lineStartPadd;
Point pre = new Point();
pre.set(stepTemp, -getValueHeight(dataList.get(0).getValue()));
linePath.moveTo(pre.x, pre.y);

pre设置开始点x的位置,和第一个点在当前x点y轴上的位置。有了第一个点,则开始添加(x1,y1)、(x2,y2)、(x3,y3)点。

for (int i = 1; i < dataList.size(); i++) {
    Data data = dataList.get(i);
    Point next = new Point();
    next.set(stepTemp += stepSpace, -getValueHeight(data.getValue()));
    int cW = pre.x + stepSpace / 2;
    Point p1 = new Point();
    p1.set(cW, pre.y);
    Point p2 = new Point();
    p2.set(cW, next.y);
    linePath.cubicTo(p1.x, p1.y, p2.x, p2.y, next.x, next.y);
    pre = next;
}

到此主要path工作已经初始化完成,最值线和坐标轴线如下所示:

for (int ii = 0; ii < dataList.size(); ii++) { if (min > Math.abs(dataList.get(ii).getValue())) {
        min = Math.abs(dataList.get(ii).getValue());
    }
    if (miValue > dataList.get(ii).getValue()) {
        miValue = dataList.get(ii).getValue();
    }
    if (maValue < dataList.get(ii).getValue()) {
        maValue = dataList.get(ii).getValue();
    }
}

int width = tablePadding + getTableEnd() + getPaddingLeft() + getPaddingRight();//计算自己的宽度
yPath.moveTo(stepSpace + dip2px(lineStartPadd), -getValueHeight(0));
minDottedPath.moveTo(stepSpace + dip2px(lineStartPadd), -getValueHeight(miValue));
maxDottedPath.moveTo(stepSpace + dip2px(lineStartPadd), -getValueHeight(maValue));
minDottedPath.lineTo(width, -getValueHeight(miValue));
maxDottedPath.lineTo(width, -getValueHeight(maValue));
yPath.lineTo(width, -getValueHeight(0));

接下来,则是把初始化完成的path,通过canvas绘制出来。
Canvas

canvas.drawPath(linePath, linePaint);
    canvas.drawPath(yPath, xyLinePaint);
    canvas.drawPath(minDottedPath, dottedLinePaint);
    canvas.drawPath(maxDottedPath, dottedLinePaint);
    canvas.drawText("0", stepSpace + dip2px(textStartPadding), -(getValueHeight(0)+baseLineY), textPaint);
    canvas.drawText(String.valueOf(miValue), stepSpace + dip2px(textStartPadding), -(getValueHeight(miValue)+baseLineY), textPaint);
    canvas.drawText(String.valueOf(maValue), stepSpace + dip2px(textStartPadding), -(getValueHeight(maValue)+baseLineY), textPaint);

canvas功能很强大,方法很多,作为Android 2D绘制的基础可操控强,比较难用。各个方法之间没有比较成熟的固定搭配。想要使用的好,还得多多练习。
Canvas常用方法:

通过上面简单的例子,希望可以让你基本了解下path,canvas的基本用法。自定义view想要熟练用好,还要多练,多思考。

书写不易,点赞鼓励!

发表评论

邮箱地址不会被公开。