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

今天来自李晨玮分享的直播礼物效果Demo。对于直播中送车,点赞都有借鉴意义。李晨玮的简书为:http://www.jianshu.com/u/b2df0a5ead6f,欢迎大家关注。

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

仿直播送礼动画

仿饿了么购物车动画

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

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);

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

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

1、要确定一个View所在屏幕内的位置,我们可以利用谷歌官方给我们提供的API(具体根据界面中的布局来确定):

/**
     * <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的基础,还有属性动画的用法,大家自行查阅相关资料哈。

原文发布于微信公众号 - 何俊林(DriodDeveloper)

原文发表时间:2017-11-07

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏QQ空间开发团队的专栏

手 Q 人脸识别动画实现详解

当存在多个人脸的时候进行主次脸动画的切换,摄像头移动的时候动画的追踪,多个动画的之间的时序控制等问题,总之,UI 展示加上各种业务逻辑使得这个动画变得异常复杂。

2K4
来自专栏腾讯NEXT学位

我不知道你知不知道但前端NEXT知道的伪元素小技巧

3647
来自专栏前端侠2.0

通过CSS,实现元素反转 原

      今天突然看到ES6还有Reflect 概念,我怎么以前没注意过,赶快去CANIUSE上查一下吧,却意外看到CSS Reflections,竟然可以有...

2351
来自专栏Android知识点总结

Android原生绘图之炫酷倒计时

4444
来自专栏Android知识点总结

Android关于Color你所知道的和不知道的一切

1544
来自专栏Android知识点总结

D6-Android绘图之一言不合画个表

1254
来自专栏Coco的专栏

奇妙的 CSS shapes(CSS图形)

2565
来自专栏Jack的Android之旅

自定义天气显示温度变化的LinearChart控件

这次发表的是前几个月搞定的一个自定义控件,那时自己在写一个小的查看天气的软件,在这过程中就涉及了显示天气变化的折线图,一开始想用一些画图框架来解决问题,不过考虑...

2771
来自专栏Spring相关

使用canvas绘制渐变色矩形和使用按键控制人物移动

1243
来自专栏Android常用基础

自定义View(一)-动画- XML生成View动画

感觉好久没有写博客了。首先因为最近比较忙,有在学习即时通讯相关的开源项目,好不容易忙完了。有点时间就抓紧写博客。之前学习的开源项目百篮应用已经获得360+sta...

1261

扫码关注云+社区

领取腾讯云代金券