Android自定义滚动弹幕

1,前言

应公司开发需要,需要做一个类似视频弹幕的UI效果,故手起刀落,那就自己琢磨一下写一个吧, 其核心功能就是使用属性动画从屏幕右边平移到屏幕左边,通过缓存已经滑动到屏幕左边之外的item来实现复用item,优化内存消耗以及UI卡顿,使用LifecycleEventObserver来监听宿主的生命周期来实现暂停滚动,恢复滚动,销毁数据,完整代码如下

/**
 * create by lijianhui
 * on
 * <p>
 * description: 滚动弹幕
 */
class ScrollSubtitleView : FrameLayout, LifecycleEventObserver {
    private val mSubtitleInfoList = ArrayList<SubtitleInfo>()
    private val mCacheViewList = ArrayList<ItemSubtitleBinding>()
    private val mAnimatorMap = HashMap<ItemSubtitleBinding, ObjectAnimator>()
    private var mLeftMargin = 30
    private var mLines = 5
    private var mScreenWidth = 0
    private var mCurrentPosition = 0
    private var mSpeed = 0.1f
    private var mSubtitleClickCallback: WeakReference<SubtitleItemClickCallback>? = null

    constructor(context: Context) : this(context, null)

    constructor(context: Context, attributeSet: AttributeSet?) : this(context, attributeSet, 0, 0)

    constructor(
        context: Context,
        attributeSet: AttributeSet?,
        defStyleAttr: Int,
        defStyleRes: Int
    ) : super(context, attributeSet, defStyleAttr, defStyleRes) {
        mScreenWidth = context.resources.displayMetrics.widthPixels
    }

    /**
     * 设置数据
     */
    fun setData(subtitleInfoList: ArrayList<SubtitleInfo>) {
        mSubtitleInfoList.clear()
        mSubtitleInfoList.addAll(subtitleInfoList)
        initData()
    }

    /**
     * 追加数据
     */
    fun addData(subtitleInfoList: ArrayList<SubtitleInfo>) {
        mSubtitleInfoList.addAll(subtitleInfoList)
    }

    /**
     * 初始化数据
     */
    private fun initData() {
        if (mSubtitleInfoList.isEmpty()) {
            return
        }

        for (line in 0 until mLines) {
            initView(line)
        }
    }

    /**
     * 初始化并启动滚动的弹幕
     */
    private fun initView(line: Int) {
        //初始化布局
        val itemSubtitleBinding: ItemSubtitleBinding
        if (mCacheViewList.isNotEmpty()) {
            itemSubtitleBinding = mCacheViewList[0]
            mCacheViewList.removeAt(0)
        } else {
            itemSubtitleBinding = ItemSubtitleBinding.inflate(LayoutInflater.from(context))
            itemSubtitleBinding.root.setOnClickListener {
                run {
                    mSubtitleClickCallback?.apply {
                        get()?.clickSubTitleItem(itemSubtitleBinding.subtitle)
                    }
                }
            }
        }

        //索引归位
        if (mCurrentPosition >= mSubtitleInfoList.size) {
            mCurrentPosition = 0
        }

        //绑定数据
        itemSubtitleBinding.subtitle = mSubtitleInfoList[mCurrentPosition]
        itemSubtitleBinding.tvContent.text = mSubtitleInfoList[mCurrentPosition].message
        itemSubtitleBinding.root.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)

        //初始化宽高 边距
        val measuredHeight = itemSubtitleBinding.root.measuredHeight
        val measuredWidth = itemSubtitleBinding.root.measuredWidth
        val layoutParams = LayoutParams(measuredWidth, measuredHeight)
        layoutParams.leftMargin = mScreenWidth
        layoutParams.topMargin = measuredHeight * line
        itemSubtitleBinding.root.layoutParams = layoutParams
        addView(itemSubtitleBinding.root)

        //初始化动画
        val totalScrollX = mScreenWidth + measuredWidth
        val animator = ObjectAnimator.ofFloat(
            itemSubtitleBinding.root,
            "translationX",
            0f,
            -totalScrollX.toFloat()
        )
        mAnimatorMap[itemSubtitleBinding] = animator

        //开启动画
        val duration = abs(totalScrollX / mSpeed)
        animator.duration = duration.toLong()
        animator.interpolator = LinearInterpolator()
        animator.start()

        //监听动画滑动距离
        val animatorUpdateListener = ValueAnimator.AnimatorUpdateListener { animator ->
            var animatedValue: Float = abs(animator.animatedValue as Float)
            if (animatedValue >= (measuredWidth + mLeftMargin)) {
                animator.removeAllUpdateListeners()
                initView(line)
            }
        }
        animator.addUpdateListener(animatorUpdateListener)

        //监听动画执行完毕
        animator.addListener(onEnd = {
            mCacheViewList.add(itemSubtitleBinding)
            mAnimatorMap.remove(itemSubtitleBinding)
            removeView(itemSubtitleBinding.root)
        })

        //索引自增
        mCurrentPosition++
    }

    /**
     * 设置弹幕之间的边距
     */
    fun setLeftMargin(leftMargin: Int) {
        this.mLeftMargin = leftMargin
    }

    /**
     * 设置弹幕行数
     */
    fun setLines(lines: Int) {
        this.mLines = lines
    }

    /**
     * 设置宽度
     */
    fun setScreenWidth(width: Int) {
        this.mScreenWidth = width
    }

    /**
     * 设置滚动速度
     */
    fun setScrollSpeed(speed: Float) {
        this.mSpeed = speed
    }

    /**
     * 设置滚动弹幕点击回调
     */
    fun setItemClickCallback(subtitleItemClickCallback: SubtitleItemClickCallback) {
        this.mSubtitleClickCallback = WeakReference(subtitleItemClickCallback)
    }

    /**
     * 生命周期回调
     */
    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        when (event) {
            Lifecycle.Event.ON_PAUSE -> stopScroll()
            Lifecycle.Event.ON_RESUME -> startScroll()
            Lifecycle.Event.ON_DESTROY -> destroy()
        }
    }

    /**
     * 停止滚动
     */
    private fun stopScroll() {
        if (mAnimatorMap.isNotEmpty()) {
            for (animator in mAnimatorMap) {
                animator.value.pause()
            }
        }
    }

    /**
     * 恢复滚动
     */
    private fun startScroll() {
        if (mAnimatorMap.isNotEmpty()) {
            for (animator in mAnimatorMap) {
                animator.value.resume()
            }
        }
    }

    /**
     * 停止动画 销毁弹幕
     */
    private fun destroy() {
        if (mAnimatorMap.isNotEmpty()) {
            for (animator in mAnimatorMap) {
                animator.value.cancel()
            }
        }

        removeAllViews()
        mCacheViewList.clear()
        mSubtitleInfoList.clear()
    }

    /**
     * 点击回调接口
     */
    interface SubtitleItemClickCallback {
        fun clickSubTitleItem(subtitleInfo: SubtitleInfo?)
    }
}

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值