Mobile Development 10 min read

Implementing a Simple Danmu (Bullet Screen) Effect Using RecyclerView and StaggeredGridLayoutManager in Android

The article demonstrates how to create a lightweight, infinite‑scrolling danmu (bullet‑screen) effect in Android by embedding a RecyclerView inside a custom DanMuView, using StaggeredGridLayoutManager for multi‑row layout and a scrollBy‑based Runnable to continuously scroll and control speed.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Implementing a Simple Danmu (Bullet Screen) Effect Using RecyclerView and StaggeredGridLayoutManager in Android

In Android project development, a bullet screen (danmu) is a popular effect that can dynamically display large amounts of text such as comments or messages. Existing third‑party danmu libraries are often too heavyweight for simple needs, so this article demonstrates a lightweight solution using RecyclerView and StaggeredGridLayoutManager to achieve infinite scrolling and multi‑line display.

Core Requirements

Infinite scrolling: ensure the danmu continuously scrolls without restarting at the end.

Multi‑line display: show several rows of danmu simultaneously, each scrolling independently.

Speed control: adjust the scrolling speed for smooth visual effect.

Implementation Overview

Key Technologies

RecyclerView : used to display the list of danmu items.

StaggeredGridLayoutManager : provides a multi‑row layout manager.

View.scrollBy : implements the infinite scrolling animation.

DanMuView Implementation

A custom view DanMuView extends ConstraintLayout . It creates a RecyclerView inside, configures a StaggeredGridLayoutManager with a configurable number of rows, and uses a Runnable that repeatedly calls scrollBy(5, 0) to move the content. Lifecycle observers start and stop the scrolling automatically.

/**
 * 弹幕View
 */
class DanMuView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {

    companion object {
        private const val INTERVAL = 14L
    }

    private var mRow: Int = 1 //弹幕行数
    private var rvDanMu: RecyclerView? = null
    private val slideRunnable = object : Runnable {
        override fun run() {
            rvDanMu?.let {
                it.scrollBy(5, 0) // 这里控制滚动速度
                it.postDelayed(this, INTERVAL)
            }
        }
    }

    init {
        LayoutInflater.from(context).inflate(R.layout.layout_danmu_rv, this)
        rvDanMu = findViewById(R.id.rv_dan_mu)
        if (context is ComponentActivity) {
            context.lifecycle.addObserver(object : DefaultLifecycleObserver {
                override fun onResume(owner: LifecycleOwner) { startPlay() }
                override fun onPause(owner: LifecycleOwner) { stopPlay() }
            })
        }
    }

    /**
     * @param row 弹幕行数
     */
    fun setRow(row: Int) { this.mRow = row }

    /**
     * @param contentList 数据
     * @param row 设置成几行
     */
    @SuppressLint("ClickableViewAccessibility")
    fun setModels(contentList: List
, startFromEnd: Boolean = true) {
        if (contentList.isEmpty()) return
        val viewAdapter = BarrageAdapter(contentList, mRow, startFromEnd)
        rvDanMu?.run {
            layoutManager = StaggeredGridLayoutManager(mRow, StaggeredGridLayoutManager.HORIZONTAL)
            adapter = viewAdapter
            //屏蔽滑动
            setOnTouchListener { _, _ -> true }
        }
    }

    /** 停止轮播 */
    fun stopPlay() {
        removeCallbacks(slideRunnable)
        visibility = View.VISIBLE
    }

    /** 开始轮播 */
    fun startPlay() {
        removeCallbacks(slideRunnable)
        postDelayed(slideRunnable, INTERVAL)
        visibility = View.VISIBLE
    }

    class BarrageAdapter(
        private val dataList: List
,
        private val row: Int,
        private val startFromEnd: Boolean
    ) : RecyclerView.Adapter
() {

        class ViewDataHolder(view: View) : RecyclerView.ViewHolder(view) {
            val textView: TextView = view.findViewById(R.id.tvText)
        }

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewDataHolder {
            return ViewDataHolder(
                LayoutInflater.from(parent.context).inflate(R.layout.layout_danmu_1, parent, false)
            )
        }

        override fun onBindViewHolder(holder: ViewDataHolder, position: Int) {
            if (dataList.isEmpty()) return
            holder.textView.run {
                val params = layoutParams
                if (startFromEnd) {
                    if (position < row) {
                        // 首屏空白,最大展示3行,如需展示更多行,可自行扩展
                        val screenWidth = DpUtil.getScreenSizeWidth(context)
                        when (position) {
                            1 -> params.width = screenWidth + 30.dp2px()
                            2 -> params.width = screenWidth + 10.dp2px()
                            else -> params.width = ViewGroup.LayoutParams.MATCH_PARENT
                        }
                        visibility = View.INVISIBLE
                    } else {
                        // 弹幕从第二屏开始显示
                        val realIndex = if (position - row > 0) position - row else 0
                        val textStr = dataList[realIndex % dataList.size]
                        params.width = ViewGroup.LayoutParams.WRAP_CONTENT
                        visibility = if (textStr.isNotEmpty()) View.VISIBLE else View.GONE
                        text = textStr // 无限循环
                    }
                    layoutParams = params
                } else {
                    // 弹幕从第一屏开始滑动
                    val textStr = dataList[position % dataList.size]
                    visibility = if (textStr.isNotEmpty()) View.VISIBLE else View.GONE
                    text = textStr // 无限循环
                }
            }
        }

        override fun getItemCount(): Int = Int.MAX_VALUE // 无限循环
    }
}

XML Layout Files

RecyclerView container:

<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/rv_dan_mu"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/transparent" />

Item layout for a single danmu text:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/tvText"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginEnd="40dp"
    android:layout_marginBottom="8dp"
    android:background="@drawable/item_danmu_bg"
    android:gravity="center"
    android:textColor="@color/white"
    android:textSize="12sp"
    tools:text="我是一个大弹幕" />

DanMu Activity

The DanMuAnimActivity demonstrates how to instantiate the view, feed it with sample data, and control playback.

/**
 * 弹幕Activity
 */
class DanMuAnimActivity : BaseActivity() {

    private val mDanMuView: DanMuView by id(R.id.danMuView)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_dan_mu_anim)
        initData()
    }

    private fun initData() {
        val danMuList = ArrayList
()
        for (i in 0..50) {
            danMuList.add("我是一个大大的弹幕 $i")
        }
        mDanMuView.setRow(3) // 设置行数
        mDanMuView.setModels(danMuList) // 设置数据
    }

    fun startDanMu(view: View) { mDanMuView.startPlay() }
    fun stopDanMu(view: View) { mDanMuView.stopPlay() }
}

The corresponding activity layout includes the custom DanMuView and two buttons to start and stop the animation.

Conclusion

This tutorial shows that by combining RecyclerView , StaggeredGridLayoutManager , and scrollBy , developers can implement a simple, lightweight danmu effect suitable for scenarios such as product comment streams. For more complex requirements, a dedicated third‑party library may be needed.

UIandroidKotlinRecyclerViewBulletScreendanmuStaggeredGridLayoutManager
Sohu Tech Products
Written by

Sohu Tech Products

A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.