仿抖音、映客礼物打赏,连击、追加等功能效果实现

1,366 阅读3分钟

在主流的直播软件当中,经常会在直播间看到当有人送出礼物后,会在屏幕的左边看到用户送出的礼物信息,出现的方式是从屏幕左侧的边缘飘进来,且当有多条这样的礼物信息时,同一时间只显示两条礼物信息,显示一定时间后,再显示后面的礼物信息,而在礼物信息的后侧,会伴随着送出的礼物数量,仔细观察的话,会看到有一个放大后缩小的动画效果,刚好最近公司也有这样的类似需求,因此为了防止以后还会有同样的需求,在此做个记录,做个备份,同时也希望能帮到有同样需要的同行

废话不多说,先看视频

video2.gif

代码实现

1、自定义礼物itemView类

GiftView.kt


/**
 * 礼物View
 */
class GiftView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr) {

    private val binding by lazy { ItemGiftViewBinding.inflate(LayoutInflater.from(context)) }

    var giftMessage: GiftMessage? = null
        set(value) {
            field = value
            initView()
        }

    private fun initView() {
        with(binding) {
            val lp = ConstraintLayout.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT
            )
            lp.topMargin = 8
            layoutParams = lp

            addView(binding.root)

            tvFromName.text = giftMessage!!.fromUserName
            tvToName.text = giftMessage!!.toUserName
            // 礼物
            Glide.with(context).load(giftMessage?.giftUrl).error(R.mipmap.live_red_packet)
                .into(ivGiftImg)
            // 头像
            Glide.with(context).load(giftMessage?.fromUserAvatarUrl).error(R.mipmap.img_error_holder_circle)
                .into(fromAvatar)
            giftMessage!!.updateTime = System.currentTimeMillis() //设置时间标记
            giftNumView.tag = 1 //给数量控件设置标记
            tag = giftMessage //设置view标识
            val giftViewInAnim = AnimationUtils.loadAnimation(
                context, R.anim.item_gift_in
            ) as TranslateAnimation
            clContainer.startAnimation(giftViewInAnim) //开始执行显示礼物的动画
            //设置动画监听
            giftViewInAnim.setAnimationListener(object : AnimListenerAdapter() {
                override fun onAnimationEnd(animation: Animation?) {
                    giftNumView.visibility = VISIBLE
                    setGiftNum(giftNumView)
                    startComboAnim(giftNumView) // 设置一开始的连击事件
                }

            })

        }
    }

    /**
     * 连击动画
     *
     * @param giftNumView
     * @param
     */
    fun startComboAnim(giftNumView: TextView) {
        val anim1 = ObjectAnimator.ofFloat(giftNumView, "scaleX", 1.2f, 1.0f)
        val anim2 = ObjectAnimator.ofFloat(giftNumView, "scaleY", 1.2f, 1.0f)
        val animSet = AnimatorSet()
        animSet.duration = 300
        animSet.interpolator = OvershootInterpolator()
        animSet.playTogether(anim1, anim2)
        animSet.start()
        animSet.addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: Animator) {
                (tag as GiftMessage).updateTime = System.currentTimeMillis() //设置时间标记
                giftNumView.tag = giftNumView.tag as Int + 1
                //这里用((GiftMessage)giftView.getTag()) 来实时的获取GiftMessage  便于礼物的追加
                if (giftNumView.tag as Int <= (tag as GiftMessage).giftNum!!) {
                    setGiftNum(giftNumView)
                    startComboAnim(giftNumView)
                } else {
                    (tag as GiftMessage).isComboAnimationOver = true
                }
            }
        })
    }

    companion object {
        private val giftNumMap = ArrayMap<String, Int>().apply {
            put("0", R.drawable.ic_0)
            put("1", R.drawable.ic_1)
            put("2", R.drawable.ic_2)
            put("3", R.drawable.ic_3)
            put("4", R.drawable.ic_4)
            put("5", R.drawable.ic_5)
            put("6", R.drawable.ic_6)
            put("7", R.drawable.ic_7)
            put("8", R.drawable.ic_8)
            put("9", R.drawable.ic_9)
        }

        @JvmStatic
        fun setGiftNum(giftNumView: TextView) {
            val giftNum = giftNumView.tag
            val spanUtils = SpanUtils()
            spanUtils.appendImage(R.drawable.ic_x)
            giftNum.toString().forEach {
                val n = it.toString()
                LogUtils.i("数字:$n")
                giftNumMap[n]?.let { resId ->
                    spanUtils.appendImage(resId)
                }
            }
            giftNumView.text = spanUtils.create()
        }
    }
}

这个类用于显示头像、送礼人的信息、礼物数量等信息

GiftViewManager.kt

/**
 * 礼物管理类
 */
@SuppressLint("StaticFieldLeak")
object GiftViewManager {

    private var mContext: Context? = null

    /**
     * 礼物飞出的动画
     */
    private var mGiftLayoutOutAnim: Animation? = null

    /**
     * 礼物定时器  用于清除和从礼物队列中取礼物
     */
    private var mGiftClearTimer: Timer? = null

    /**
     * 礼物队列
     */
    private val mGiftList = ArrayList<GiftMessage>()

    /**
     * 礼物容器 目前只是小礼物的容器
     */
    private var mGiftViewContainer: LinearLayout? = null

    /**
     * 礼物定时器执行间隔
     */
    private var mGiftClearTimerInterval = 1500L

    /**
     * 礼物无更新后的存在时间
     */
    private var mGiftClearInterval = 3000L

    /**
     * 同时存在的最大礼物数目
     */
    private var mGiftMaxNumber = 2

    /**
     * init 动画 和 context
     *
     * @param context
     */
    fun init(context: Context?, options: Options? = null) {
        mContext = context
        mGiftLayoutOutAnim = AnimationUtils.loadAnimation(context, R.anim.item_gift_out)
        options?.let {
            mGiftClearTimerInterval = it.giftClearTimerInterval
            mGiftClearInterval = it.giftClearInterval
            mGiftMaxNumber = it.giftMaxNumber
        }
    }

    /**
     * 添加礼物container layout
     *
     * @param container
     * @return 添加成功或失败
     */
    fun addGiftContainer(container: LinearLayout): Boolean {
        if (container.orientation == LinearLayout.HORIZONTAL) {
            return false
        }
        mGiftViewContainer = container
        return true
    }

    /**
     * 将动画信息添加到动画队列
     *
     * @param message
     */
    fun addGiftMessage(message: GiftMessage) {
        mGiftList.add(message)
        if ((mGiftClearTimer == null) && (mGiftViewContainer != null) && (mContext != null)) {
            startTimer()
        }
    }

    /**
     * 添加动画view
     */
    private fun createGiftView(message: GiftMessage): View {
        return GiftView((mContext)!!).apply {
            giftMessage = message
        }
    }

    /**
     * 删除view
     */
    private fun removeGiftView(targetView: View) {
        mGiftLayoutOutAnim?.setAnimationListener(object : AnimListenerAdapter() {
            override fun onAnimationEnd(animation: Animation?) {
                mGiftViewContainer?.post { mGiftViewContainer?.removeView(targetView) }
                if ((mGiftList.isEmpty()) && (mGiftViewContainer?.isEmpty() == true)) {
                    mGiftClearTimer?.cancel()
                    mGiftClearTimer = null
                }
            }
        })

        AppExecutors.mainThread {
            targetView.startAnimation(mGiftLayoutOutAnim)
        }
    }

    /**
     * 定时清除礼物
     */
    private fun startTimer() {
        mGiftClearTimer = Timer()
        mGiftClearTimer?.schedule(
            object : TimerTask() {
                override fun run() {
                    // 清除礼物
                    mGiftViewContainer?.forEach { view ->
                        val message = view.tag as GiftMessage
                        val nowTime = System.currentTimeMillis()
                        val upTime = message.updateTime
                        if ((nowTime - upTime) >= mGiftClearInterval) {
                            removeGiftView(view)
                            return
                        }
                    }
                    val count = mGiftViewContainer?.childCount ?: 0
                    // 添加礼物
                    if (count < mGiftMaxNumber && mGiftList.isNotEmpty()) {
                        AppExecutors.mainThread {
                            showGiftView(mGiftList.first())
                            mGiftList.removeAt(0)
                        }
                    }
                }
            }, 0, mGiftClearTimerInterval
        )
    }

    /**
     * 根据message寻找view
     *
     * @param message
     * @return
     */
    private fun findViewByMessage(message: GiftMessage): View? {
        mGiftViewContainer?.forEach {
            val giftMessage = it.tag
            if (giftMessage is GiftMessage) {
                if (giftMessage.uid == message.uid && giftMessage.toUid == message.toUid) {
                    return it
                }
            }
        }
        return null
    }

    /**
     * 显示礼物的方法
     */
    private fun showGiftView(giftMessage: GiftMessage) {
        var giftView = findViewByMessage(giftMessage)
        if (giftView == null) { //该用户不在礼物显示列表 或者又送了一个新的礼物
            giftView = createGiftView(giftMessage)
            mGiftViewContainer?.addView(giftView) /*将礼物的View添加到礼物的ViewGroup中*/
            mGiftViewContainer?.invalidate()
        } else {
            //该用户在礼物显示列表  1. 连击动画还未结束,只更新message即可
            val message = giftView.tag as? GiftMessage // 原来的礼物view的信息
            message?.apply {
                giftNum = (giftNum ?: 0) + ((giftMessage.giftNum) ?: 0) // 合并追送的礼物数量
                giftView.tag = this
                if (message.isComboAnimationOver) {
                    // 2.连击动画已完成 此时view 未消失,除了1 的操作外,还需重新启动连击动画
                    val giftNumTextView = giftView.findViewById<TextView>(R.id.giftNumView)
                    setGiftNum(giftNumTextView)
                    (giftView as GiftView).startComboAnim(giftNumTextView)
                }
            }

        }
    }

    /**
     * 释放资源,必须调用。
     */
    fun release() {
        mGiftClearTimer?.cancel()
        mGiftClearTimer = null

        mGiftList.clear()
        mGiftViewContainer?.removeAllViews()
        mGiftLayoutOutAnim = null
        mContext = null
    }
}

这个类主要用于实现礼物的进入、删除等功能,以上两个类是主要的实现类,其他代码就不贴出来了,Demo我已经上传至coding,有需要的可以下载。

Demo地址:widget.coding.net/p/live/d/li…