初试 贝塞尔曲线 (二)

之前写的一篇 《初试 贝塞尔曲线》,如果您感兴趣可以看看,是关于贝塞尔曲线的基础用法。

最近看了很多有关贝塞尔曲线的文章,感觉大神们都好屌,文章都写的好好,就是有的时候注释写的不太够,理解起来有点费劲,所以我打算研读过后,补充注释,以便其他同学方便学习~~

效果图

开始本文之前先查看一下目标效果是如何的。


69b7d63agw1evxh2kw1bcg20m80go1l2.gif

这个动画的来源是优秀网页设计的一个微博,看到这个效果感觉下面的圆的动画十分的赞,于是就打算模仿这个效果。

然后接下来看我所做的简单效果吧。


111.gif

因为时间缘故就简单的模仿了自己感兴趣的主要效果,并没有做到全部模仿,等以后有时间了再完善

贝塞尔曲线知识讲解

将这个圆的动画效果拆解开看的画,其实会分为5个状态。

状态1.png
状态2.png
状态3.png
状态4.png
状态5.png

注:我这里的状态4和状态5其实与原设计图是有出入的,我这里的5个状态其实是对称关系的,但是原设计图并非是对称关系,然后因为我偷懒,就做成了对称设计,这个以后再优化吧~

也就是说,这个动画效果的实现就是不同状态之间的转化加上水平位移的实现,现在已经将动画肢解完了,那么接下来就讲解下如何实现圆的形变吧。

开始讲解具体内容之前我们需要先了解一下如何用贝塞尔曲线画一个圆,因为我的做法是通过贝塞尔曲线来实现的。

aEsuA.png

上图是stackoverflow上的一个答案,这个答案可能说的不是很清楚,这里还有一篇文章,这两个的结果都是差不多,就是所需要的数值c约等于0.551915024494f,具体的论证过程可以看这两篇文章,那么这个c的值有什么用么,我用最简单的方法来说明,就是把图中的1理解为圆的半径,那么对应的另外个值就应该是半径乘以0.551915024494f。
可能有朋友还是看不懂这个c到底干啥用呢,我下面画一个图来描述下,大概是怎么个意思。

jiangjietu.png

这里的坐标轴也就是Android中的坐标轴了,如果我们打算用贝塞尔曲线来画这么一个圆的话,我们需要知道这个圆的半径,以及图中的M的值,知道这两个值的话就能够知道图中12个点的坐标,知道坐标就能够用Path的cubicTo方法来使用贝塞尔曲线画出圆了。
这里稍微展示点代码来说明如何绘制P0至P3这段圆弧。

mPath.moveTo(p0.x,p0.y);
mPath.cubicTo(p1.x, p1.y, p2.x, p2.y, p3.x,p3.y);

这样我们就知道如何使用贝塞尔曲线来绘制一个圆了。也就是状态1和状态5我们都会绘制了,接下来看看状态2如何绘制。

1.jpg

通过上图大家就能很快的明白状态2应该如何绘制,其实就是把右边的点向右移动点距离就行了。
其实photoshop(sketch)这些绘图软件中的钢笔工具(Vector)就是用的贝塞尔曲线,然后这里推荐个网址给大家,轻松上手钢笔工具的使用哦,强烈推荐!!!

看完上面的讲解,那么状态3也就一点都不难了。

jiangjietu3.png.jpg

看到上图就明白状态3的实现就是在状态2的基础上修改了个值,一个是M的值加大,让圆看起来跟肥一点,还有就是圈住的那些点向右移动,做到居中。

至此,这个动画效果的分解也就完成了,其实一点都不难。最后还剩一个回弹,我的做法就是加个sin函数来控制,比较简单 。

示例代码

代码中的四个点,方便理解
代码中的四个点,方便理解.jpeg
/**
 * Created by liuboyu on 17/1/12.
 */
public class MagicCircleView extends View implements View.OnClickListener {


    private Path mPath;
    private Paint mFillCirclePaint;

    /**
     * View的宽度
     **/
    private int width;
    /**
     * View的高度,这里View应该是正方形,所以宽高是一样的
     **/
    private int height;
    /**
     * View的中心坐标x
     **/
    private int centerX;
    /**
     * View的中心坐标y
     **/
    private int centerY;

    private float maxLength;
    /**
     * 平移动化时间(值总在0-1之间)
     */
    private float mInterpolatedTime;
    /**
     * 拉伸长度
     */
    private float stretchDistance;
    /**
     * 圆球半径
     */
    private float radius;
    private float c;
    private float blackMagic = 0.551915024494f;
    private VPoint p2, p4;
    private HPoint p1, p3;

    public MagicCircleView(Context context) {
        super(context);
        init();
    }

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

    public MagicCircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }


    /**
     * 初始化
     */
    private void init() {
        mFillCirclePaint = new Paint();
        mFillCirclePaint.setColor(0xFFfe626d);
        mFillCirclePaint.setStyle(Paint.Style.FILL);
        mFillCirclePaint.setStrokeWidth(1);
        mFillCirclePaint.setAntiAlias(true);
        mPath = new Path();

        p2 = new VPoint();
        p4 = new VPoint();

        p1 = new HPoint();
        p3 = new HPoint();
    }

    /**
     * 初始化圆的各个点
     */
    private void initCirclePoint() {
        p1.setY(radius);
        p3.setY(-radius);
        p3.x = p1.x = 0;
        p3.left.x = p1.left.x = -c;
        p3.right.x = p1.right.x = c;

        p2.setX(radius);
        p4.setX(-radius);
        p2.y = p4.y = 0;
        p2.top.y = p4.top.y = -c;
        p2.bottom.y = p4.bottom.y = c;
        setOnClickListener(this);
    }

    /**
     * 绘图
     */
    private void drawCircle(float interpolatedTime) {

        if (mInterpolatedTime >= 0 && mInterpolatedTime <= 0.2) {
            drawCircleStatus_1(interpolatedTime);
        } else if (mInterpolatedTime > 0.2 && mInterpolatedTime <= 0.5) {
            drawCircleStatus_2(interpolatedTime);
        } else if (mInterpolatedTime > 0.5 && mInterpolatedTime <= 0.8) {
            drawCircleStatus_3(interpolatedTime);
        } else if (mInterpolatedTime > 0.8 && mInterpolatedTime <= 0.9) {
            drawCircleStatus_4(interpolatedTime);
        } else if (mInterpolatedTime > 0.9 && mInterpolatedTime <= 1) {
            drawCircleStatus_5(interpolatedTime);
        }
    }

    /**
     * 状态1
     *
     * @param interpolatedTime 为当前动画帧对应的相对时间,值总在0-1之间
     */
    private void drawCircleStatus_1(float interpolatedTime) {//0~0.2
        initCirclePoint();
        p2.adjustAllX(stretchDistance * interpolatedTime * 5);
    }

    /**
     * 状态2
     *
     * @param interpolatedTime 为当前动画帧对应的相对时间,值总在0-1之间
     */
    private void drawCircleStatus_2(float interpolatedTime) {//0.2~0.5
        drawCircleStatus_1(0.20f);
        p4.adjustAllX(-stretchDistance * (interpolatedTime - 0.2f) * (10f / 3));
    }

    /**
     * 状态3
     *
     * @param interpolatedTime 为当前动画帧对应的相对时间,值总在0-1之间
     */
    private void drawCircleStatus_3(float interpolatedTime) {//0.5~0.8
        drawCircleStatus_2(0.5f);
        p2.adjustAllX(-stretchDistance * (interpolatedTime - 0.5f) * (10f / 3));
    }

    /**
     * 状态4
     *
     * @param interpolatedTime 为当前动画帧对应的相对时间,值总在0-1之间
     */
    private void drawCircleStatus_4(float interpolatedTime) {//0.8~0.9
        drawCircleStatus_3(0.8f);
        p4.adjustAllX(stretchDistance * (interpolatedTime - 0.8f) * 10);
    }

    /**
     * 状态5
     *
     * @param interpolatedTime 为当前动画帧对应的相对时间,值总在0-1之间
     */
    private void drawCircleStatus_5(float interpolatedTime) {
        drawCircleStatus_4(0.9f);
        interpolatedTime = interpolatedTime - 0.9f;
        p4.adjustAllX((float) (Math.sin(Math.PI * interpolatedTime * 10f) * (2 / 10f * radius)));
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.reset();
        canvas.translate(2 * radius, radius);
        drawCircle(mInterpolatedTime);

        // 平移
        float offset = maxLength * (mInterpolatedTime - 0.2f);
        offset = offset > 0 ? offset : 0;
        p1.adjustAllX(offset);
        p2.adjustAllX(offset);
        p3.adjustAllX(offset);
        p4.adjustAllX(offset);

        // 圆球效果
        mPath.moveTo(p1.x, p1.y);
        mPath.cubicTo(p1.right.x, p1.right.y, p2.bottom.x, p2.bottom.y, p2.x, p2.y);
        mPath.cubicTo(p2.top.x, p2.top.y, p3.right.x, p3.right.y, p3.x, p3.y);
        mPath.cubicTo(p3.left.x, p3.left.y, p4.top.x, p4.top.y, p4.x, p4.y);
        mPath.cubicTo(p4.bottom.x, p4.bottom.y, p1.left.x, p1.left.y, p1.x, p1.y);

        canvas.drawPath(mPath, mFillCirclePaint);
    }


    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        width = w;
        height = h;
        centerX = width / 2;
        centerY = height / 2;
        radius = 100;
        c = radius * blackMagic;
        stretchDistance = radius;
        maxLength = width - radius - radius;
    }

    @Override
    public void onClick(View v) {
        startAnimation();
    }


    class VPoint {
        public float x;
        public float y;
        public PointF top = new PointF();
        public PointF bottom = new PointF();

        public void setX(float x) {
            this.x = x;
            top.x = x;
            bottom.x = x;
        }

        public void adjustY(float offset) {
            top.y -= offset;
            bottom.y += offset;
        }

        public void adjustAllX(float offset) {
            this.x += offset;
            top.x += offset;
            bottom.x += offset;
        }
    }

    class HPoint {
        public float x;
        public float y;
        public PointF left = new PointF();
        public PointF right = new PointF();

        public void setY(float y) {
            this.y = y;
            left.y = y;
            right.y = y;
        }

        public void adjustAllX(float offset) {
            this.x += offset;
            left.x += offset;
            right.x += offset;
        }
    }

    /**
     * 重写动画
     */
    private class MoveAnimation extends Animation {

        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            super.applyTransformation(interpolatedTime, t);

            // interpolatedTime 为当前动画帧对应的相对时间,值总在0-1之间
            mInterpolatedTime = interpolatedTime;
            invalidate();
        }
    }

    public void startAnimation() {
        mPath.reset();
        mInterpolatedTime = 0;
        MoveAnimation move = new MoveAnimation();
        move.setDuration(3000);
        move.setInterpolator(new AccelerateDecelerateInterpolator());
        startAnimation(move);
    }
}

在看代码的时候,可能会比较蒙,分状态画圆球的时候,有的*10,有的*5,有的又* (10f / 3),其实很好懂,只是为了把时间变化值调整为0-1,以便相乘来操作距离的变化

拿状态1举例吧

    /**
     * 状态1
     *
     * @param interpolatedTime 为当前动画帧对应的相对时间,值总在0-1之间
     */
    private void drawCircleStatus_1(float interpolatedTime) {//0~0.2
        initCirclePoint();
        p2.adjustAllX(stretchDistance * interpolatedTime * 5);
    }
p2.adjustAllX(stretchDistance * interpolatedTime * 5);

状态1 中,在时间后面乘了个 5,因为状态一,时间上是 0~0.2,为了拉伸一整个stretchDistance,所以乘了个5,其他状态以此类推。

行了,后续还会更新相关贝塞尔曲线的文章,如果您喜欢,请关注~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容

  • 贝塞尔曲线开发的艺术 一句话概括贝塞尔曲线:将任意一条曲线转化为精确的数学公式。 很多绘图工具中的钢笔工具,就是典...
    eclipse_xu阅读 27,528评论 38 370
  • 谈谈贝塞尔曲线 最近在做项目的时候,需要用到一个动画,非常简单的动画,简单到就是直接对一个View做平移… 然而虽...
    雨润听潮阅读 5,848评论 1 16
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,571评论 25 707
  • 本文主要内容为贝塞尔曲线原理解析并用 SurfaceView 实现其展示动画 关于SurfaceView 的使用,...
    涤生_Woo阅读 13,192评论 5 94
  • 考研复盘总结 一.进行考研的目的: (以下总结于16年9月初,原题为“我希望通过考研获得什么”) 1.自信:对艰苦...
    李药丸阅读 848评论 0 0