Android贝塞尔动画实现QQ,虎牙等平台点赞效果初探

本文详细解析了如何实现QQ附近人和直播平台点赞效果的自定义动画,包括属性动画、贝塞尔曲线、估器和插补器的使用。通过自定义ViewGroup和AnimatorSet,展示了动画集合的创建、添加动画以及执行过程。提供了实现效果的代码示例,供开发者参考。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

先上效果:


此效果模仿qq附近人,以及各直播平台的点赞效果.

原理解析:
        属性动画(一开始底部的放大渐变效果)+贝塞尔曲线(上飘过程的弯曲路线)+估值器+插补器(加速减速)

        属性动画大家都熟悉,过多介绍不再多说.下面我们主要介绍少贝塞尔曲线和估值器以及插补器的简单使用(由于本人也是初学阶段,如有错误欢迎指正).

首先我们得做一些知识的了解:如已了解贝塞尔曲线以及插补器,估值器等请跳过此干燥无味段落,直接看下面的代码逻辑思路分析.
何为贝塞尔曲线? 

贝塞尔曲线

编辑
贝塞尔曲线(Bézier curve),又称 贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。一般的矢量图形软件通过它来精确画出曲线,贝兹曲线由 线段节点组成,节点是可拖动的支点,线段像可伸缩的皮筋,我们在绘图工具上看到的钢笔工具就是来做这种矢量曲线的。贝塞尔曲线是计算机图形学中相当重要的参数曲线,在一些比较成熟的位图软件中也有贝塞尔 曲线工具,如PhotoShop等。在Flash4中还没有完整的曲线工具,而在Flash5里面已经提供出贝塞尔曲线工具。
贝 塞尔曲线于1962,由法国工程师皮埃尔·贝塞尔(Pierre Bézier)所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计。贝塞尔曲线最初由Paul de Casteljau于1959年运用de Casteljau演算法开发,以稳定数值的方法求出贝兹曲线。

由于用计算机 画图大部分时间是操作鼠标来掌握线条的路径,与 手绘的感觉和效果有很大的差别。即使是一位精明的画师能轻松绘出各种图形,拿到鼠标想随心所欲的画图也不是一件容易的事。这一点是计算机万万不能代替手工的工作,所以到目前为止人们只能颇感无奈。使用 贝塞尔工具画图很大程度上弥补了这一缺憾。
贝塞尔曲线
贝塞尔曲线是计算机图形图像造型的基本工具,是图形造型运用得最多的基本线条之一。它通过控制曲线上的四个点(起始点、终止点以及两个相互分 离的中间点)来创造、编辑图形。其中起重要作用的是位于曲线中央的控制线。这条线是虚拟的,中间与贝塞尔曲线交叉,两端是控制端点。移动两端的端点时贝塞 尔曲线改变曲线的曲率(弯曲的程度);移动中间点(也就是移动虚拟的控制线)时,贝塞尔曲线在起始点和终止点锁定的情况下做均匀移动。注意,贝塞尔曲线上 的所有控制点、 节点均可编辑。这种“智能化”的矢量线条为艺术家提供了一种理想的图形编辑与创造的工具。

线性公式

给定点P0、P1,线性贝兹曲线只是一条两点之间的直线。这条线由下式给出:



且其等同于线性插值。

二次方公式

二次方贝兹曲线的路径由给定点P0、P1、P2的函数B(t)追踪:



TrueType字型就运用了以贝兹样条组成的二次贝兹曲线。

三次方公式

P0、P1、P2、P3四个点在平面或在三维空间中定义了三次方贝兹曲线。曲线起始于P0走向P1,并从P2的方向来到P3。一般不会经过P1或P2;这两个点只是在那里提供方向资讯。P0和P1之间的间距,决定了曲线在转而趋进P3之前,走向P2方向的“长度有多长”。
曲线的参数形式为:



现代的成象系统,如PostScript、Asymptote和Metafont,运用了以贝兹样条组成的三次贝兹曲线,用来描绘曲线轮廓。

一般参数公式

阶贝兹曲线可如下推断。给定点P0、P1、…、Pn,其贝兹曲线即:



如上公式可如下递归表达: 用表示由点 P0、 P1、…、 Pn所决定的贝兹曲线。
用平常话来说,阶的贝兹曲线,即双阶贝兹曲线之间的插值。

公式说明

1.开始于P0并结束于Pn的曲线,即所谓的端点插值法属性。
2.曲线是直线的充分必要条件是所有的控制点都位在曲线上。同样的,贝塞尔曲线是直线的充分必要条件是控制点共线。
3.曲线的起始点(结束点)相切于贝塞尔多边形的第一节(最后一节)。
4.一条曲线可在任意点切割成两条或任意多条子曲线,每一条子曲线仍是贝塞尔曲线。
5.一些看似简单的曲线(如圆)无法以贝塞尔曲线精确的描述,或分段成贝塞尔曲线(虽然当每个内部控制点对单位圆上的外部控制点水平或垂直的的距离为时,分成四段的贝兹曲线,可以小于千分之一的最大半径误差近似于圆)。
6.位于固定偏移量的曲线(来自给定的贝塞尔曲线),又称作偏移曲线(假平行于原来的曲线,如两条铁轨之间的偏移)无法以贝兹曲线精确的形成(某些琐屑实例除外)。无论如何,现存的启发法通常可为实际用途中给出近似值。

装逼走一波儿~哭晕...  这是神马.咳咳~~  这一堆东西,我们用到的也就一个公式,建议了解一下请自行百度....


估值器:
            生成我们所给点的坐标使用,额....  只是这么说很难理解,一会儿直接看代码.
插补器:    
            插补器的原理就是通过改变实际执行动画的时间点,提前/延迟执行默认的时间点来达到加速/减速的效果。(所以,有些朋友可能想通过它来实现一些特定的轨 迹,这并不能达到目的。至少目前的api是不支持的。最多通过设置负时间/大于1的时间比,来使控件按设定的动画轨迹反方向/超出改变一点轨迹。)
            在Interpolator的实现类里面,都实现了一个float getInterpolator(float input)的方法。传入参数是正常执行动画的时间点,返回值是用户真正想要它执行的时间点。传入参数是0~1,返回值一般也是0~1。(0~1)表示整 段动画的过程。中间的0.2、0.3等小数表示在整个动画(原本是匀速的)中的位置,其实就是一个比值。如果返回值是负数,会沿着相反的方向执行。(如果 是alpha值,返回负数会有点问题,因为alpha值比较特殊,本身只能是0~1,并不是一个循环的周期,如果取了负值,这就很难确定了,要看 android是怎么计算alpha值的。)如果返回的是大于1,会超出正方向执行。同样,如果是alpha值,也存在不确定的效果。

其实呢,说白了就就是当要执行input的时间时,给它另外一个时间,让系统执行另外一个时间的效果。已达到改变效果.
在插补器里面的方法呢是这样的:
public float getInterpolation(float input) { 
        if (mFactor == 1.0f) { 
            return input * input; 
        } else { 
            return (float)Math.pow(input, mDoubleFactor); 
        } 
    } 

mFactor其实一直都是1,因为配置文件并没有提供设置其它值的入口,默认会是1。mDoubleFactor是2,由此可见,这个方法其实就 是个x^2的曲线。当input=0.3时,返回值=0.09,假如这个动画的总时长是1秒,也就是说,在0.3秒的时候只是执行0.09秒时候的效果。 这样说可能还有点难理解。

其实这个速度和x^2这条抛物线的斜率的变化是有关的,和斜率的变化率是正比关系。比如,上面这个加速插补器的曲线就如下图:



红色的那条是正常的时间-实际时间变化曲线:g(x)=x;实际时间是按照正常时间匀速变化的,斜率是常数1.而抛物线在[0,1]时,斜率先是0,再慢 慢增加的。也就是在0时,执行的时间是0,在0.1时,执行的时间(位置)依然是接近于0的时间点(执行的位置)。执行的时间一直比正常该执行的时间点 迟。但是执行的时间变化的增量也一直在变,而且越变越快,也就是斜率一直在变大,这导致了感官上看到了一个加速的过程。它起步的速度是一样的,但是它把原 本该执行的时间提前执行/或者推迟执行,由此产生速度不一致。

另附系统提供的其它几个插补器的曲线图:

1.AccelerateDecelerateInterpolator 加速减速插补器

 
 


取值是[0,1],可见,从原点开始,斜率是0,先圆滑地慢慢增大,再慢慢减小,最后返回值还是1。这就是一个先加速再减速的效果。而且整个过程是平滑变化的。这也是数学的奥妙啊。

2.DecelerateInterpolator 减速插补器

 
 


明显,虽然斜率一直是大过正常值(因为开始时执行较快,后来才慢慢回归正常时间,但直到最后一刻才回到),但斜率一直在减小,是一个减速的过程。

3.AnticipateInterpolator 向前插补器

 
 


这是一条符合g(x)=3x^3-2x^2的曲线。前面2/3秒之前(假设整个过程是1秒),返回值是负数。如果这是一个直线运动的动画,这段时间会向设 定的相反的方向运动一点,而且速度显得稍缓(反方向先加速再减速)。这之后以一个较快速度跑到终点(大概0.7秒时会回到原点)。

4.AnticipateOvershootInterpolator 向前向后插补器

 
 


这是3条曲线合并起来的图,我们只需要较深色的那条x轴上[0,0.5)和稍浅那条[0.5,1]这两段就可以。可以看到,前面一点时间有一段负数的返回值,而后面有一段大于1的返回值,这就有了向前向后的效果。

5.OvershootInterpolator 超出插补器

 



一堆废话走完下面我们开始整理思路撸码!!!!

简单思路分析:
            要想实现上面效果,首先我们得搞一个自定义控件来做.这个自定义控件简单点就是一个ViewGroup,我们不断的向里面添加view,然后让view执行我们写好的动画.当然这中间需要考虑view的释放.
附上分析图:



代码走起~

我们的这个自定义控件选择组合式自定义控件:
      1.首先我们的自定义ViewGroup继承RelativeLayout
    2.暴露一个addView方法,以方便适用添加view进来.在此方法中我们将view添加到当前的ViewGroup中,并且给view设置动画.

    /**
     * 添加一个View.
     */
    @SuppressLint("NewApi")
    public void addBezierView() {
        ImageView view = new ImageView(getContext());
        int nextInt = mRandom.nextInt(loves.length - 1);
        view.setImageDrawable(loves[nextInt]);
        mParams.addRule(CENTER_IN_PARENT);
        mParams.addRule(ALIGN_PARENT_BOTTOM);
        view.setLayoutParams(mParams);
        addView(view);
        AnimatorSet matorSet = getAnimatorSet(view);
        matorSet.setInterpolator(interpolators[mRandom.nextInt(interpolators.length-1)]);
        matorSet.start();
    }

     3.我们需要一个动画集合AnimatorSet来定义我们的动画,让view执行.用于代码有点多为了方便我们把获取AnimatorSet封装到一个getAnimatorSet(view)方法中,此方法返回一个AnimatorSet.
 
    /**
     * 获取一个贝塞尔+平移等动画效果的AnimatorSet;
     * 
     * @param view
     * @return
     */
    @SuppressLint("NewApi")
    private AnimatorSet getAnimatorSet(final ImageView view) {
        // 创建动画
        AnimatorSet set = new AnimatorSet();
        ObjectAnimator trax = ObjectAnimator.ofFloat(view, "scaleX", 0.4f, 1f);
        ObjectAnimator tray = ObjectAnimator.ofFloat(view, "scaleY", 0.4f, 1f);
        ObjectAnimator alpha = ObjectAnimator.ofFloat(view, "alpha", 0.4f, 1f);

        // 三个动画一起执行
        AnimatorSet enterSet = new AnimatorSet();
        enterSet.setDuration(mPDuration);
        enterSet.playTogether(trax, tray, alpha);

        // 创建贝塞尔动画
        ValueAnimator bezierAnimator = getBezierAnimator(view);

        // 所有动画一起执行
        set.playSequentially(enterSet, bezierAnimator);
        set.setTarget(view);

        // 给动画添加一个执行的状态监听,当动画执行结束的时候把view释放掉.
        set.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                removeView(view);
                super.onAnimationEnd(animation);
            }
        });
        return set;
    }


      4.步骤3中我们需要一个贝塞尔动画,封装为方法 getBezierAnimator(view)获取一个ValueAnimator对象.getBezierAnimator(view)方法具体实现如下.

    /**
     * 获取贝塞尔动画
     * 
     * @param view
     * @return
     */
    @SuppressLint("NewApi")
    private ValueAnimator getBezierAnimator(final ImageView view) {

        // 初始化贝塞尔动画的几个点
        PointF pointF0 = new PointF((cWidth - mWidth) / 2, cHeight - mHeight);
        PointF pointF1 = getTogglePointF(1);
        PointF pointF2 = getTogglePointF(2);
        PointF pointF3 = new PointF(mRandom.nextInt(cWidth), 0);

        // 贝塞尔动画的路径由 一个估值器来表示.
        // 获取一个估值器,估值器的点集为pointF1,pointF2;
        BezierEvaluator bezierEvaluator = new BezierEvaluator(pointF1, pointF2);
        ValueAnimator valueAnimator = ValueAnimator.ofObject(bezierEvaluator,
                pointF0, pointF3);
        valueAnimator.setDuration(mBDuration);

        // 给动画添加一个动画的进度监听;在动画执行的过程中动态的改变view的位置;
        valueAnimator.addUpdateListener(new AnimatorUpdateListener() {

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {

                PointF pointF = (PointF) animation.getAnimatedValue();
                view.setX(pointF.x);
                view.setY(pointF.y);

                // 设置view的透明度,达到动画执行过程view逐渐透明效果;
                view.setAlpha(1 - animation.getAnimatedFraction());

            }
        });

        return valueAnimator;
    }

    /**
     * 生成不同的PointF;
     * 
     * @param i
     *            从上到下标记依次为1-....
     * @return
     */
    private PointF getTogglePointF(int i) {

        PointF pointF = new PointF();
        pointF.x = mRandom.nextInt(cWidth);
        float nextFloat = mRandom.nextFloat();
        float nextFloat2 = mRandom.nextFloat();

        if (nextFloat > 0.5)
            nextFloat /= 2;

        if (nextFloat2 < 0.5)
            nextFloat2 /= 0.5;

        if (i == 1) {
            pointF.y = (float) (cHeight * nextFloat);
        } else if (i == 2) {
            pointF.y = (float) ((cHeight - mHeight )* nextFloat2);
        }
        return pointF;
    }


其中我们用到了一个估值器, bezierEvaluator  ,具体实现:

public class BezierEvaluator implements TypeEvaluator<PointF> {

    private PointF pointF1;
    private PointF pointF2;

    public BezierEvaluator(PointF pointF1, PointF pointF2) {

        this.pointF1 = pointF1;
        this.pointF2 = pointF2;

    }

    @Override
    public PointF evaluate(float fraction, PointF pointF0, PointF pointF3) {
        // TODO Auto-generated method stub

        PointF pointF = new PointF();
        //贝塞尔曲线的实现.根据贝塞尔曲线的公式得到x.y的值
        pointF.x = (float) ((pointF0.x * (Math.pow((1 - fraction), 3))) + 3
                * pointF1.x * fraction * (Math.pow((1 - fraction), 2)) + 3
                * pointF2.x * (Math.pow(fraction, 2) * (1 - fraction)) + pointF3.x
                * (Math.pow(fraction, 3)));

        pointF.y = (float) ((pointF0.y * (Math.pow((1 - fraction), 3))) + 3
                * pointF1.y * fraction * (Math.pow((1 - fraction), 2)) + 3
                * pointF2.y * (Math.pow(fraction, 2) * (1 - fraction)) + pointF3.y
                * (Math.pow(fraction, 3)));

        return pointF;
    }

}




上述实现效果,只是提供实现方式参考,可以根据此方法实现自己所需要的效果.


评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值