自定义view(三) 贝塞尔曲线 水波纹效果实现

在上面的博客中说了path的绘制,path绘制, 介绍了除了贝塞尔曲线的其他情况。 在这里单独介绍一下贝塞尔曲线。贝塞尔曲线是应用于二维图形应用程序的数学曲线。一般的矢量图形软件通过它来精确画出曲线,贝塞尔曲线由线段与节点组成,节点是可拖动的支点,线段像可伸缩的皮筋,我们在绘图工具上看到的钢笔工具就是来做这种矢量曲线的。贝塞尔曲线是计算机图形学中相当重要的参数曲线。 我们再开发中很多都会用到贝塞尔曲线的情况,比如水波纹效果。贝塞尔曲线扫盲贴这一片博客很形象的把它的原理解释了一下。看完这个我们可以比较明白的看出。贝塞尔曲线就是两个固定点确定的情况下,根据一个或者多个控制点通过计算得到的曲线。

  • api分析

这是贝塞尔曲线的数学解释,我们再实现对path绘制的时候,的确不需要知道这个。只要知道大致的形式就行,但是对于后期了解属性动画之后,就有意义了。很多时候都是需要用到这个公式。比如经常出现的直播间花束心形点赞效果,那些心形的移动路线就是按照贝塞尔曲线绘制。

我们通过官方文档来看,关于二次,三次贝塞尔曲线的情况:

 /** 从注释中可以看出,x1,y1,就是控制点的座标,x2,y2就是最后固定点的座标。

     * Add a quadratic bezier from the last point, approaching control point
     * (x1,y1), and ending at (x2,y2). 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 control point on a quadratic curve
     * @param y1 The y-coordinate of the control point on a quadratic curve
     * @param x2 The x-coordinate of the end point on a quadratic curve
     * @param y2 The y-coordinate of the end point on a quadratic curve
     */
    public void quadTo(float x1, float y1, float x2, float y2) {
        isSimplePath = false;
        nQuadTo(mNativePath, x1, y1, x2, y2);
    }


 /**
      x1,y1,x2,y2就是控制点的座标,x3,y3就是末尾固定点的座标
     * 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);
    }

我们可以可以做一个例子,通过控制控制点的座标,来看看贝塞尔曲线的情况 。

 

图一就是quadTo的应用,图二就是cubicTo的应用。我们可以通过改变控制点的座标,来改变曲线的样式。代码如下:

  @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.reset();
        mPaint.setStrokeWidth(8);
        mPaint.setColor(Color.parseColor("#ff00ff"));
        mPath.moveTo(100,400);
        mPath.quadTo(mControlPoint.x,mControlPoint.y,1000,400);
        canvas.drawPath(mPath,mPaint);
        mPaint.setColor(Color.RED);
        mPaint.setStrokeWidth(20);
        canvas.drawPoint(100,400,mPaint);
        canvas.drawPoint(1000,400,mPaint);
        canvas.drawPoint(mControlPoint.x,mControlPoint.y,mPaint);

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        //通过移动改变控制点的座标,然后重绘,改变贝塞尔曲线的状态
        mControlPoint.set((int) event.getX(), (int) event.getY());
        invalidate();
        return true;
    }
  • 水波纹效果

我们可以想象水波纹其实就可以看做是两个曲线相互交叉的效果,比如下图所示:

其实这就相当于一个向左移动和一个向右移动的两个曲线,然后和一个圆形相交的部分。 其实绘制所需要的特殊view。就是想它一步步分解的过程,分解成最基本的可以直接通过api绘画的模块。

public class BezierView extends View {

    private static final String TAG = ViewPath.class.getSimpleName();

    private ValueAnimator mAnimator;

    private int currentValue;

    private int xOffset;

    Paint mPaint;
    Point mCenterPoint;
    Path mPath;

    private int windowWidth;

    Path mRightPath;

    Path mDesPath;

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

    private void init() {
        mPaint = new Paint();
        //抗锯齿
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(8);
   //     mPaint.setStyle(Paint.Style.STROKE);
        mCenterPoint = new Point();
        mCenterPoint.set(500, 100);
        mPath = new Path();
        mRightPath = new Path();
        mDesPath = new Path();
    }

    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //获取屏幕宽度
        windowWidth = getWidth();
        drawRightWave(canvas);
        drawLeftWave(canvas);
    }

    //从右向左移动的曲线
    private void drawRightWave(Canvas canvas) {
        mRightPath.reset();
        mDesPath.reset();
        mPaint.setStrokeWidth(5);
        mPaint.setColor(Color.parseColor("#7700CDCD"));
        mPaint.setAlpha(200);
        mPaint.setStrokeWidth(5);
        mPaint.setStyle(Paint.Style.FILL);

        //计算真个向左移动的贝塞尔曲线中需要点的个数,
        //因为每个贝塞尔曲线中的两个固定点之间的间距是300像素,所以要除以300
        //移动的距离这里取的是屏幕宽度的7倍,以当前可见屏幕为标准,向左,右各扩展到3个屏幕宽度
        //这个具体的一定距离,可以按需设置,我这里就直接用了一个比较大的值
        int pointNumbers = windowWidth*7/300;

        mRightPath.moveTo(windowWidth*4 - xOffset, 400);
        //从右向左依次把曲线添加到path中,用rQuadTo是因为他总是以path的最后一个点为原点
        for (int i = 0; i < pointNumbers+1; i++) {
            if (i % 2 == 0) {
                mRightPath.rQuadTo(-150, -50, -300, 0);
            } else {
                mRightPath.rQuadTo(-150, 50, -300, 0);
            }

        }

        //因为有一个圆形在里面所以首先要将path形成一个闭环,因为我们在这里写死圆的位置
        //它是一个以400,400为圆心,300为半径的圆,所以我们的mRightPath形成的闭环Y轴座标应该是
        //700, 以保证和圆的底部相切
        mRightPath.lineTo(-(windowWidth*3)-xOffset - 300*(pointNumbers+1),700);
        mRightPath.lineTo(windowWidth*4 - xOffset,700);
        mRightPath.close();
        mDesPath.addCircle(400,400,300, Path.Direction.CW);

        //将圆形和曲线取交集
        mDesPath.op(mRightPath, Path.Op.INTERSECT);
        canvas.drawPath(mDesPath, mPaint);
        mPaint.setColor(Color.GREEN);
        mPaint.setStyle(Paint.Style.STROKE);
        canvas.drawCircle(400,400,300,mPaint);
    }


    //从左向右移动的曲线,这个与从右向左类似
    private void drawLeftWave(Canvas canvas) {
        mPath.reset();
        mPaint.setStrokeWidth(5);
        mPaint.setColor(Color.parseColor("#7700F5FF"));
        mPaint.setAlpha(200);
        mPaint.setStyle(Paint.Style.FILL);
        mPath.moveTo(-(windowWidth*3) + xOffset, 400);
        int pointNumbers = windowWidth*7/300;

        for (int i = 0; i < pointNumbers+1; i++) {
            if (i % 2 == 0) {
                mPath.rQuadTo(100, -50, 200, 0);
            } else {
                mPath.rQuadTo(100, 50, 200, 0);
            }
        }
        mPath.lineTo(windowWidth*4 + xOffset,700);
        mPath.lineTo(-(windowWidth*3) + xOffset,400);
        mDesPath.addCircle(400,400,300, Path.Direction.CW);
        mDesPath.op(mPath, Path.Op.INTERSECT);
        canvas.drawPath(mDesPath, mPaint);
        mPaint.setColor(Color.GREEN);
        mPaint.setStyle(Paint.Style.STROKE);
        canvas.drawCircle(400,400,300,mPaint);
    }

    //设置移动的动画,通过valueanimator进行对连个曲线的移动
    private void startAnimator(int start, int end, long animTime) {
        mAnimator = ValueAnimator.ofInt(start, end);
        mAnimator.setDuration(animTime);
        mAnimator.setRepeatCount(ValueAnimator.INFINITE);
        mAnimator.setRepeatMode(ValueAnimator.RESTART);
        mAnimator.setInterpolator(new LinearInterpolator());
        mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int value = (int) animation.getAnimatedValue();
                xOffset = value;
                currentValue = (int) (value * 0.6);
                invalidate();
            }
        });
        mAnimator.start();
    }

 
    public void setCurrentValue(int value, int offset) {
        startAnimator(0, offset, 1000*2);
    }

}

很多进度条需要这样实现 ,如果是进度条的话,那么他的变量就是圆的y轴的位置变化,通过移动两个曲线y轴的座标来实现这个功能。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章