弹幕刷屏之术——Android无时间线弹幕实现
标签(空格分隔): Android
作者:陈小默
弹幕预览今天我们来实现一种普通的弹幕,这种弹幕不是用在视频上的但是稍加修改也可以增加时间线的。
使用方式
1,首先我们先创建一个用于默认显示的佩恩语录数组
private val mMessageList = arrayOf(
"他们的痛苦使我成长",
"我已经在无限存在的痛苦之中",
"这就是神的使命",
"要让世界成长就要让他知道什么叫痛苦",
"感受痛苦吧",
"体验痛苦吧",
"接受痛苦吧",
"了解痛苦吧",
"你为了你的正义,我为了我的正义",
"人类是无论如何都不会互相理解的愚蠢生物",
"如果和平真的存在,那么就让我来实现它",
"是神的命令!要杀了你!",
"因为这就是我的“正义”")
2,创建一个随机的头像数组
private val mAvatarList = intArrayOf(
R.drawable.avatar_kakashi_1,
R.drawable.avatar_konan_1,
R.drawable.avatar_madara_1,
R.drawable.avatar_minato_1,
R.drawable.avatar_naruto_1,
R.drawable.avatar_sakura_1,
R.drawable.avatar_sasuke_1
)
3,创建一个随机颜色的数组
private val mTextColor = intArrayOf(
Color.parseColor("#3366ff"),
Color.WHITE,
Color.parseColor("#66ff99"),
Color.parseColor("#ff9966"))
4,在onCreate方法中启动弹幕并添加数据
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_barrage_list)
mBarrageLayout.onCreate() //在OnCreate中(或其他合适位置)启动弹幕
val initBarrageItems = mMessageList.map {
object : AbsBarrageItem() {
override fun getText(): String = it
override fun getBackgroundResource(): Int = R.drawable.shape_radius_alpha_black
override fun avatarEnable(): Boolean = true
override fun inflateAvatar(avatar: ImageView) = avatar.inflateCircle(R.drawable.avatar_pain_1)
}
}.toList()
mBarrageLayout.addItems(initBarrageItems) //在弹幕启动前后都可以设置数据
}
在这里我们将语录数据通过map
转换为了能被弹幕接收的一组BarrageItem
链表对象。
5,在Activity销毁时调用弹幕销毁
override fun onDestroy() {
super.onDestroy()
mBarrageLayout.onDestroy()
}
6,当然,少不了互动。对于通过addItem
函数添加的弹幕都会在下一个有空的位置显示出来。
/**
* 发送按钮被点击
*/
private fun onSendClick(view: View?) {
val message = mBarrageEdit.text.toString()
if (message.isNotBlank()) {
mBarrageEdit.setText("")
val item = object : AbsBarrageItem() {
override fun getText(): String = message
override fun getBackgroundResource(): Int = R.drawable.shape_radius_alpha_black
override fun getTextColor(): Int = mTextColor[(Math.random() * mTextColor.size).toInt()]
override fun avatarEnable(): Boolean = true
override fun inflateAvatar(avatar: ImageView) = avatar.inflateCircle(mAvatarList[(Math.random() * mAvatarList.size).toInt()])
}
mBarrageLayout.addItem(item)
}
}
自己发送的弹幕的颜色和头像被设置为了随机显示。
下面是最终结果:
带有头像的弹幕预览实现过程
如果不关心实现过程,只需要拿来就能用的话,可以参看github-MyDemo-BarrageLayout.kt使用方式就可以参照BarrageListActivity
弹幕的实现过程中,最重要的不是让View从一侧移动到另一侧,使用Animation就可以实现,而是防止弹幕的重叠。
1,定义弹幕接口
/**
* 弹幕Item接口
*/
interface BarrageItem {
fun getTextColor(): Int
fun getBackgroundColor(): Int
fun getBackgroundResource(): Int
fun avatarEnable(): Boolean
fun inflateAvatar(avatar: ImageView)
fun onClick()
fun getText(): String
}
接口中的这几个方法分别时用来定义字体颜色,弹幕背景颜色(资源)、是否显示头像以及处理弹幕的点击事件。
由于发送弹幕除了弹幕显示什么字外,其他的都不是必要属性,都可以用默认值代替。
于是我们定义一个抽象类,覆盖除了getText
之外的方法
/**
* 弹幕Item的基本实现
*/
abstract class AbsBarrageItem : BarrageItem {
override fun getTextColor() = Color.WHITE
override fun getBackgroundColor() = Color.TRANSPARENT
override fun getBackgroundResource(): Int = -1
override fun avatarEnable() = false
override fun inflateAvatar(avatar: ImageView) {}
override fun onClick() {}
}
2,实现动画
private fun startAnimation(view: View, start: Float, barrageWidth: Float) {
val animator = ObjectAnimator.ofFloat(view, "translationX", start, -barrageWidth).setDuration(speed)
animator.setInterpolator { it }
animator.addListener(object : Animator.AnimatorListener {
override fun onAnimationRepeat(animation: Animator?) {}
override fun onAnimationCancel(animation: Animator?) {}
override fun onAnimationStart(animation: Animator?) {}
override fun onAnimationEnd(animation: Animator?) {
removeView(view)
}
})
animator.start()
}
因为需要处理点击事件,所以这里必须使用属性动画而不是View动画。View从start
位置起开始运动,并最终停止在-barrageWidth
位置处。
注意:当动画完毕时,我们需要将这个弹幕从当前布局中移除。但是这里存在一个问题,如果弹幕发射过快,就存在着View的频繁创建和销毁的过程,此时如果去观察内存曲线的话,是可以看到内存抖动的。其实最好的实现方式就是使用
ConvertView
机制,自己去管理一个View缓冲池,然后通过Adapter去添加弹幕。如果有需要使用者可以自行修改实现(本人太懒,写完后发现有问题,但是懒得改)。
3,使用Rx进行中断控制
我们发送弹幕是有时间间隔的,在这里我们通过Rxjava的interval
操作符实现。
每次中断发生时,我们需要判断两件事,第一件就是当前是否有空闲位置可以显示弹幕。第二件就是有没有弹幕需要显示:
map {
val pos = getIndex()
var barrage: BarrageIndex? = null
if (pos != -1) {
var item: BarrageItem? = null
if (mTempItems.isNotEmpty()) { //有刚刚发送的消息
item = mTempItems.remove()
} else if (items.isNotEmpty()) {
item = items.remove()
} else { //一轮数据发射完毕
val max = mVPosition.max() ?: 0 + 10//两轮数据之中间隔10个中断
for (i in mVPosition.indices) mVPosition[i] = max
items.addAll(mItems)
}
if (item != null) {
mVPosition[pos] = Int.MAX_VALUE
barrage = BarrageIndex(item, pos)
}
}
barrage
}
函数getIndex
的作用就是查找有空闲行数的位置,这个保存空闲行数的数据结构就是一个int类型的数组
private val mVPosition by lazy { IntArray(maxLine) { 0 } }
当其中的值小于等于0的时候就说明该行空闲
private fun getIndex(): Int {
for (i in mVPosition.indices) mVPosition[i]--
return mVPosition.indexOfFirst { it <= 0 }
}
如果有空行,我们就需要取出下一条数据来显示了。在具体的处理之前,我们需要将当前位置的记录更新为最大值,防止在该数据还未处理完时下一次中断又进行导致该行可能会被分配多个数据。
if (item != null) {
mVPosition[pos] = Int.MAX_VALUE
barrage = BarrageIndex(item, pos)
}
4,显示弹幕
{ res ->
if (isStart && res != null) {
val barrageView = generateBarrageView(res.item)
val lot = computeLot(barrageView.mItemText, res.item.getText())
mVPosition[res.pos] = lot
showBarrageItem(barrageView, res.pos)
}
}
generateBarrageView
方法的作用是根据item对象的信息生成一条弹幕View,computeLot
方法的作用是计算当前Item的长度占用了几个中断周期。最后将他们显示出来即可。
private fun computeLot(textView: TextView, text: String): Int {
val length = Math.min(textView.paint.measureText(text) + dip(40), width.toFloat())
return ((length * speed) / (2 * width * rate)).toInt() + 1
}
计算过程为当前弹幕长度lenght
除以一个中断所走过的路程distance
,distance
等于弹幕的移动速度s
处于一个中断的周期长度rate
,s
等于弹幕移动的距离2*width
除以弹幕在屏幕上停留的时间长度。总结下来就是上面computeLot
函数的实现。
网友评论