通用弹幕实现

本文介绍了Android弹幕系统的设计与实现,包括IBarrageView、BarrageView及AbsBarrageController的职责。AbsBarrageController维护等待、准备和运行的弹幕队列,使用计时器来调度弹幕的播放。通过onPlayBarrage方法处理弹幕状态转换,以及在画布上的绘制和触摸事件。此外,还详细说明了如何实现随机线弹幕控制器RandomLineBarrageController,以及弹幕对象AbsBarrage的状态管理和生命周期。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

IBarrageView

interface IBarrageView {

    // 弹幕是否可点击
    fun setEnableTouch(enable: Boolean)

    // 添加弹幕控制器
    fun addController(controller: AbsBarrageController, priority: Int = 0)

    // 移除弹幕控制器
    fun removeController(controller: AbsBarrageController)

    fun clear()
}

BarrageView

  • 内部维护 List<BarrageController>
  • 重写 onDraw、onTouchEvent 和 onDetachedFromWindow,进而调用 BarrageController 对应方法
class BarrageView {

    override fun onDraw(canvas: Canvas) {
        if (controllerList.size == 0) {
            canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
        }
        loopWithIndex(controllerList.size) {
            controllerList[it].apply {
                draw(canvas)
            }
            false
        }
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        if (mEnableTouch) {
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    mTouchController = null
                    for (barrageController in controllerList) {
                        if (barrageController.onTouchEvent(event)) {
                            mTouchController = barrageController
                            return true
                        }
                    }
                }
                MotionEvent.ACTION_UP -> {
                    mTouchController?.let {
                        return it.onTouchEvent(event)
                    }
                }
                MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_MOVE -> {
                    mTouchController?.let {
                        return it.onTouchEvent(event)
                    }
                }
            }
            return super.onTouchEvent(event)
        } else {
            return super.onTouchEvent(event)
        }
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow() // 清理 Controller,防止空指针
        controllerList.forEach {
            it.release()
        }
        controllerList.clear()
    }
}

AbsBarrageController

内部维护三个队列,分别为 waitingList、preparingList、runningList

内部维护计时器,通过 AnimationUpdateListener 计算时间差,并调用 onPlayBarrage 抽象方法,如果三个队列都为空,则停止计时器

abstract fun onPlayBarrage(runningList: MutableList<AbsBarrage>, deltaTime: Float)

@MainThread
private fun initAnimation(): ValueAnimator {
    val animator = ValueAnimator.ofFloat(0F, AbsBarrageController.ANIMATOR_DURATION.toFloat())
    animator.interpolator = LinearInterpolator()
    animator.duration = AbsBarrageController.ANIMATOR_DURATION.toLong()
    animator.repeatMode = ValueAnimator.RESTART
    animator.repeatCount = ValueAnimator.INFINITE
    animator.addUpdateListener { animation ->
        val oldAnimatorValue = oldAnimatorValue
        val animatorValue = animation.animatedValue as Float
        this.oldAnimatorValue = animatorValue
        var deltaTime = animatorValue - oldAnimatorValue
        if (animatorValue < oldAnimatorValue) {
            deltaTime = AbsBarrageController.ANIMATOR_DURATION - oldAnimatorValue + animatorValue
        }
        onPlayBarrage(runningList, deltaTime)
        if (waitingList.isEmpty() && preparingList.isEmpty() && runningList.isEmpty()) {
            stop()
        }
        barrageView.invalidate()
    }
    return animator
}

另外,Controller 也提供了 resume、pause 方法,用于在 UI 状态改变时启动/停止计时器

@MainThread
fun start() {
    checkMainThread()
    if (valueAnimator.isPaused) {
        valueAnimator.resume()
    }
}

@MainThread
fun stop() {
    checkMainThread()
    if (!valueAnimator.isPaused && valueAnimator.isRunning) {
        valueAnimator.pause()
    }
}

添加弹幕接口,在该方法中首先会判断是否启动计时器,接着会判断是否需要将 waitingList 中的 Barrage 升级为 prepare 状态

fun addBarrage(barrage: AbsBarrage, addToFront: Boolean = false) {
    checkMainThread()
    if (addToFront) {
        waitingList.addFirst(barrage)
    } else {
        waitingList.add(barrage)
    }
    barrage.state = AbsBarrage.State.ADDED
    start()
}

@MainThread
private fun start() {
    checkMainThread()
    if (!valueAnimator.isRunning) {
        valueAnimator.start()
    }
    notifyPrepare()
}

private fun notifyPrepare() {
    while (preparingList.size <= AbsBarrageController.PRE_BUILD_COUNT && waitingList.size != 0) {
        val barrage = waitingList.removeFirst()
        if (barrage.isHighPriority) {
            preparingList.addFirst(barrage)
        } else {
            preparingList.add(barrage)
        }
        barrage.state = AbsBarrage.State.PREPARING
    }
}

onPlayBarrage 的实现方法会维护的各个 Barrage 位置,需要动态获取 preparingList 中的 Barrage,并添加到 runningList 中,此时 AbsBarrageController 会判断是否将 waitingList 中的 Barrage 升级为 prepare 状态。同时 onPlayBarrage 的实现方法也需要负责 running 状态的 Barrage 从 runningList 中移除

@MainThread
protected fun nextPreparedBarrage(): AbsBarrage? {
    val iterator = preparingList.iterator()
    while (iterator.hasNext()) {
        val barrage = iterator.next()
        if (barrage.readyToDraw) {
            iterator.remove()
            notifyPrepare()
            return barrage
        }
    }
    return null
}

另外,AbsBarrageController 提供了 draw 和 onEventTouch 方法,方法内部会调用 runningList 的对应方法

@MainThread
internal fun draw(canvas: Canvas) {
    loopWithIndex(runningList.size) {
        runningList[it].draw(canvas)
        false
    }
}

RandomLineBarrageController

在 AbsBarrageController具体实现类中,我们需要实现 onPlayBarrage 方法,并通过 runningList 中的 Barrage 实时位置,决定是否添加新的 Barrage,并设置新添加 Barrage 的初始位置,或者 remove 掉 running 的 Barrage

class RandomLineBarrageController(
    private val firstLinePaddingTop: Int,
    private val minIntervalDistance: Int,
    private val lineHeight: Int,
    private val barrageView: BarrageView,
    private val barrageLines: Int,
    private val minIntervalTime: Int
) {

    override fun onPlayBarrage(runningList: MutableList<AbsBarrage>, deltaTime: Float) {
        ...
        // 删除 running barrage
        if (barrage.rectF.right < 0) {
            iterator.remove()
        }
        ...
        // 添加 running barrage,并设置初始位置
        val absBarrage = nextPreparedBarrage() ?: return@loopWithIndex false
        val width = absBarrage.rectF.width()
        val height = absBarrage.rectF.height()
        absBarrage.rectF.apply {
            left = canvasWidth.toFloat()
            right = canvasWidth + width
            top = (i * lineHeight + firstLinePaddingTop).toFloat()
            bottom = i * lineHeight + firstLinePaddingTop + height
        }
    }
}

AbsBarrage

AbsBarrage 内部定义了 State 信息 和一些关键字段,其中,readyToDraw 用于标识当前 Barrage 是否已经准备好,rectF 用于记录 Barrage 位置信息

其中,子类主要需要实现 State 状态转移回调和 handleProgress 方法

abstract fun handleProgress(progress: Float, parentWidth: Int)

var readyToDraw = false

val rectF = RectF(0F, 0F, 0F, 0F)

@Volatile
var state: AbsBarrage.State = AbsBarrage.State.CREATED
    set(state) {
        val prev = field
        field = state
        moveToState(prev, state)
    }

private fun moveToState(prev: AbsBarrage.State, state: AbsBarrage.State) {
    if (prev == state) return
    check(prev < state) { "Wrong state! Prev: $prev, Target: $state" }
    when (state) {
        AbsBarrage.State.CREATED -> throw IllegalStateException("Shouldn't set state to CREATED.")
        AbsBarrage.State.ADDED -> onAdd()
        AbsBarrage.State.PREPARING -> onPrepare()
        AbsBarrage.State.VISIBLE -> onShow()
        AbsBarrage.State.HIDDEN -> onHide()
    }
}

open fun onAdd() { }

open fun onPrepare() { }

open fun onShow() { }

open fun onHide() { }

open fun draw(canvas: Canvas) {
    val bm = bitmap ?: return
    if (state != AbsBarrage.State.VISIBLE || bm.isRecycled) {
        return
    }
    canvas.drawBitmap(
            bm,
            rectF.left + config.paddingHorizontal,
            rectF.top + config.paddingVertical,
            paint
    )
}

ViewWrapperBarrage

实现 State 生命周期回调,生成并保存 Bitmap 信息,在 State.HIDDEN 时回收 Bitmap

override fun onHide() {
    _bitmap?.let { config.bitmapProvider.recycle(it) }
}

private var _bitmap: Bitmap? = null
override val bitmap: Bitmap? get() = _bitmap

private val bitmapListener: BitmapListener = { bitmap: Bitmap, width: Int, height: Int ->
    if (state >= AbsBarrage.State.HIDDEN) {
        config.bitmapProvider.recycle(bitmap)
    } else {
        _bitmap = bitmap
        setSize(width.toFloat(), height.toFloat())
        readyToDraw = true
    }
}

override fun onPrepare() {
    if (view == null) {
        view = mProvideView(this)
    }
    view?.let {
        updateView(it)
    }
}

private fun updateView(view: View) {
    if (_bitmap != null) return
    this.view = view
    if (state == AbsBarrage.State.PREPARING || state == AbsBarrage.State.VISIBLE) {
        doConvertViewToBitmap(
                view,
                config.bitmapProvider,
                config.workingExecutor,
                bitmapListener
        )
    }
}

protected open fun doConvertViewToBitmap(
        view: View,
        bitmapProvider: BitmapProvider,
        executor: Executor,
        bitmapListener: BitmapListener
) {
    convertViewToBitmapLocked(view, bitmapProvider, executor, bitmapListener)
}

AccelerateBarrage

  • 传递需要渲染的 View 信息
  • 实现了 handleProgress 方法,更新 rectF 位置

Utils

View 转换为 Bitmap 时,需要在线程中执行,并且需要加锁

internal inline fun convertViewToBitmapLocked(
        view: View,
        bitmapProvider: BitmapProvider,
        executor: Executor,
        crossinline listener: BitmapListener) {
    executor.execute {
        runLocking(convertBitmapLock) {
            internalConvertViewToBitmap(view, bitmapProvider, listener)
        }
    }
}

手动调用 View.measure 和 View.layout,且需要所有子 View 调用 forceLayout

获取到 Bitmap 后,继续调用 View.draw 方法

private inline fun internalConvertViewToBitmap(
        view: View,
        bitmapProvider: BitmapProvider,
        crossinline listener: BitmapListener) {
    view.rForceLayout()
    view.measure(
            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
    )
    val width = view.measuredWidth
    val height = view.measuredHeight
    view.layout(0, 0, width, height)
    runCatching {
        bitmapProvider.obtain(
                width,
                height,
                if (view.isOpaque) Bitmap.Config.RGB_565 else Bitmap.Config.ARGB_8888
        ).also { view.draw(Canvas(it)) }
    }.getOrNull()?.let { bitmap ->
        mainThreadHandler.post { listener(bitmap, width, height) }
    }
}

private fun View.rForceLayout() {
    forceLayout()
    if (this is ViewGroup) {
        for (i in 0 until childCount) {
            getChildAt(i).rForceLayout()
        }
    }
}

BitmapProvider

通过 TreeSet 保存 Bitmap,内部会根据 Bitmap 大小进行排序,在获取 Bitmap 时,会获取最适合的大小

BitmapProvider 内部维护了一个定时器,每 3 秒中清除一张最大的缓存图

class CachedBitmapProvider @JvmOverloads constructor(
        private val countLimit: Int = 3,
        private val evictInterval: Long = 3000L
) : BitmapProvider {

    private val pool: TreeSet<Bitmap>
        get() = TreeSet { o1, o2 ->
            if (o1 == o2) {
                0
            } else {
                val result = o1.byteCount - o2.byteCount
                if (result == 0) {
                    -1
                } else {
                    result
                }
            }
        }

    @AnyThread
    override fun obtain(w: Int, h: Int, config: Bitmap.Config): Bitmap {
        return runLocking(lock) {
            obtainSmallestAvailable(w, h, config) ?: Bitmap.createBitmap(w, h, config)
        }
    }

    @AnyThread
    override fun recycle(bitmap: Bitmap) {
        runLocking(lock) {
            pool.add(bitmap)
            if (pool.size > countLimit) {
                pool.iterator().apply {
                    next().recycle()
                    remove()
                }
            }
            delayedEvict()
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

little-sparrow

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值