频道栏目
首页 > 资讯 > 其他 > 正文

自定义View学习笔记:Path之Bezier

17-12-30        来源:[db:作者]  
收藏   我要投稿

一、贝赛尔曲线来源
在数学的数值分析领域中,贝赛尔曲线(Bézier曲线)是电脑图形学中相当重要的参数曲线。更高维度的广泛化贝塞尔曲线就称作贝塞尔曲面,其中贝塞尔三角是一种特殊的实例。

这两篇文章是目前为止,我发现的讲解的最清楚的了,可以观摩学习。

二、Android中贝赛尔曲线概要
1、一阶贝赛尔曲线:
原理:没有控制点,仅有两个数据点(A 和 B),最终效果就是一条线段;其实就是前面讲解过的lineTo。
2、二阶贝赛尔曲线:
原理:由两个数据点(确定曲线的起始和结束位置),一个控制点(确定曲线的弯曲程度)来描述曲线状态;对应的方法:

public void quadTo(float x1, float y1, float x2, float y2);
public void rQuadTo(float dx1, float dy1, float dx2, float dy2);

3、三阶贝赛尔曲线:
原理:由两个数据点(确定曲线的起始和结束位置),两个控制点(确定曲线的弯曲程度和状态)来描述曲线状态;对应的方法:

public void cubicTo(float x1,float y1,float x2,float y2,float x3,float y3);
public void rCubicTo(float x1,float y1,float x2,float y2,float x3,float y3);

这里,关于数据点和控制点的说明如下:
数据点和控制点的说明

三阶曲线相比于二阶曲线可以制作更加复杂的形状,但是对于高阶的曲线,用低阶的曲线组合也可达到相同的效果,就是传说中的降阶。因此我们对贝塞尔曲线的封装方法一般最高只到三阶曲线。
关于降阶和升阶的说明:
降阶和升阶

三、二阶贝塞尔曲线的使用

1、二阶贝塞尔曲线quadTo的使用:

public void quadTo(float x1, float y1, float x2, float y2);

参数中(x1,y1)是控制点坐标,(x2,y2)是终点坐标 。疑问:有控制点和终点坐标,那起始点是多少呢?
解答:
整条线的起始点是通过Path.moveTo(x,y)来指定的,而如果我们连续调用quadTo(),前一个quadTo()的终点,就是下一个quadTo()函数的起点;如果初始没有调用Path.moveTo(x,y)来指定起始点,则默认以控件左上角(0,0)为起始点;(moveTo(x, y)一个点,再加上quadTo(x1,y1,x2,y2)两个点,一共三个点.)

2、代码示例:

public class Bezier extends View {
    private Paint mPaint;
    private int centerX, centerY;
    private PointF start, end, control;

    public Bezier(Context context) {
        super(context);
        mPaint = new Paint();
        mPaint.setColor(Color.BLACK);
        mPaint.setStrokeWidth(8);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setTextSize(60);

        start = new PointF(0,0);
        end = new PointF(0,0);
        control = new PointF(0,0);
    }

    public Bezier(Context context,AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onSizeChanged(int w,int h,int oldw,int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        centerX = w/2;
        centerY = h/2;  
        // 初始化数据点和控制点的位置
        start.x = centerX - 200;
        start.y = centerY;
        end.x = centerX + 200;
        end.y = centerY;
        control.x = centerX;
        control.y = centerY-100;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 根据触摸位置更新控制点,并提示重绘
        control.x = event.getX();
        control.y = event.getY();
        invalidate();
        return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 绘制数据点和控制点
        mPaint.setColor(Color.GRAY);
        mPaint.setStrokeWidth(20);
        //绘制三个点:start,end ,control
        canvas.drawPoint(start.x,start.y,mPaint);
        canvas.drawPoint(end.x,end.y,mPaint);
        canvas.drawPoint(control.x,control.y,mPaint);

        // 绘制辅助线
        mPaint.setStrokeWidth(4);
        canvas.drawLine(start.x,start.y,control.x,control.y,mPaint);
        canvas.drawLine(end.x,end.y,control.x,control.y,mPaint);
        // 绘制贝塞尔曲线
        mPaint.setColor(Color.RED);
        mPaint.setStrokeWidth(8);
        Path path = new Path();
        //path.moveTo以start点开始,对应的path.quadTo以end点结束;相反的;
        //如果以end点开始,对应的path.quadTo以start结束,否则,无法绘制曲线

        //参数中(control.x,control.y)是控制点坐标,(end.x,end.y)是终点坐标
        //整条线的起始点是通过Path.moveTo(start.x,start.y)来指定的
        path.moveTo(start.x,start.y);
        path.quadTo(control.x,control.y,end.x,end.y);
        canvas.drawPath(path, mPaint);
    }
}

3、使用Path.lineTo()所存在问题:

当用Path.lineTo()绘制图形,尤其是有圆角弧度的图形的时候,在转角或者圆弧出,会出现明显的马赛克样的锯齿之类的东西,一点也不平滑,看起来不自在,尤其是图形比较大的时候。这就需要优化,实现线与线之间的平滑过渡;优化有两个方案:

一是设置mPaint.setAntiAlias(true);//防锯齿;
二是利用二阶贝赛尔曲线的Path.quadTo函数来重新实现移动轨迹效果。

两个方案可以结合起来使用,效果更佳。

public class FingerPath extends View {
    private Paint mPaint;
    private Path mPath;
    private float mPreX,mPreY;
    //true:调用LineTo方法;false:调用QuadTo方法
    private boolean lineOrQuad = true;
    public FingerPath(Context context) {
        super(context);
        if(mPaint == null){
            mPaint = new Paint();
            mPaint.setStrokeWidth(20);
            mPaint.setAntiAlias(true);//防锯齿
            mPaint.setColor(Color.GREEN);
            mPaint.setStyle(Paint.Style.STROKE);
            mPath = new Path();
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN: {
                if(lineOrQuad){
                    mPath.moveTo(event.getX(), event.getY());
                }else {
                    mPath.moveTo(event.getX(),event.getY());
                    //mPreX,mPreY表示手指的前一个点
                    mPreX = event.getX();
                    mPreY = event.getY();
                }
                return true;
            }
            case MotionEvent.ACTION_MOVE:
                if(lineOrQuad){
                    mPath.lineTo(event.getX(), event.getY());
                }else {
                    //除以2的原因:mPreX/mPreY其实是很接近event.getX()/
                    event.getY()的,除以2取平均值,让每个点之间的波动更小。
                    float endX = (mPreX + event.getX())/2;
                    float endY = (mPreY + event.getY())/2;
                    mPath.quadTo(mPreX, mPreY, endX, endY);
                    //mPreX,mPreY表示手指的前一个点
                    mPreX = event.getX();
                    mPreY = event.getY();
                }
        //注意:这里用的是postInvalidate(),不是Invalidate();关乎到线程安全,
        //Invalidate()一定要在UI线程执行,否则就会报错; postInvalidate()则没有那么
        //多讲究,它可以在任何线程中执行,而不必一定要是主线程,其实postInvalidate()是
        //利用handler给主线程发送刷新界面的消息来实现的,所以它是可以在任何线程中执行,由
        //此带来的影响是在UI界面刷新的时候,postInvalidate()没有Invalidate()快。
        invalidate();
        break;
        default:
        break;
    }
    return super.onTouchEvent(event);
}
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.GRAY);
        canvas.drawPath(mPath, mPaint);
    }
    public void reset(){
        mPath.reset();
        postInvalidate();
    }
    public void setQuadOrLine(boolean quadOrLine){
        this.lineOrQuad = quadOrLine;
    }
}

在ACTION_DOWN的时候,利用 mPath.moveTo(event.getX(),event.getY())将Path的初始位置设置到手指的触点处,如果不调用mPath.moveTo的话,会默认是从(0,0)开始的。然后我们定义两个变量mPreX,mPreY来表示手指的前一个点。我们通过上面的分析知道,这个点是用来做控制点的。最后return true让ACTION_MOVE,ACTION_UP事件继续向这个控件传递。

在ACTION_MOVE的时候,我们先找到结束点,我们说了结束点是这个线段的中间位置,所以很容易求出它的坐标endX,endY;控制点是上一个手指位置即mPreX,mPreY;那有些同学可能会问了,那起始点是哪啊。在开篇讲quadTo()函数时,就已经说过,第一个起始点是Path.moveTo(x,y)定义的,其它部分,一个quadTo的终点,是下一个quadTo的起始点。 所以这里的起始点,就是上一个线段的中间点。把各个线段的中间点做为起始点和终点,把终点前一个手指位置做为控制点。

4、Path.rQuadTo():
API提供的方法预览:

public void rQuadTo(float dx1, float dy1, float dx2, float dy2);

前面我们说过方法签的单独的r其实就是relaytiveLayout的缩写,明白了这点,就会明白该方法也是一个利用相对位置来自定义View的。

其中:

dx1:控制点X坐标,表示相对上一个终点X坐标的位移坐标,正值表示相加,负值表示相减;
dy1:控制点Y坐标,表示相对上一个终点Y坐标的位移坐标,正值表示相加,负值表示相减;
dx2:终点X坐标,表示相对上一个终点X坐标的位移值,正值表示相加,负值表示相减;
dy2:终点Y坐标,表示相对上一个终点Y坐标的位移值。正值表示相加,负值表示相减;

这四个参数都是传递的都是相对值,相对上一个终点的位移值。

public class WaveView extends View {
    private Paint mPaint;
    private Path mPath;
    private Path mPath2;
    private int mItemWaveLength = 400;//波长
    private int mItemWaveLength2 = 900;//波长
    private int dx = 0;
    public WaveView(Context context) {
        super(context);
        mPaint = new Paint();
        mPaint.setStrokeWidth(5);
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.FILL);
        mPath = new Path();
        mPath2 = new Path();
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.reset();
        mPath2.reset();
        int originY = 500;//波峰距离控件顶部的距离
        int halfWaveLen = mItemWaveLength / 2;
        float Height = 150;//波浪的高度的一半(波峰与波谷的垂直距离)
        //将mPath的起始位置向左移一个波长:
        mPath.moveTo(-mItemWaveLength + dx, originY);
        mPath2.moveTo(-mItemWaveLength2 * 1.15f + dx, originY);
        //for循环画出当前屏幕中可能容得下的所有波(因将mPath的起始位置向左移一个波长,同理也向右移一个波长,故乘2)
        for (int i=-mItemWaveLength;i <= getWidth() + mItemWaveLength * 2; i += mItemWaveLength){
            //画的是一个波长中的前半个波;halfWaveLen表示波的高度
            mPath.rQuadTo(halfWaveLen / 2, Height / 2, halfWaveLen, 0);
            mPath2.rQuadTo(halfWaveLen / 2, Height / 2, halfWaveLen, 0);
            //画的是一个波长中的后半个波,波的深度,与高度合起来就是波高
            mPath.rQuadTo(halfWaveLen / 2, -Height / 2, halfWaveLen, 0); 
            mPath2.rQuadTo(halfWaveLen / 2, -Height / 2, halfWaveLen, 0);
        }
        mPath.lineTo(getWidth(), getHeight());
        mPath.lineTo(0, getHeight());
        mPath.close();
        mPath2.lineTo(getWidth(), getHeight());
        mPath2.lineTo(0, getHeight());
        mPath2.close();
        mPaint.setColor(Color.parseColor("#428ddd"));
        canvas.drawPath(mPath,mPaint);
        mPaint.setAlpha(20);
        mPaint.setColor(Color.parseColor("#43c5dd"));
        canvas.drawPath(mPath2,mPaint);
    }
    public void startAnim(){
        ValueAnimator animator = ValueAnimator.ofInt(0, mItemWaveLength);
        animator.setDuration(500);//动画时间
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.setInterpolator(new LinearInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                dx = (int)animation.getAnimatedValue();
                postInvalidate();
            }
        });
        animator.start();
    }
}

四、三阶贝赛尔
三阶贝赛尔曲线的使用与二阶贝塞尔曲线类似,不同的是需要两个控制点,

public void cubicTo(float x1,float y1,float x2,float y2,float x3,float y3);
public void rCubicTo(float x1,float y1,float x2,float y2,float x3,float y3);

后面有更多的使用体会再来细说这个,在此先行略过了。

相关TAG标签
上一篇:IOS metal 编辑器 玻璃材质
下一篇:shell 编程基础语法
相关文章
图文推荐

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 红黑联盟--致力于做实用的IT技术学习网站