自定义View 仿微信聊天炸弹效果

6,436 阅读6分钟

Long Long Ago,微信8.0更新了一个“炸屎”的新功能,还蛮有新意。

这两天和朋友聊天触发了这个功能,当时心想,诶!这文章素材不就来了吗?!趁着周末时间,赶紧来实现一下。

我们先来看看整体实现的效果:

boom.gif

本篇文章无技术含量,纯属娱乐,纯属娱乐

思路

经过不断试验,反复查看了微信炸💩的效果,基本上可以将其拆解成几个小部分。

  • 选择炸弹表情,发送后炸弹会以抛物线的形式向对方发送,同时💣会进行翻转;
  • 在💣碰到💩表情的时候,出现爆炸动画;
  • 在动画爆炸的同时,出现大概7个💩表情,随机朝向,做放大效果,最后下移并渐渐消失。

效果已经拆解了,技术方面具体实现思路如下:

  • IM聊天界面,主要以Recyclerview分为发送方和接收方两种type;
  • 以贝塞尔曲线实现炸弹抛物线的效果,这里用二阶贝塞尔曲线即可;
  • 炸弹抛过去的同时,使用动画进行旋转;
  • Lottie实现爆炸效果;
  • 缩放、平移、渐变动画实现7个💩的炸出效果。

具体实现

IM聊天界面

Im聊天界面很简单,一个Recyclerview,分为左右两个type,根据type值,分别加载左侧和右侧的布局。

class ImAdapter  : BaseDelegateMultiAdapter<ImMsg?, BaseViewHolder>(){
    companion object {
        private const val TAG = "ImMsgAdapter"
        private const val TYPE_LEFT = 0
        private const val TYPE_RIGHT = 1
    }

    init {
        setMultiTypeDelegate(object : BaseMultiTypeDelegate<ImMsg?>() {
            override fun getItemType(data: List<ImMsg?>, position: Int): Int {

                if (data[position]?.type == 0) {
                    return TYPE_LEFT
                }
                return TYPE_RIGHT
            }
        })
        Objects.requireNonNull(getMultiTypeDelegate())!!
            .addItemType(TYPE_LEFT, R.layout.item_im_left)
            .addItemType(TYPE_RIGHT, R.layout.item_im_right)
    }

    override fun convert(holder: BaseViewHolder, item: ImMsg?) {

        if (holder.itemViewType == TYPE_LEFT) {
            holder.setText(R.id.tv_chat_left_msg,item?.msg)
        } else {
            holder.setText(R.id.tv_chat_right_msg,item?.msg)
        }

    }
}

这里创建一个实体类ImMsg,用于模拟聊天数据。

data class ImMsg(val type:Int,val msg:String)


//初始化聊天信息,模拟数据
val msgList = mutableListOf<ImMsg>()
msgList.add(ImMsg(0, "Hello!"))
msgList.add(ImMsg(0, "我们来测试一下"))
msgList.add(ImMsg(0, "微信爆炸效果"))
msgList.add(ImMsg(1, "欧了!"))
msgList.add(ImMsg(0, "\uD83D\uDE14"))
msgList.add(ImMsg(0, "\uD83D\uDCA9"))
msgList.add(ImMsg(0, "\uD83D\uDCA9"))
msgList.add(ImMsg(0, "\uD83D\uDCA9"))

val adapter = ImAdapter()
imRv.adapter = adapter
adapter.addData(msgList)

当然,聊天界面还需要加上输入框,以及表情栏,这里就不一一展示。

发送炸弹💣

聊天界面搭建完成,接下来就开始进入正题,从扔炸弹开始。

上面已经分析了,扔炸弹的过程其实是一个类似抛物线的过程,从右侧聊天发送出去开始到碰到左侧💩结束。

掘金-付十一.png

看到扔炸弹的过程,第一反应就是用贝塞尔曲线来解决炸弹路线的问题,炸弹的行进路线比较简单,这里用二阶贝塞尔曲线就能实现。

我们都知道,要实现二阶贝塞尔曲线,至少得知道两个点的数据,一个是控制点,一个是终点的坐标。

从上图就可以基本知道,控制点在开始点和终点中间靠上的位置,这里以屏幕宽和高来做计算,模拟出控制点。

val resources: Resources = this.resources
val dm: DisplayMetrics = resources.displayMetrics
val scWidth = dm.widthPixels
val scHeight = dm.heightPixels
val controlX : Int = scWidth/2 - 400//控制点x
val controlY : Int = scHeight/2 - 400//控制点y

终点同样的道理,以起点为参考值,计算出坐标

val lastX = ivBomb.x-200
val lastY = ivBomb.y-800

有个控制点和终点的坐标,那就开始绘制二阶贝塞尔曲线,实现贝塞尔曲线,Android中已经有现成的API,Path中有个quadTo方法,只要传入控制点和终点的坐标,即生成贝塞尔曲线路径。

path.quadTo(
    -controlX,
    -controlY,
    -lastX,
    -lastY
)

有了path,接下来就是将炸弹按照贝塞尔曲线动起来,这里就需要用到属性动画结合PathMeasure,让炸弹💣按照路径移动。

pathMeasure = PathMeasure(path, false)
valueAnimator = ValueAnimator.ofFloat(0f, pathMeasure.length)
valueAnimator.duration = MILLS + 100
valueAnimator.interpolator = LinearInterpolator()
valueAnimator.addUpdateListener { animation ->
    val animatedValue = animation.animatedValue as Float
    pathMeasure.getPosTan(animatedValue, mCurrentPosition, null)
    ivBomb.translationX = mCurrentPosition[0]
    ivBomb.translationY = mCurrentPosition[1]
    ivBomb.rotation = animatedValue
}

微信的炸弹在抛物线的同时,是有个不断翻转的效果。

那我们实现就只需要监听属性动画的更新listener,在监听移动的过程中,设置rotation,使炸弹不断进行翻转。

我们来看效果:

掘金_付十一.gif

爆炸效果

接下来该说的就是炸弹的爆炸效果,这里给出的方案是使用Lottie库来加载。微信的爆炸效果还是蛮逼真,找了好久,都没找到相应的素材,这里属实无法模拟。

于是就从lottiefiles.com/ 资源库中找到了一个类似爆炸效果的json文件,结合Lottie实现爆炸的效果。

60963-fire-element-effect-animation.gif

Lottie的使用也很简单,添加依赖

implementation 'com.airbnb.android:lottie:3.4.0'

在layout中添加LottieAnimationView

<com.airbnb.lottie.LottieAnimationView
    android:id="@+id/lottie_layer_view"
    android:layout_width="200dp"
    android:layout_height="200dp"
    app:layout_constraintBottom_toTopOf="@id/cl_ed_send"
    android:layout_marginBottom="140dp"/>

在炸弹执行抛物线结束时,开始播放

mLottieAnimation?.playAnimation()

来看下炸弹结合爆炸的整体效果

448x282_file1.gif

💩炸开效果

当炸弹爆炸后,有一个💩被炸开的效果,如下图。

2579ba679ff6a727cdb9b1bcecf2eec.png

从微信效果上看,存在7个💩的表情,并且每个表情随机倾斜角度,从爆炸动画那里为起点,由小到大弹出,形成由远及近的效果。

实现的方案是自定义View绘制出7个相同的表情图,利用Math.random随机旋转角度,且7个表情在可控范围内随机摆放。

//爆炸刚开始时随机分别7个图,且旋转角度随机
for (i in 0..6) {
    dst?.let {
        it.left = i * 50 + 150
        it.top = mHeight + (Math.random() * 400).toInt() - mHeight / 4
        it.right = it.left + 100
        it.bottom = it.top + 100
    }
    
    val matrix = Matrix()
    matrix.postScale(i.toFloat() + 4, i.toFloat() + 4)
    matrix.postTranslate((-bitmap.width / 2).toFloat(), (-bitmap.height / 2).toFloat())
    matrix.postRotate((Math.random() * 70).toFloat())
    matrix.postTranslate(dst!!.top.toFloat(), dst!!.left.toFloat())
    val bitmap1 =
        Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
    canvas.drawBitmap(bitmap1, null, dst!!, paint)
    matrix.reset()
}

至于由远及近的效果,就需要对自定义的粑粑View进行缩放动画。

val scaleXAni = ObjectAnimator.ofFloat(baView, "scaleX", 0f, 1.8f)
val scaleYAni = ObjectAnimator.ofFloat(baView, "scaleY", 0f, 1.5f)

另外,在💩炸开的最后,💩会向下移动并且同时渐变透明,所以在缩放动画后开始平移动画 translation和渐变动画alpha。

val tranY1Ani = ObjectAnimator.ofFloat(baView, "translationY", 0f, -300f)
val tranYAni = ObjectAnimator.ofFloat(baView, "translationY", -300f, 300f)
val alphaAni = ObjectAnimator.ofFloat(baView, "alpha", 1f, 0f)

最后使用AnimatorSet将动画集合,开始播放。

val animatorSet = AnimatorSet()
animatorSet.play(scaleXAni).with(scaleYAni).with(tranY1Ani).before(tranYAni)
    .before(alphaAni)

看看效果。

408x425_boom2.gif

基本上每个拆解的部分都已经实现,现在开始将IM界面、炸弹抛物线、爆炸效果以及最后的炸开效果合在一起。

最后的最后,实现的效果如下:

boom.gif

在炸弹爆炸的瞬间,用户头像和表情都会有被震到抖动的效果,这里就没实现了,有兴趣的小伙伴可以试试。

好了,本篇文章就到这了。觉得还凑合的,可以给个三连哟!

推荐阅读:

玩会儿Compose,原神主题列表
Compose版来啦!仿自如裸眼3D效果

image.png

彩蛋

image.png

前几日成功申请到了掘金的周边,那就在此文下面开启抽奖吧,感谢掘金官方的大力支持。

抽奖方式:

截止到9月10号,如果评论区超过 10 人互动(不含本人),就从热门评论区抽取排名前两位的幸运观众,分别送出“掘金徽章”一枚。(注:给自己刷评论的不计在内)

开奖时间: 9月11号中午12:00

image.png