Android开发之贝塞尔曲线进阶篇(仿直播送礼物,饿了么购物车动画)

又是一年毕业季,今年终于轮到我了,最近一边忙着公司的项目,一边赶着毕设和论文,还私下和朋友搞了些小外包,然后还要抽出时间写博客,真是忙的不要不要的。

好了,言归正传,前几天写了一篇关于贝塞尔曲线的基础篇,如果你对贝塞尔曲线还不是很了解,建议你先去阅读下:Android开发之贝塞尔曲线初体验
,今天这篇文章主要来讲讲关于贝塞尔曲线的实际应用。

国际惯例,先来看下今天要实现的效果图:

仿直播送礼动画
仿饿了么购物车动画

上面两张图分别是仿直播平台送礼动画和饿了么商品加入购物车动画。

1、小试牛刀

我们先来热热身,这里我打算用二阶贝塞尔曲线画出动态波浪的效果,效果如下:

动态波浪

效果还是不错的,很自然的动画呈现,平滑的过渡。
我们来一步步分析下:
1、首先,我们先单纯的思考屏幕内的可见区域,可以把它理解成近似一个周期的sin函数,只是它的幅度没有那么高,类似下图:

sin函数

根据上面的图,其实我们可以发现它的起始点分别是(0,0)和(2π,0),控制点分别是(π/2,1)和(3π/2,-1),由于有两个控制点,所以这里可以用三阶贝塞尔曲线来画,不过我暂时打算先用二阶贝塞尔曲线来画,也就是把上面的图拆分成两部分:
第一部分:起始点为(0,0)和(π,0),控制点为(π/2,1)
第二部分:起始点为(π,0)和(2π,0),控制点为(3π/2,-1)
然后我们把2π的距离当成是屏幕的宽度,那么π的位置就是屏幕宽度的一半,这样分解下来,配合谷歌官方给我们提供的API,我们就可以很好的实现这2段曲线的绘制,我们先暂定波浪的高度为100px,实现代码也就是:

        mPath.moveTo(0, mScreenHeight / 2);
        mPath.quadTo(mScreenWidth / 4, mScreenHeight / 2 - 100, mScreenWidth / 2 , mScreenHeight / 2);
        mPath.quadTo(mScreenWidth * 3 / 4, mScreenHeight / 2 + 100, mScreenWidth , mScreenHeight / 2);

然后我们把下面的空白区域铺满:

        mPath.lineTo(mScreenWidth, mScreenHeight);
        mPath.lineTo(0, mScreenHeight);

来看下此时的效果图:

波浪图

2、实现了初步的效果,那现在我们就应该来思考如何让这个波浪动起来,其实很简单,只需要我们在屏幕外再画出另一周期的曲线,然后让它做平移动画这样就可以了,熟悉sin函数的朋友,肯定能想到下面这幅图:

sin函数

现在我们把屏幕外的另一半也曲线也画出来(具体坐标这里就不再写出来了,大家画下图就能清楚):

        mPath.moveTo(-mScreenWidth + mOffset, mScreenHeight / 2);
        mPath.quadTo(-mScreenWidth * 3 / 4 + mOffset, mScreenHeight / 2 - 100, -mScreenWidth / 2 + mOffset, mScreenHeight / 2);
        mPath.quadTo(-mScreenWidth / 4 + mOffset, mScreenHeight / 2 + 100, 0 + mOffset, mScreenHeight / 2);
        mPath.quadTo(mScreenWidth / 4 + mOffset, mScreenHeight / 2 - 100, mScreenWidth / 2 + mOffset, mScreenHeight / 2);
        mPath.quadTo(mScreenWidth * 3 / 4 + mOffset, mScreenHeight / 2 + 100, mScreenWidth + mOffset, mScreenHeight / 2);

3、平移动画的实现,这里我们利用到了Android3.0以后给我们提供的属性动画,然后平移长度即为一个周期长度(屏幕宽度):

    /**
     * 设置动画效果
     */
    private void setViewanimator() {
        ValueAnimator valueAnimator = ValueAnimator.ofInt(0, mScreenWidth);
        valueAnimator.setDuration(1200);
        valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mOffset = (int) animation.getAnimatedValue();//当前平移的值
                invalidate();
            }
        });
        valueAnimator.start();
    }

拿到平移的值后,我们只需要在各点的x轴动态的加上值,这样就会呈现出动态波浪了。

        mPath.quadTo(-mScreenWidth * 3 / 4 + mOffset, mScreenHeight / 2 - 100, -mScreenWidth / 2 + mOffset, mScreenHeight / 2);
        mPath.quadTo(-mScreenWidth / 4 + mOffset, mScreenHeight / 2 + 100, 0 + mOffset, mScreenHeight / 2);
        mPath.quadTo(mScreenWidth / 4 + mOffset, mScreenHeight / 2 - 100, mScreenWidth / 2 + mOffset, mScreenHeight / 2);
        mPath.quadTo(mScreenWidth * 3 / 4 + mOffset, mScreenHeight / 2 + 100, mScreenWidth + mOffset, mScreenHeight / 2);

可以简化写成

        for (int i = 0; i < 2; i++) {
            mPath.quadTo(-mScreenWidth * 3 / 4 + (mScreenWidth * i) + mOffset, mScreenHeight / 2 - 100, -mScreenWidth / 2 + (mScreenWidth * i) + mOffset, mScreenHeight / 2);
            mPath.quadTo(-mScreenWidth / 4 + (mScreenWidth * i) + mOffset, mScreenHeight / 2 + 100, +(mScreenWidth * i) + mOffset, mScreenHeight / 2);
        }

2、仿饿了么商品加入动画效果:

如果你理解了上面的“小试牛刀”例子,要实现这个效果就非常容易了,首先我们要确定添加购物车“+”的位置,然后确定购物车的位置,也就是我们贝塞尔曲线的起始点了,然后再给出一个控制点,只需要让它比“+”的位置高一些,让它成抛物线的效果即可。

1、要确定一个View所在屏幕内的位置,我们可以利用谷歌官方给我们提供的API(具体根据界面中的布局来确定):
getLocationInWindow(一个控件在其父窗口中的坐标位置)
getLocationOnScreen(一个控件在其整个屏幕上的坐标位置)

 /**
     * <p>Computes the coordinates of this view on the screen. The argument
     * must be an array of two integers. After the method returns, the array
     * contains the x and y location in that order.</p>
     *
     * @param outLocation an array of two integers in which to hold the coordinates
     */
    public void getLocationOnScreen(@Size(2) int[] outLocation) {
        getLocationInWindow(outLocation);

        final AttachInfo info = mAttachInfo;
        if (info != null) {
            outLocation[0] += info.mWindowLeft;
            outLocation[1] += info.mWindowTop;
        }
    }

    /**
     * <p>Computes the coordinates of this view in its window. The argument
     * must be an array of two integers. After the method returns, the array
     * contains the x and y location in that order.</p>
     *
     * @param outLocation an array of two integers in which to hold the coordinates
     */
    public void getLocationInWindow(@Size(2) int[] outLocation) {
        if (outLocation == null || outLocation.length < 2) {
            throw new IllegalArgumentException("outLocation must be an array of two integers");
        }

        outLocation[0] = 0;
        outLocation[1] = 0;

        transformFromViewToWindowSpace(outLocation);
    }

这里可以获取到一个int类型的数组,数组下标0和1分别代表着x和y坐标,需要注意的一点是,别在onCreate里去调用这个方法(点击事件内可以),否则获取到的坐标只会是(0,0),这个方法需要在Activity获取到焦点后调用才有效果。

2、当我们拿到了这3点坐标,我们就可以画出对应的贝塞尔曲线。然后我们只需要让这个小红点在这条曲线路径里去做平滑移动就可以了,由于小红点是带有x,y坐标的,曲线的每一个点也是带有x,y坐标的,聪明的你应该已经想到这里还是一样用到了属性动画,动态的去改变当前小红点的x,y坐标即可。
由于谷歌官方只给我们提供了一些比较基础的插值器,比如Int,Float,Argb等,并没有给我们提供关于坐标的插值器,不过好在它给我们开放了相关接口,我们只需要对应的去实现它即可,这个接口叫TypeEvaluator:

/**
 * Interface for use with the {@link ValueAnimator#setEvaluator(TypeEvaluator)} function. Evaluators
 * allow developers to create animations on arbitrary property types, by allowing them to supply
 * custom evaluators for types that are not automatically understood and used by the animation
 * system.
 *
 * @see ValueAnimator#setEvaluator(TypeEvaluator)
 */
public interface TypeEvaluator<T> {

    /**
     * This function returns the result of linearly interpolating the start and end values, with
     * <code>fraction</code> representing the proportion between the start and end values. The
     * calculation is a simple parametric calculation: <code>result = x0 + t * (x1 - x0)</code>,
     * where <code>x0</code> is <code>startValue</code>, <code>x1</code> is <code>endValue</code>,
     * and <code>t</code> is <code>fraction</code>.
     *
     * @param fraction   The fraction from the starting to the ending values
     * @param startValue The start value.
     * @param endValue   The end value.
     * @return A linear interpolation between the start and end values, given the
     *         <code>fraction</code> parameter.
     */
    public T evaluate(float fraction, T startValue, T endValue);

}

从注释里我们可以得到这些信息,首先我们需要去实现evaluate方法,然后这里提供了3个回调参数,它们分别代表:
float fraction:动画的完成程度,0~1
T startValue:动画开始值
T endValue: 动画结束值(这里而外补充一点,要想得到当前的动画值其实也很简单,只需要用(动画开始值+动画完成程度*动画结束值))
这里贴下关于小红点移动坐标的插值器代码:(Point是系统自带的类,可以用来记录X,Y坐标点)

    /**
     * 自定义Evaluator
     */
    public class CirclePointEvaluator implements TypeEvaluator {

        /**
         * @param t   当前动画进度
         * @param startValue 开始值
         * @param endValue   结束值
         * @return
         */
        @Override
        public Object evaluate(float t, Object startValue, Object endValue) {

            Point startPoint = (Point) startValue;
            Point endPoint = (Point) endValue;

            int x = (int) (Math.pow((1-t),2)*startPoint.x+2*(1-t)*t*mCircleConPoint.x+Math.pow(t,2)*endPoint.x);
            int y = (int) (Math.pow((1-t),2)*startPoint.y+2*(1-t)*t*mCircleConPoint.y+Math.pow(t,2)*endPoint.y);

            return new Point(x,y);
        }

    }

这里的x和y是根据二阶贝塞尔曲线计算出来的,对应的公式为:


二阶贝塞尔表达式

然后我们在值变化监听器中去不断绘制这个小红点的位置就可以了。

        //设置值动画
        ValueAnimator valueAnimator = ValueAnimator.ofObject(new CirclePointEvaluator(), mCircleStartPoint, mCircleEndPoint);
        valueAnimator.setDuration(600);
        valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                Point goodsViewPoint = (Point) animation.getAnimatedValue();
                mCircleMovePoint.x = goodsViewPoint.x;
                mCircleMovePoint.y = goodsViewPoint.y;
                invalidate();
            }
        });

3、仿直播送礼物:

有了前两个例子的基础,现在要做类似于这种运动轨迹的效果是不是很有感觉了?打铁要趁热,我们接着来说直播送礼这个效果。
首先,我们先简化一下,看下图:


仿直播送礼

1、首先我们需要知道这条曲线的路径要怎么画,这里我应该不需要我再说了,三阶贝塞尔曲线,起始点和结束点分别为(屏幕宽度的一半,屏幕高度)和(屏幕宽度的一半,0),然后控制点有2个,分别是(屏幕宽度,四分之三屏幕高度)和(0,四分之一屏幕高度)

        mPath.moveTo(mStartPoint.x, mStartPoint.y);
        mPath.cubicTo(mConOnePoint.x, mConOnePoint.y, mConTwoPoint.x, mConTwoPoint.y, mEndPoint.x, mEndPoint.y);
        canvas.drawPath(mPath, mPaint);

2、然后我们来说下关于这个星星的实现,这里是用到一张星星的图片,通过资源文件转Bitmap对象,再赋予给所创建的Canvas画布,然后通过Xfermodes将图片进行渲染变色,最后通过ImageView来加载。

来自Graphics下的XferModes

这里我们取SrcIn模式,也就是我们先绘制Dst(资源文件),然后再绘制Src(画笔颜色),当我们设置SrcIn模式时,自然就剩下的Dst的形状+Src的颜色,也就是不同颜色的星星。

    /**
     * 画星星并随机赋予不同的颜色
     *
     * @param color
     * @return
     */
    private Bitmap drawStar(int color) {
        //创建和资源文件Bitmap相同尺寸的Bitmap填充Canvas
        Bitmap outBitmap = Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(outBitmap);
        canvas.drawBitmap(mBitmap, 0, 0, mPaint);
        //利用Graphics中的XferModes对Canvas进行着色
        canvas.drawColor(color, PorterDuff.Mode.SRC_IN);
        canvas.setBitmap(null);
        return outBitmap;
    }

3、接下来就是让星星动起来,老套路,我们利用属性动画,去获取贝塞尔曲线上的各点坐标位置,然后动态的给ImageView设置坐标即可。这里的坐标点我们需要通过三阶贝塞尔曲线公式来计算:


三阶贝塞尔表达式
   public class StarTypeEvaluator implements TypeEvaluator<Point> {

        @Override
        public Point evaluate(float t, Point startValue, Point endValue) {
            //利用三阶贝塞尔曲线公式算出中间点坐标
            int x = (int) (startValue.x * Math.pow((1 - t), 3) + 3 * mConOnePoint.x * t * Math.pow((1 - t), 2) + 3 *
                    mConTwoPoint.x * Math.pow(t, 2) * (1 - t) + endValue.x * Math.pow(t, 3));
            int y = (int) (startValue.y * Math.pow((1 - t), 3) + 3 * mConOnePoint.y * t * Math.pow((1 - t), 2) + 3 *
                    mConTwoPoint.y * Math.pow(t, 2) * (1 - t) + endValue.y * Math.pow(t, 3));
            return new Point(x, y);
        }
    }

4、然后再带上一个渐隐(透明度)的属性动画动画即可。

        //设置属性动画
        ValueAnimator valueAnimator = ValueAnimator.ofObject(new StarTypeEvaluator(pointFFirst, pointFSecond), pointFStart,
                pointFEnd);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                Point point = (Point) animation.getAnimatedValue();
                imageView.setX(point.x);
                imageView.setY(point.y);
            }
        });

        valueAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                StarViewGroup.this.removeView(imageView);
            }
        });


        //透明度动画
        ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(imageView, "alpha", 1.0f, 0f);

        //组合动画
        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.setDuration(3500);
        animatorSet.play(valueAnimator).with(objectAnimator);
        animatorSet.start();


        valueAnimator.start();

这样我们就实现了上面简化版的效果,然后我们来完成下最终满屏星星。
1、首先,这个星星我们是通过资源文件加载到Canvas画布,然后再装载到ImageView里去显示,现在屏幕里有很多星星,所以我们考虑自定义一个ViewGroup,让其继承于RelativeLayout。

2、再来观察下效果图,发现这些星星大致是往一定的轨迹在飘动,但是位置好像又不是一层不变的,所以这里我们可以知道,这4个关键点(起始点,结束点,2个控制点)是会变化的,所以我们只可以监听下这个ViewGroup的onTouch事件,在用户触摸屏幕的时候,去动态生成这几个点的坐标,其他的就没变化了,根据三阶贝塞尔曲线公式就可以星星当前所在的位置,然后进行绘制。

    /**
     * 监听onTouch事件,动态生成对应坐标
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mStartPoint = new Point(mScreenWidth / 2, mScreenHeight);
        mEndPoint = new Point((int) (mScreenWidth / 2 + 150 * mRandom.nextFloat()), 0);
        mConOnePoint = new Point((int) (mScreenWidth * mRandom.nextFloat()), (int) (mScreenHeight * 3 * mRandom.nextFloat() / 4));
        mConTwoPoint = new Point(0, (int) (mScreenHeight * mRandom.nextFloat() / 4));

        addStar();
        return true;
    }

好了,文章到这里就结束了,由于篇幅限制,这里不能对一些东西讲的太细,比如一些自定义View的基础,还有属性动画的用法,大家自行查阅相关资料哈。

源码下载:

这里附上源码地址(欢迎Star,欢迎Fork):源码下载

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

推荐阅读更多精彩内容