自定义view(二) Path绘画详解 圆形进度条

 

 

       view的绘制可以由无数个形状组成,在canvas基础图形绘制中,我们已经把api提供好的基本图形讲过了。Path之所以单独一章出来是因为path可以由我们自己定义形状。在绝大多数情况下,只靠上篇文章中讲的那些图形并不能实现设计师设计出来那些优美炫酷的效果。当然对于一个炫酷的效果,path也只是一块砖,要想完成炫酷效果,要一步步来。以下做demo的时候要把硬件加速关掉。以免出异常。

  • 基础api

      以下的接口介绍,并没有区分api的版本号,比如API21以后的方法也都包括在内。大家使用的时候要自己注意。其实path的路径可以看成是一系列的点的集合。所以保存这些点都是有顺序的。

          作用                                   相关接口                                           备注
移动起点   moveTo移动到指定位置,当做下一次操作的起点。
连接直线   lineTo从已经完成的最后一个点,连接到指定点的直线
设置最后的点   setLastPoint用参数中指定的点代替原来的终点作为最后一个点
形成闭合回路   close连接最后一个点和第一个点形成一个闭合的图形
rxxxx族rLineTo   rMoveTo  rCubic等

与lineto,moveTo,cubicTo作用一样,只是r开头的都是以path的最后一个点为原点,而不是以(0,0)为原点

添加路径addCircle addRoundRect等add方法将add后面代表的路径等添加到path中.
2个path合并op函数对两个path进行与,非等操作,获取结果
贝塞尔曲线quadTo,cubicTo二次三次贝尔曲线
填充模式setFillType等有关filltype的方法设置变化填充等效果
重置路径reset,remind清除Path中的内容
reset不保留内部数据结构,但会保留FillType.
rewind会保留内部的数据结构,但不保留FillType
矩阵变化transfor以矩阵的形式改变path
位置偏移offset对整体path进行位移
优化内存incReserve提示剩余还有多少个点需要加入到path中。
计算边界computeBounds(Rect ct,boolean flag)计算path边界,并将边界值赋予ct,第二个参数已经无意义。如果path只包含0,或1个点,则返回(0,0,0,0)

    还有其他一些接口,我们看名字就知道其含义,就没有必要一一列举了。

1 add族函数

通过add函数比如addRect(),addCircle()函数,可以为path添加一些固定的图案,但是在此add组函数中,有一个参数是Path.Direction, 这是一个枚举类型,它的值为:CW,CCW,分别为顺时针和逆时针,表示添加的图案是以哪种顺序添加。如果只是添加一个图案没有后续操作,那么这个参数体现不出来,因为它代表团的顺序,不会改变添加的图案。比如下图:

我们移动原点之后,通过add方法顺时针添加A,B,C,D组成的绿色的矩形,因为是顺时针添加,这个时候path的最后一个点是D,而我们通过setLastPoint方法将(-50,600)这个点设置为最终点,因此D就被舍弃,最终形成了黑色path的结果。如果这个时候是逆时针添加。那么最后一个点就是B(220,-100),那么就是(-50,600)代替了B。那么最终的path是由A,D,C,E(-50,600)组成。

 private void drawAddRect(Canvas canvas) {
        mPaint.setStrokeWidth(5);
        mPaint.setColor(Color.RED);
        canvas.translate(500, 500);
        mPaint.setColor(Color.BLACK);
        //顺时针添加一个矩形
        mPath.addRect(new RectF(100, -100, 220, 100), Path.Direction.CW);
        //将最后一个点设置为(-50,400)
        mPath.setLastPoint(-50, 400);
        canvas.drawPath(mPath, mPaint);

    }

下面是通过顺时针,逆时针添加一个圆形,并且沿着圆形书写文字,效果如下:

2 setLastPoint(px,py)

这个接口就是将点(px,py)设置为path的最后一点。就是代替原来的最后一点。具体使用在上面的代码中已经使用。我们可以通过path的源码看出这一点:

//这是setlastpath最终实现方法,属于lib中的skia模块,
//如果当前path是空,那么相当于执行movtTo(),
//否则将path的最后一个点指向此点。原来的最后点舍弃

void SkPath::setLastPt(SkScalar x, SkScalar y) {
    SkDEBUGCODE(this->validate();)

    int count = fPathRef->countPoints();
    if (count == 0) {
        this->moveTo(x, y);
    } else {
        SkPathRef::Editor ed(&fPathRef);
        ed.atPoint(count-1)->set(x, y);
    }
}

3 rLineTo, rMoveTo等r族函数

这类函数和不加r的区别就是,r族的函数都是相对path的最后一个点为标准作出的操作,也就是都是相对位置。不是绝对位置。(绝对位置是指相对于原点的位置)。

private void drawPathClose(Canvas canvas) {
        mPaint.setColor(Color.BLACK);
        mPaint.setStrokeWidth(5);
        //移动到100,100
        mPath.moveTo(100, 100);
        //画(100,100)到(200,350)的直线
        mPath.lineTo(200, 350);
        //再画从(200,350)到(400,80)的直线
        mPath.lineTo(400, 80);
        //以(400,80)为原点,画到(100,100)的点。
        //如果是lineTo(100,100),那么就和起始点重合,整个path应该是一个闭合的图案。
        
        mPath.rLineTo(100,100);
      //  mPath.lineTo(100, 100);
        
    }

效果如下:

我们可以看看rLineTo的底层源码:

void SkPath::rLineTo(SkScalar x, SkScalar y) {
    this->injectMoveToIfNeeded();  // This can change the result of this->getLastPt().
    SkPoint pt;
    this->getLastPt(&pt);
    this->lineTo(pt.fX + x, pt.fY + y);
}

从代码中我们可以看出,首先获取到path的最后一个点pt,然后再执行lineTo(pt.fx+x, pt.fy+y), 这就是相对最后一个点做的移动,如果以原点看的话,他的绝对位移应该是pt.px+x, 和pt.fy+y, . 所以一定要分清楚相对和绝对的关系。

4 offset偏移

offset函数重载了2个方法,offSet(tx,ty) , offSet(tx,ty,Path dst), 第一个函数就是直接将path的每个点的坐标(x,y)做位移操作 x+tx, y+ty. 第二个函数首先要将当前的path复制给dst,然后对dst做操作,所以移动之后dst是操作后的path。原来的path并没有变化。

这一点我们可以通过源码看到:

//先将当前path复制到dst。然后对dst做offset(dx,dy)的操作

public void offset(float dx, float dy, @Nullable Path dst) {
        if (dst != null) {
            dst.set(this);
        } else {
            dst = this;
        }
        dst.offset(dx, dy);
}

我们可以可以看如下代码:

private void drawOffSet(Canvas canvas) {
        mPath = new Path();
        mPath.moveTo(300, 300);
        mPath.lineTo(400, 350);
        mPath.lineTo(200, 400);

        //将三个点画出来
        mPaint.setColor(Color.RED);
        mPaint.setStrokeWidth(10);
        canvas.drawPoint(400, 350, mPaint);
        canvas.drawPoint(200, 400, mPaint);
        mPaint.setColor(Color.BLACK);

        mPaint.setStrokeWidth(5);
        Path tempPath = new Path();
        tempPath.lineTo(600, 100);
        //通过offset对path进行位移,因为参数中包含temppath,所以先将path复制给temppath
        //然后对temppath进行位移。
        mPath.offset(300, 0, tempPath);
        canvas.drawPath(mPath, mPaint);
        
        mPaint.setColor(Color.BLUE);
        canvas.drawPath(tempPath,mPaint);
    }

效果如下:

5 Op族函数,op()

op函数表示2个path做合并操作,至于怎样合并,根据参数之一的Path.Op这个枚举类型来决定。

               枚举值                                                                         效果
DIFFERENCE path1 减去path2之后剩余的部分
REVERSE_DIFFERENCE path2减去path1剩余的部分
INTERSECT path1和path2想交的部分。
XOR 包含Path1与Path2但不包括两者相交的部分,以path1,path2为真个集合,正好是对INTERSECT取反
UNION 包含path1和path2的全部

这就是2个圆做XOR的效果

6 fillType

filetype的作用是决定path怎样填充,在讲这个之前 ,我们先看两个小例子,首先我们通过如下代码,画两个有部分相交的圆:

private void drawFillType(Canvas canvas){
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(Color.RED);
        mPath.addCircle(400,400,200, Path.Direction.CW);
       //先正向画一个相交的圆,
        mPath.addCircle(700,400,200, Path.Direction.CW);
       //再反向画一个相交的圆
       // mPath.addCircle(700,400,200, Path.Direction.CCW);
        canvas.drawPath(mPath,mPaint);
}

添加2个同样的圆只因添加顺序不一样就导致不一样的效果,这是因为绘制的填充类型起作用。(因为如果path中路径无交叉的情况,跟大家理解的一直,我们主要想path中有想交的情况),FileType是枚举类型,其值如下:

我们再绘制path的时候,即使不通过setFillType设置属性,Path也会默认一个,默认值是WINDING,可以翻译作为是:非零环绕原则,这个原则是指,在path之内的一点,向外做射线,一直穿透到path界外为止,这个时候肯定与path有交点,如果与顺时针的path边界线相交则加1(初始值默认是0),如果与逆时针的边界线相交则减1,把所有点计算完毕之后,如果结果为0, 那么就不在path之内,就不需要填充,也就是不需要绘制 。如果结果不为0.就需要绘制。如下图所示:

看图,这就是我们再上面绘制的第二种情况,使用的填充策略是默认的:WINDING,在我们选中的点向外做三条射线,每天射线与path都有2个交点,我们可以看出,交点1是顺时针,交点2是逆时针,相加结果为0.所以不会绘制这个点。以此类推,在中央灰色区域的任何一点向外做射线,与path的交点之和都是0 所以都不绘制。 而红色区域的任何一点,向外做射线,可能与path有一个交点 ,也可能有三个, 相加的结果都不是0. 所以都需要绘制填充。

我们看完了WINDING, 还有一个INVERSE_WINDING, 见名知意,它是对WINDING取反,所以它需要填充的点是哪些做射线之后与path的交点相加为0的那些点。仍然以上面的作为例子,我们猜测一下,绘制的红色区域应该变成两个圆相交的那个区域和那些path边界外的区域。代码如下:我们只需通过setFillType(Path.FillType.INVERSE_WINDING)来设置填充策略:

 private void drawFillType(Canvas canvas){
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(Color.RED);
        mPath.setFillType(Path.FillType.INVERSE_WINDING);
        mPath.addCircle(400,400,200, Path.Direction.CW);
        mPath.addCircle(700,400,200, Path.Direction.CCW);
        canvas.drawPath(mPath,mPaint);
    }

效果图如下:

接下来我们看EVEN_ODD这个值,我们可以看官方文档:Specifies that "inside" is computed by an odd number of edge crossings. 我们可以看出,他和WINDING 一样,也是从某一点向path之外做射线,但是与WINDING不同的是,他只在乎交点的个数,不关心方向的问题。 与path边界的交点个数为奇数,那么就需要绘制填充,为偶数就不需要。所以我们仍然以上面的代码为例。只是setFillType(Path.FillType.WINDING) 改为:setFillType(Path.FillType.EVEN_ODD),因为与方向无关,所以无论我们以什么样的顺序添加2个园,他都不会绘制填充两圆相交的模块。效果均为如下:

INVERSE_EVEN_ODD与EVEN_ODD正好相反,绘制填充哪些交点个数为偶数的区域。

7 reset,remind重置路径

reset不保留内部数据结构,但会保留FillType. 会保留内部的数据结构,但不保留FillType

8 incReserve

这个函数是提示还有几个点需要添加,看api说明是可以优化存储,我看这个函数对应的源码如下:

    void incReserve(int additionalVerbs, int additionalPoints) {
        SkDEBUGCODE(this->validate();)
        size_t space = additionalVerbs * sizeof(uint8_t) + additionalPoints * sizeof (SkPoint);
        this->makeSpace(space);
        SkDEBUGCODE(this->validate();)
    }

这个函数通过调用makeSpace一次性的开辟了一块内存。而通过lineto或者其他方式添加每次都用调用makeSpace来开辟内存。 我觉的是因为一次性开辟一大块内存,比多次开辟小内存。会减少内存碎片。这只是给个人的猜想。希望确认这块原理的人给我留了言不吝赐教。

  • 圆形进度条

华为的手机管家页面,打开之后是一个圆形的评分页面,其实也相当于一个进度条。其实就是一个view绘制的简单应用

这里的动画效果我设置的时间比较长,所以看着很慢。代码如下:

public class CircleProgressView extends View {

    public static final String TAG = CircleProgressView.class.getSimpleName();

    private ValueAnimator mAnimator;

    /**
     * 内外圆距离竖线的距离
     */
    public static final int CIRCLE_PADDING = 15;

    /**
     * 表示进度的竖线的长度
     */
    private int scaleLength = 30;

    /**
     * 圆半径
     */
    private int circleRadius = 300;

    /**
     * 进度
     */
    private float process;

    Paint mPaint;
    /**
     * 圆心
     */
    Point mCenterPoint;

    /**
     * 曲线渐进色
     */
    SweepGradient sweepGradient;

    /**
     * 两根直线之间的角度间隔
     */
    int degreeInterval = 4;

    int maxValue;

    int currentValue;

    /**
     * 内圆外切矩形和外圆内切矩形
     */
    RectF innerRectf, exRectf;


    public CircleProgressView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        Log.e(TAG, "THE X ==" + getX() + ", y==" + getY()
                + ", left=" + getLeft() + ", right=" + getRight()
                + ", top=" + getTop() + ", width=" + getWidth() + ", height==" + getHeight());
        int centerx = (getRight() - getLeft()) / 2;
        int centery = (getBottom() - getTop()) / 2;
        mCenterPoint.set(centerx, centery);

        innerRectf = new RectF(mCenterPoint.x - circleRadius + CIRCLE_PADDING,
                mCenterPoint.y - circleRadius + CIRCLE_PADDING, mCenterPoint.x + circleRadius - CIRCLE_PADDING,
                mCenterPoint.y + circleRadius - CIRCLE_PADDING);

        exRectf = new RectF(mCenterPoint.x - circleRadius - CIRCLE_PADDING - scaleLength,
                mCenterPoint.y - circleRadius - CIRCLE_PADDING - scaleLength, mCenterPoint.x + circleRadius + CIRCLE_PADDING + scaleLength,
                mCenterPoint.y + circleRadius + CIRCLE_PADDING + scaleLength);
    }


    private void init() {
        mPaint = new Paint();
        //抗锯齿
        mPaint.setAntiAlias(true);
        mCenterPoint = new Point();
        //设置圆弧的渐变
        int[] gradients = {Color.BLUE, Color.GREEN, Color.RED, Color.YELLOW, Color.parseColor("#ff00ff")};
        sweepGradient = new SweepGradient(mCenterPoint.x, mCenterPoint.y, gradients, null);
    }


    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.e(TAG, "IN THE ON DRWA");
        drawScaleCircleProress(canvas);
    }

    public void setProgress(float progress) {
        this.process = progress;
        invalidate();
    }

    /**
     * 带有刻度的原型进度条
     *
     * @param canvas 画布
     */
    private void drawScaleCircleProress(Canvas canvas) {
        mPaint.setStrokeWidth(3.0f);
        drawInnerCircle(canvas);
        drawExcircle(canvas);
        drawLines(canvas);
        drawText(canvas, mCenterPoint, "测试文字");
      /*  Log.e(TAG,"THE X =="+getX()+", y=="+getY()
                +", left="+getLeft()+", right="+getRight()
                +", top="+getTop());*/


    }

    /**
     * 画内圆
     *
     * @param canvas 画布
     */
    private void drawInnerCircle(Canvas canvas) {
        //表示空心,这样画出的是线,否则就是实心的图形
        mPaint.setStyle(Paint.Style.STROKE);
        //设置渐近线
        mPaint.setShader(sweepGradient);
        mPaint.setStrokeWidth(5.0f);
        canvas.drawArc(innerRectf, 0, -180, false, mPaint);
        mPaint.setShader(null);
    }

    /**
     * 画外圆
     *
     * @param canvas 画布
     */
    private void drawExcircle(Canvas canvas) {
        //表示空心,这样画出的是线,否则就是实心的图形
        mPaint.setStyle(Paint.Style.STROKE);
        //设置渐近线
        mPaint.setShader(sweepGradient);
        mPaint.setStrokeWidth(5.0f);

        canvas.drawArc(exRectf, 0, -180, false, mPaint);
        mPaint.setShader(null);
     /*   mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setShader(sweepGradient);
        canvas.drawCircle(centerPoint.x,centerPoint.y,circleRadius+CIRCLE_PADDING+scaleLength,mPaint);
        mPaint.setShader(null);*/
    }

    private void drawLines(Canvas canvas) {
        canvas.save();
        mPaint.setColor(Color.parseColor("#aabbcc"));
        canvas.drawPoint(mCenterPoint.x, mCenterPoint.y, mPaint);
        int size = 180 / degreeInterval;
        for (int i = 0; i <= size; i++) {
            canvas.drawLine(mCenterPoint.x + circleRadius, mCenterPoint.y,
                    mCenterPoint.x + circleRadius + scaleLength, mCenterPoint.y, mPaint);
            canvas.rotate(-degreeInterval, mCenterPoint.x, mCenterPoint.y);
        }
        canvas.restore();

        canvas.save();
        if (currentValue > 0) {
            //因为需要有左边向右边依次画效果,左右需要先旋转-180c,保证画笔在x轴的负方向。
            canvas.rotate(-180, mCenterPoint.x, mCenterPoint.y);
            mPaint.setColor(Color.parseColor("#ff00ff"));
            int sizeSelect = (int) (process * 180) / degreeInterval;
            for (int i = 0; i <= sizeSelect; i++) {
                canvas.drawLine(mCenterPoint.x + circleRadius, mCenterPoint.y,
                        mCenterPoint.x + circleRadius + scaleLength, mCenterPoint.y, mPaint);
                canvas.rotate(degreeInterval, mCenterPoint.x, mCenterPoint.y);
            }
        }
        canvas.restore();
    }

    private void drawText(Canvas canvas, Point centerPoint, String data) {
        canvas.save();
        mPaint.setTextSize(40);
        mPaint.setStrokeWidth(2);
        float stringWidth = mPaint.measureText(data);
        float beginx = (getRight() - getLeft() - stringWidth) / 2;
        canvas.drawText(data, beginx, centerPoint.y, mPaint);
        canvas.restore();
    }

    private void startAnimator(int start, int end, long animTime) {
        mAnimator = ValueAnimator.ofInt(start, end);
        mAnimator.setDuration(animTime);
        mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int value = (int) animation.getAnimatedValue();
                DecimalFormat df = new DecimalFormat("0.000");
                String progress = df.format((float) value / maxValue);
                process = Float.parseFloat(progress);
                Log.e(TAG, "the current process===" + progress);
                invalidate();
            }
        });
        mAnimator.start();
    }

    public void setCurrentValue(int value) {
        if (maxValue <= 0 || value < 0) {
            throw new IllegalArgumentException("the max value and set value must larger than 0," +
                    "now the max value is " + maxValue + ", the set value=" + value);
        }

        if (value > maxValue) {
            throw new IllegalArgumentException("the max value  must larger than set value," +
                    "now the max value is " + maxValue + ", the set value=" + value);
        }

        currentValue = value;
        startAnimator(0, value, 1000 * 5);
    }


    public int getMaxValue() {
        return maxValue;
    }

    public void setMaxValue(int maxValue) {
        this.maxValue = maxValue;
    }
  • 总结

​​​​​​​以上就是关于path一些长用的接口,如果有错误的地方,请大伙留言指出。拜谢。 关于贝塞尔在这里没有介绍。因为用的太多。并且篇幅也不够的原因。留待下一篇单独讲。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值