美文网首页大话安卓AndroidIT
android 贝塞尔曲线的应用

android 贝塞尔曲线的应用

作者: 许方镇 | 来源:发表于2016-06-04 22:34 被阅读4949次

    转载请标明出处:http://www.jianshu.com/p/c0d7ad796cee

    前言:

    贝塞尔曲线又称贝兹曲线,它的主要意义在于无论是直线或曲线都能在数学上予以描述。最初由保罗·德卡斯特里奥(Paul de Casteljau)于1959年运用德卡斯特里奥演算法开发(de Casteljau Algorithm),在1962,由法国工程师皮埃尔·贝塞尔(Pierre Bézier)所广泛发表。目前广泛应用于图形绘制领域来模拟光滑曲线,为计算机矢量图形学奠定了基础。在一些图形处理软件中都能见到贝塞尔曲线,比如CorelDraw中翻译成“贝赛尔工具”;而在Fireworks中叫“画笔”;Photoshop中叫“钢笔工具”。下图为Photoshop中用钢笔绘制的贝塞尔曲线,共绘制了三条贝塞尔曲线:

    Bézier curve

    数学表达

    术语:数据点、控制线、控制点、德卡斯特里奥算法、一阶,二阶,三阶,n阶……

    • 数据点:一条贝塞尔曲线的起始点和终结点都叫数据点。

    • 控制线:在图中可以看到那条中心点为数据点的线段,每个数据点对应一条控制线

    • 控制点:就是控制线的端点,通过控制线随着控制点的变化而变化;数据点和控制点决定一条贝塞尔曲线。

    • 一阶贝塞尔曲线:其实是一条直线段,没有控制点。


      一阶贝塞尔曲线示意图
    一阶贝塞尔曲线公式
    • 二阶贝塞尔曲线:图中第二段为二阶贝塞尔曲线,只有一个控制点,即只有一个控制点和两个数据点来决定曲线形状。


      二阶贝塞尔曲线示意图.gif
    二阶贝塞尔曲线公式

    二阶公式推导:

    二阶贝塞尔曲线t=0.6示意图.gif

    根据控制点和数据点,对贝塞尔曲线进行约束,满足的条件为

    满足条件

    问题变为:已知P0(x0,y0), P1(x1,y1), P2(x2,y2),根据上式求P点坐标?
    先求出A、B点坐标,其坐标
    PA=P0+(P1-P0)·t
    PB=P1+(P2-P1)·t
    之后求P点坐标,其坐标
    P=PA+(PB-PA)·t
    P=(1-t)2P0+2t(1-t)P1+t2P2, t∈[0,1]

    • 三阶贝塞尔曲线:图中第三段为三阶贝塞尔曲线,有两个控制点和两个数据点决定的曲线,同样满足等比条件:
    三阶贝塞尔曲线 三阶贝塞尔曲线公式
    • 德卡斯特里奥算法的思想:给定数据点和控制点P0、P1…Pn,首先将数据点和控制点连接形成一条折线,计算出每条折线上面的一点,使得初始数据点(初始控制点)到该点的距离与初始数据点(初始控制点)到终止数据点(终止控制点)的距离之比为t:1。将这些点连接起来形成新的折线(折线少了一段),用递归的算法继续计算,指导只有两个点,在这两个点形成的线段上去一点,满足以上的比例关系。随着t的从0到1的变化,该点的集合形成了贝塞尔曲线。

    • n阶贝塞尔曲线公式如下。

    n阶贝塞尔曲线公式

    Android中的应用

    对android中如何获取贝塞尔曲线上的点,如何绘制贝塞尔曲线,以及结合贝塞尔曲线的知识做出的效果进行分析。

    1. 获取贝塞尔曲线上点的坐标

    在android中并没有直接获取的方法,因此需要利用上面的公式进行计算,以二阶贝塞尔曲线为例,获取51个点,主要是t从0开始到1间取51项的等差数列:

    public class MainActivity extends AppCompatActivity {
    
        private static final float mPointNum = 50f;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            init();
        }
    
        private void init() {
            PointF mStartPoint = new PointF(0, 0);
            PointF mEndPoint = new PointF(0, 1200);
            PointF mControlPoint = new PointF(500, 600);
    
            List<PointF> mPointList = new ArrayList<>();
            for (int i = 0; i <= mPointNum; i++) {
                mPointList.add(getBezierPoint(mStartPoint, mEndPoint, mControlPoint, i / mPointNum));
                Log.d("Bezier", "X:" + mPointList.get(i).x + " Y:" + mPointList.get(i).y);
            }
        }
    
        private PointF getBezierPoint(PointF start, PointF end, PointF control, float t) {
            PointF bezierPoint = new PointF();
            bezierPoint.x = (1 - t) * (1 - t) * start.x + 2 * t * (1 - t) * control.x + t * t * end.x;
            bezierPoint.y = (1 - t) * (1 - t) * start.y + 2 * t * (1 - t) * control.y + t * t * end.y;
            return bezierPoint;
        }
    }```
    如果需要更高阶,可以使用递归函数来运算
    ```java
    //用递归获取贝塞尔曲线点的x轴坐标
    private float getBezierPointX(int n, int position, float t) {
            if (n == 1) {
                return (1 - t) * mPointList.get(position).x + t * mPointList.get(position + 1).x;
            }
            return (1 - t) * getBezierPointX(n - 1, position, t) + t * getBezierPointX(n - 1, position + 1, t);
        }
    
    //用递归获取贝塞尔曲线点的x轴坐标
    private float getBezierPointX(int n, int position, float t) {
            if (n == 1) {
                return (1 - t) * mPointList.get(position).x + t * mPointList.get(position + 1).x;
            }
            return (1 - t) * getBezierPointX(n - 1, position, t) + t * getBezierPointX(n - 1, position + 1, t);
        }
    
    private ArrayList<PointF> buildBezierPoints() {
            ArrayList<PointF> points = new ArrayList<>();
            int order = mPointList.size() - 1;
            float delta = 1.0f / POINT_NUM;
            for (float t = 0; t <= 1; t += delta) {
                // Bezier点集
                points.add(new PointF(getBezierPointX(order, 0, t), getBezierPointY(order, 0, t)));
            }
            return points;
        }
    

    2. 绘制贝塞尔曲线

    Android 中的Path类可以直接绘制一阶到三阶的贝塞尔曲线,在onDraw(Canvas canvas) 方法中使用:

    • 绘制一阶贝塞尔曲线:
    canvas.drawLine(start.x,start.y,end.x,end.y);```
    * 绘制二阶贝塞尔曲线:
    ```java 
    mPath.moveTo(startPoint.x, startPoint.y);//起点
    mPath.quadTo(controlPoint1.x, controlPoint1.y, endPoint.x, endPoint.y);
    canvas.drawPath(mPath, mPaint);```
    * 绘制三阶贝塞尔曲线:
    ```java 
    mPath.moveTo(startPoint.x, startPoint.y);//起点
    mPath.cubicTo(controlPoint1.x, controlPoint1.y, controlPoint2.x, controlPoint2.y, endPoint.x, endPoint.y);
    canvas.drawPath(mPath, mPaint);```
    * 绘制n阶贝塞尔曲线
    n阶贝塞尔曲线绘制,需要结合递归函数,设定一条曲线由多少个点组成,通过循环获取每个点的坐标进行绘制。
    
    ![7阶贝塞尔曲线](https://img.haomeiwen.com/i1903970/204ebcd675f7a0f1.gif?imageMogr2/auto-orient/strip)
    
    ![8阶贝塞尔曲线](https://img.haomeiwen.com/i1903970/a93923157b0c6a70.gif?imageMogr2/auto-orient/strip)
    ### 3.Demo 
    
    贝塞尔曲线在android中最常用的是做出一些动画特效,如QQ消息数拖拽形变效果,一些炫酷的下拉刷新控件,阅读软件的翻书效果,一些平滑的折线图的制作,某图片的运动轨迹等……
    ##### 3.1 形状变形
    只需要知道变形前和变形后图形的数据点和控制点即可,通过改变数据点和控制点使得形状发生改变。
    ![变形](https://img.haomeiwen.com/i1903970/8a62b7b1d05dfa9b.gif?imageMogr2/auto-orient/strip)
    初始化
    ```java
        private float[] mData = new float[8];               // 顺时针记录绘制圆形的四个数据点
        private float[] mCtrl = new float[16];              // 顺时针记录绘制圆形的八个控制点
    
        private float mDuration = 1000;                     // 变化总时长
        private float mCurrent = 0;                         // 当前已进行时长
        private float mCount = 100;                         // 将时长总共划分多少份
        private float mPiece = mDuration / mCount;            // 每一份的时长```
    在onDraw(Canvas canvas)方法中绘制
    ```java
            path.reset();
            path.moveTo(mData[0], mData[1]);
    
            path.cubicTo(mCtrl[0], mCtrl[1], mCtrl[2], mCtrl[3], mData[2], mData[3]);
            path.cubicTo(mCtrl[4], mCtrl[5], mCtrl[6], mCtrl[7], mData[4], mData[5]);
            path.cubicTo(mCtrl[8], mCtrl[9], mCtrl[10], mCtrl[11], mData[6], mData[7]);
            path.cubicTo(mCtrl[12], mCtrl[13], mCtrl[14], mCtrl[15], mData[0], mData[1]);
    
            canvas.drawPath(path, mPaint);
    
            mCurrent += mPiece;
            if (mCurrent < mDuration) {
    
                mData[1] -= 120 / mCount;
                mCtrl[7] += 80 / mCount;
                mCtrl[9] += 80 / mCount;
    
                mCtrl[4] -= 20 / mCount;
                mCtrl[10] += 20 / mCount;
    
                postInvalidateDelayed((long) mPiece);
            }
    
    3.2 漂浮的爱心

    爱心的漂浮轨迹就是一条三阶贝塞尔曲线,结合属性动画中的估值器进行设置。

    漂浮的爱心
    • 首先定义一个属性动画的估值器
    public class BezierEvaluator implements TypeEvaluator<PointF> {
    
        private PointF mControlP1;
        private PointF mControlP2;
    
        public BezierEvaluator(PointF controlP1, PointF controlP2) {
            this.mControlP1 = controlP1;
            this.mControlP2 = controlP2;
        }
    
        @Override
        public PointF evaluate(float time, PointF start, PointF end) {
    
            float timeLeft = 1.0f - time;
            PointF point = new PointF();
    
            point.x = timeLeft * timeLeft * timeLeft * (start.x) + 3 * timeLeft * timeLeft * time *
                    (mControlP1.x) + 3 * timeLeft * time *
                    time * (mControlP2.x) + time * time * time * (end.x);
    
            point.y = timeLeft * timeLeft * timeLeft * (start.y) + 3 * timeLeft * timeLeft * time *
                    (mControlP1.y) + 3 * timeLeft * time *
                    time * (mControlP2.y) + time * time * time * (end.y);
            return point;
        }
    }```
    
    * 之后自定义一个view可以生成爱心,添加透明度,缩放等动画和根据贝塞尔曲线改变其位置的属性动画。
    
    初始化爱心图片和多个插值器等,到时随即选取
    ```java
       private void init() {
    
            // 初始化显示的图片
            drawables = new Drawable[3];
            drawables[0] = getResources().getDrawable(R.drawable.red);
            drawables[1] = getResources().getDrawable(R.drawable.yellow);
            drawables[2] = getResources().getDrawable(R.drawable.green);
    
            // 初始化插补器
            mInterpolators = new Interpolator[4];
            mInterpolators[0] = new LinearInterpolator();// 线性
            mInterpolators[1] = new AccelerateInterpolator();// 加速
            mInterpolators[2] = new DecelerateInterpolator();// 减速
            mInterpolators[3] = new AccelerateDecelerateInterpolator();// 先加速后减速
    
            // 底部 并且 水平居中
            dWidth = drawables[0].getIntrinsicWidth();
            dHeight = drawables[0].getIntrinsicHeight();
            lp = new LayoutParams(dWidth, dHeight);
            lp.addRule(CENTER_HORIZONTAL, TRUE);// 这里的TRUE 要注意 不是true
            lp.addRule(ALIGN_PARENT_BOTTOM, TRUE);
    
        }```
    入场动画
    ```java
    private AnimatorSet getEnterAnimator(final View target) {
            ObjectAnimator alpha = ObjectAnimator.ofFloat(target, View.ALPHA, 0.2f, 1f);
            ObjectAnimator scaleX = ObjectAnimator.ofFloat(target, View.SCALE_X, 0.2f, 1f);
            ObjectAnimator scaleY = ObjectAnimator.ofFloat(target, View.SCALE_Y, 0.2f, 1f);
            AnimatorSet enter = new AnimatorSet();
            enter.setTarget(target);
            enter.setInterpolator(new LinearInterpolator());
            enter.setDuration(500).playTogether(alpha, scaleX, scaleY);
            return enter;
        }```
    贝塞尔曲线动画
    ```java
        private ValueAnimator getBezierValueAnimator(final View target) {
            // 初始化贝塞尔估值器
            BezierEvaluator evaluator = new BezierEvaluator(getPointF(2), getPointF(1));
            // 起点在底部中心位置,终点在底部随机一个位置
            ValueAnimator animator = ValueAnimator.ofObject(evaluator, new PointF((mWidth - dWidth) /
                    2, mHeight - dHeight), new PointF(random.nextInt(getWidth()), 0));
            animator.setTarget(target);
            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator valueAnimator) {
                    // 这里获取到贝塞尔曲线计算出来的的x y值 赋值给view 这样就能让爱心随着曲线走啦
                    PointF pointF = (PointF) valueAnimator.getAnimatedValue();
                    target.setX(pointF.x);
                    target.setY(pointF.y);
                    // alpha动画
                    target.setAlpha(1 - valueAnimator.getAnimatedFraction());
                }
            });
    
            animator.setDuration(3000);
            return animator;
        }
    

    结合动画添加爱心

    public void addHeart() {
    
            final ImageView imageView = new ImageView(getContext());
            // 随机选一个爱心
            imageView.setImageDrawable(drawables[random.nextInt(3)]);
            imageView.setLayoutParams(lp);
            addView(imageView);
    
            AnimatorSet finalSet = new AnimatorSet();
    
            AnimatorSet enterAnimatorSet = getEnterAnimator(imageView);//入场动画
            ValueAnimator bezierValueAnimator = getBezierValueAnimator(imageView);//贝塞尔曲线路径动画
    
            finalSet.playSequentially(enterAnimatorSet, bezierValueAnimator);
            finalSet.setInterpolator(mInterpolators[random.nextInt(4)]);
            finalSet.setTarget(imageView);
    
            finalSet.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    removeView((imageView));//删除爱心
                }
            });
            finalSet.start();
    
        }
    
    3.3 贝塞尔曲线侧边索引条

    https://github.com/PoplarTang/FancyListIndexer

    相关文章

      网友评论

      • chuwe1:兄得 你是网易考拉的那个 许方镇?
        chuwe1:如果是 有这篇文章里https://kaolamobile.github.io/2018/04/03/vertical-nested-scroll-layout/的VerticalNestedScrollLayout的源码方便给看一下不?
      • Q大疯zi:牛逼
      • eb15e719394c:嗨,请问漂浮爱心有demo可以共享吗
        17982d20309f:lz 你的Demo能共享一下吗
        许方镇:@瓦良格舰长 搜一下,多了去了
      • 丶花落谁家:贝塞尔曲线侧边索引条,太棒了一直在寻找这种实现效果,给力

      本文标题:android 贝塞尔曲线的应用

      本文链接:https://www.haomeiwen.com/subject/qqoldttx.html