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