FlingAnimation/SpringAnimation实现弹球动画

You have to believe in yourself. That's the secret of success.

之前的QQ版本里面有个大表情的动画很炫酷,之前就想着怎么实现的。最近看了DynamicAnimation,发现利用它很容易实现类似效果,自己最近也在看Kotlin,因此就用Kotlin写个demo练练手。

实现效果

等我实现了效果,发现QQ已经把该功能去掉了T_T。原版的效果看不到了,直接放我实现的效果。

在这里插入图片描述

实现原理

窗口的左右抖动效果

借助SpringAnimation 可以实现,顾名思义该动画库能实现弹簧般的抖动效果。与弹簧相关的两个重要物理属性分别是Damping ratio(阻尼比)和Stiffness(刚性)。

  • Damping ratio(阻尼比)

阻尼比描述了弹簧振荡过程中逐渐减小的速度。 通过阻尼比,可以调整振荡从一次反弹到下一次反弹的衰减速度。

官方用法示例:


findViewById<View>(R.id.imageView).also { img ->

    SpringAnimation(img, DynamicAnimation.TRANSLATION_Y).apply {

        …

        //Setting the damping ratio to create a low bouncing effect.

        spring.dampingRatio = SpringForce.DAMPING_RATIO_LOW_BOUNCY

        …

    }

}

动画库已经提供了四种类型的阻尼比,官方也提供了相对应的效果图

DAMPING_RATIO_HIGH_BOUNCY

在这里插入图片描述

DAMPING_RATIO_MEDIUM_BOUNCY

在这里插入图片描述

DAMPING_RATIO_LOW_BOUNCY

在这里插入图片描述

DAMPING_RATIO_NO_BOUNCY

在这里插入图片描述
  • Stiffness(刚性)

刚性指弹簧产生形变时,产生的弹力大小。相应的系统也提供了四种值。

STIFFNESS_HIGH

STIFFNESS_MEDIUM

STIFFNESS_LOW

STIFFNESS_VERY_LOW

Stiffness的值越大,弹簧动画看起来的抖动也就越不明显,就像我们施加相同的力给粗细不同的铁丝弹簧,施加的力取消后,越粗的弹簧刚性越大,它的抖动就没有细的弹簧明显。

官方用法示例:


findViewById<View>(R.id.imageView).also { img ->

    SpringAnimation(img, DynamicAnimation.TRANSLATION_Y).apply {

        …

        //Setting the spring with a low stiffness.

        spring.stiffness = SpringForce.STIFFNESS_LOW

        …

    }

}

  • SpringForce自定义弹簧动画属性

如果想将Damping ratio/Stiffness 应用在多个弹簧动画中,可以通过SpringForce 来设置。


SpringForce force = new SpringForce();

force.setDampingRatio(DAMPING_RATIO_LOW_BOUNCY).setStiffness(STIFFNESS_LOW);

setSpring(force);

有了上面的知识,可以很容易的实现窗口左右抖动的效果。


private fun startWindowAnimation() {

        val springAnimation = createSpringAnimation(window.decorView, SpringAnimation.TRANSLATION_X);

        springAnimation.start();

    }

fun createSpringAnimation(view: View, property: DynamicAnimation.ViewProperty): SpringAnimation {

        val animation = SpringAnimation(view, property)

        animation.setStartVelocity(4000f);

        val spring = SpringForce(0f)

        spring.stiffness = SpringForce.STIFFNESS_MEDIUM

        spring.dampingRatio = SpringForce.DAMPING_RATIO_HIGH_BOUNCY

        animation.spring = spring

        animation.setMinimumVisibleChange(DynamicAnimation.MIN_VISIBLE_CHANGE_PIXELS)

        return animation

    }

在这里插入图片描述

弹球动画

弹球动画比较麻烦点,拆分成三个步骤。

  • 动态添加view

每点击一次button,就动态添加一个弹球view,然后在表情下落时我们添加一个变淡的动画,在动画结束时在将该view删除掉。

  • X轴上的动画

弹球的动画拆分成X轴跟Y轴,在分别实现以此来降低处理难度。弹球在X轴没有重力影响,只有空气阻力跟碰撞引起的速度衰减,因此很容易通过FlingAnimation动画来模拟,为了进一步简化操作,当弹球碰撞到左右屏幕边缘时,只是将速度取反,不考虑碰撞损失的速度。

FlingAnimation动画可以通过setFriction方法设置一个速度衰减因子,通过该值就可以模拟处X轴受到的空气阻力。


private fun startBallAnimation(movingView: View) {

        val flingXAnimation = createFlingAnimation(movingView, DynamicAnimation.TRANSLATION_X);

        flingXAnimation.addUpdateListener { animation, value, velocity ->

            if (velocity > 0 && movingView.x > screenW - movingView.width) {

                flingXAnimation.setStartVelocity(-Math.abs(velocity))

            } else if (velocity < 0 && movingView.x < 0) {

                flingXAnimation.setStartVelocity(Math.abs(velocity))

            }

            animation.start()

        }

        flingXAnimation.setFriction(0.2f)

        flingXAnimation.start();

}

fun createFlingAnimation(view: View, property: DynamicAnimation.ViewProperty): FlingAnimation {

        val animation = FlingAnimation(view, property)

        animation.setStartVelocity(-3000f).setFriction(0.01f)

        animation.setMinimumVisibleChange(DynamicAnimation.MIN_VISIBLE_CHANGE_PIXELS)

        return animation

    }

在这里插入图片描述
  • Y轴上的动画

Y轴上的动画继续分成两个部分,初次上抛时通过FlingAnimation来模拟,当弹球碰到屏幕的顶端后,转换成属性动画,先取消掉Y轴上的FlingAnimation,在通过给属性动画设置BounceInterpolator插值器来模拟,同时也设置上变淡的动画。在属性动画结束时移除掉弹球。


private fun startBallAnimation(movingView: View) {

        //X轴方向的动画

        val flingXAnimation = createFlingAnimation(movingView, DynamicAnimation.TRANSLATION_X);

        flingXAnimation.addUpdateListener { animation, value, velocity ->

            if (velocity > 0 && movingView.x > screenW - movingView.width) {

                flingXAnimation.setStartVelocity(-Math.abs(velocity))

            } else if (velocity < 0 && movingView.x < 0) {

                flingXAnimation.setStartVelocity(Math.abs(velocity))

            }

            animation.start()

        }

        flingXAnimation.setFriction(0.2f)

        flingXAnimation.start();

        //Y轴方向的动画

        val flingYAnimation = createFlingAnimation(movingView, DynamicAnimation.TRANSLATION_Y);

        flingYAnimation.addUpdateListener { animation, value, velocity ->

            if (velocity < 0 && movingView.y < 0) {

                flingYAnimation.setStartVelocity(Math.abs(velocity)).setFriction(0.3f)

            } else if (velocity > 0) {

                flingYAnimation.cancel();

                val holder1 = PropertyValuesHolder.ofFloat(View.ALPHA, 1f, 1f, 0f)

                val holder2 = PropertyValuesHolder.ofFloat(

                    View.Y,

                    movingView.getY(),

                    (getScreenHeight(this) - 200 - movingView.height).toFloat()

                )

                val animator = ObjectAnimator.ofPropertyValuesHolder(movingView, holder1, holder2)

                animator.setDuration(3000);

                val bounceInterpolator = BounceInterpolator()

                animator.setInterpolator(bounceInterpolator);

                animator.addListener(object : AnimatorListenerAdapter() {

                    override fun onAnimationEnd(animation: Animator?) {

                        super.onAnimationEnd(animation)

                        rootView?.removeView(movingView)

                    }

                })

                animator.start()

            }

        }

        flingYAnimation.start();

    }

    fun createFlingAnimation(view: View, property: DynamicAnimation.ViewProperty): FlingAnimation {

        val animation = FlingAnimation(view, property)

        animation.setStartVelocity(-3000f).setFriction(0.01f)

        animation.setMinimumVisibleChange(DynamicAnimation.MIN_VISIBLE_CHANGE_PIXELS)

        return animation

    }

通过以上步骤,就实现了弹球效果。

完整代码点击这里

总结

最近刚在学习kotlin,demo代码看起来结构不太清晰,有疑问或者更好的实现方案欢迎交流。

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

推荐阅读更多精彩内容