自定义View实战《弹幕》

前言

前面已经学习了自定义的View《渐变色的文字》是继承View的。现在我们在继承ViewGroup来实现我们的《弹幕》View。
本文分为三部分

1、步骤讲解和分步实战
2、缓存优化
3、三方弹幕框架DanmakuFlameMaster
4、第一步初步实现的完整代码

继承View和ViewGroup实现的View有什么明显的区别吗?
一句话打通你任督二脉:
自定义View主要实现的是onMeasure和onDraw
自定义ViewGroup主要实现的是onMeasure和onLayout

好的现在让我们来实现我们的《弹幕》吧。

一、步骤讲解

实现弹幕听起来复杂其实很简单,不信的话往下看。
步骤:
1、初始化弹幕子View

  • 可以自定义弹幕子View(我这里就先用TextView代替了)
  • 添加方法可以是单个和多个
fun addBarrage(data: BarrageItem){
    
     //BarrageItem自定义的View的数据实体
    //1、初始化弹幕子View
    val childView : TextView = TextView(context).apply {
    
    
    text = data.text
    textSize = data.textSize
    setTextColor(ContextCompat.getColor(context, R.color.white))
}

fun addBarrageList(dataList : MutableList<BarrageItem>){
    
    
    this.barrageList = dataList
    dataList.forEach {
    
    
        addBarrage(it)
    }
}

2、测量子View

  • 测量的那肯定就是onMeasure了,前面《View的绘制流程》中说过子onMeasure的两个参数widthMeasureSpec和heightMeasureSpec是父View给他的测量信息。
  • 这里我弹幕是整个View大小的,XML中用了match_parent,所以就不用写他的onMeasue方法了
//2、测量弹幕子View,也就是measure呗
childView.measure(measuredWidth, measuredHeight)

3、向ViewGroup中添加子View

//3、添加弹幕子View
addView(childView)

4、测量完事了,也添加完了,那就该摆放位置了

  • 摆放位置不就是onLayout吗。
//4、设置弹幕子View的布局,也就是layout呗
//这里我想让他从右面到左面移动,上下的位置是随机的
val left = measuredWidth
val top = nextInt(1, measuredHeight) //kotlin的Random.nextInt函数
childView.layout(left, top, left + childView.measuredWidth, top + childView.measuredHeight)

5、开启弹幕滚动的动画

  • 那从左到右的动画那岂不是很简单吗
  • 可以使用TranslateAnimation相对简单
  • 但是后面大家需要扩展一下效果还是用ValueAnimator比较好(透明度、旋转角度、缩放比例)
//5、开启弹幕子View动画
val anim : ValueAnimator = ValueAnimator.ofFloat(1.0f).apply {
    
    
    duration = 7000
    interpolator = LinearInterpolator() //线性的插值器
    addUpdateListener {
    
    
        val value = it.animatedFraction
        val left = (measuredWidth - value *(measuredWidth + childView.measuredWidth)).toInt()
        //通过改变布局来实现弹幕的滚动
        childView.layout(left, top, left + childView.measuredWidth, top + childView.measuredHeight)
    }

    addListener(onEnd = {
    
    
        removeView(childView)
        barrageList.remove(data)
    })
}

anim.start()

完事初版的弹幕就完成了。
完整代码我放在最后吧还是,在中间的话太乱了。

二、缓存优化

实现其实大家一看就都懂了,那么稍微进阶一点那肯定是要优化一下。
那么最明显的优化那肯定是View的绘制消耗的资源了。因为这样实现的话,每发送一条弹幕就意味这要绘制一个弹幕的子View,那肯定是消耗资源的。

有什么方法能减轻压力呢,那通过之前讲解的RecyclerView的缓存我们受到了启发。

1、我们把移除屏幕的弹幕子View不去做销毁,给他缓存起来不就行了吗
2、再需要新的弹幕子View添加的时候,给他复用一下,就会减少绘制,减轻压力了
3、这样就利用了RecyclerView的缓存思想,进行了优化

第一步:使用SimplePool

Android中在androidx.core.util提供了一个缓冲池Pools类。来帮助开发者们来缓存一些视图、对象等等。
我们就利用他的实现类SimplePool来帮助我们实现缓存。
先看一下SimplePool中的源码:(相关的解释都写在注释上了)

public class SimplePool<T> implements Pools.Pool<T> {
    
    
    // 池对象容器
    private final Object[] mPool;
    // 池大小
    private int mPoolSize;

    public SimplePool(int maxPoolSize) {
    
    
        if (maxPoolSize <= 0) {
    
    
            throw new IllegalArgumentException("The max pool size must be > 0");
        }
        // 构造池对象容器
        mPool = new Object[maxPoolSize];
    }

    // 从池容器中获取对象
    public T acquire() {
    
    
        if (mPoolSize > 0) {
    
    
            // 总是从池容器末尾读取对象
            final int lastPooledIndex = mPoolSize - 1;
            T instance = (T) mPool[lastPooledIndex];
            mPool[lastPooledIndex] = null;
            mPoolSize--;
            return instance;
        }
        return null;
    }

    // 释放对象并存入池
    @Override
    public boolean release(@NonNull T instance) {
    
    
        if (isInPool(instance)) {
    
    
            throw new IllegalStateException("Already in the pool!");
        }
        // 总是将对象存到池尾
        if (mPoolSize < mPool.length) {
    
    
            mPool[mPoolSize] = instance;
            mPoolSize++;
            return true;
        }
        return false;
    }

    // 判断对象是否在池中
    private boolean isInPool(@NonNull T instance) {
    
    
        // 遍历池对象
        for (int i = 0; i < mPoolSize; i++) {
    
    
            if (mPool[i] == instance) {
    
    
                return true;
            }
        }
        return false;
    }
}

第二步:应用到弹幕View中

1、初始化弹幕池

// 弹幕池
private var pool: Pools.SimplePool<TextView> = Pools.SimplePool(10)

2、获取缓存的弹幕空间(这里我用了TextView,可以传递任何View因为他的SimplePool是范型的)

//1、初始化弹幕子View
//pool.acquire() 失败之后 就创建一个
val childView = pool.acquire() ?: TextView(context).apply {
    
    
    text = data.text
    textSize = data.textSize
}

3、在动画结束时销毁视图时缓存起来

addListener(onEnd = {
    
    
    removeView(childView)
    pool.release(childView)
    barrageList.remove(data)
})

这样我们的弹幕就减轻了重新创建的压力,嘻嘻。简单的小优化。

三、弹幕框架DanmakuFlameMaster

介绍DanmakuFlameMaster

1、弹幕数据的解析:DanmakuFlameMaster支持多种弹幕数据格式,包括ASS、XML、JSON等。它首先根据不同的格式,对弹幕数据进行解析,将其转化为统一的数据结构。

2、弹幕的布局和渲染:通过对解析得到的弹幕数据进行布局,确定每条弹幕在屏幕上的位置和显示时间。然后,使用图像渲染技术,将弹幕绘制到屏幕上。

3、弹幕的控制和交互:DanmakuFlameMaster提供了丰富的弹幕控制和交互功能。用户可以设置弹幕的显示样式、字体颜色、速度等属性。同时,还支持弹幕的暂停、播放、弹幕屏蔽等操作。

4、弹幕的优化和性能提升:为了提高弹幕的渲染效率和用户体验,DanmakuFlameMaster采用了多线程技术和缓存策略。它将弹幕的渲染和显示分离开来,使得弹幕的处理和渲染可以并行进行,从而提高了整体的性能和效率。

总的来说,DanmakuFlameMaster通过对弹幕数据的解析和渲染,以及弹幕的控制和交互,实现了弹幕的高效显示和优化。它在很多弹幕视频网站和应用中被广泛应用。

DanmakuFlameMaster的使用

1、引入DanmakuFlameMaster库
首先,在你的项目的build.gradle文件中添加以下依赖项,以引入DanmakuFlameMaster库:

implementation 'com.github.ctiao:DanmakuFlameMaster:0.8.7'

然后,进行同步操作,确保依赖项成功导入。
2、DanmakuView的基本使用
在你的布局文件中添加DanmakuView控件,并设置相关属性。例如,指定宽高、背景色等:

<su.litvak.danmaku.view.DanmakuView
    android:id="@+id/danmaku_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#000000" />

在代码中,获取DanmakuView的实例,并进行基本的配置和初始化操作:

DanmakuView danmakuView = findViewById(R.id.danmaku_view);
danmakuView.enableDanmakuDrawingCache(true);
danmakuView.enableDanmakuDropping(true);
danmakuView.setCallback(new DrawHandler.Callback() {
    
    
    @Override
    public void prepared() {
    
    
        danmakuView.start();
    }

    @Override
    public void updateTimer(DanmakuTimer timer) {
    
    
        
    }

    @Override
    public void danmakuShown(BaseDanmaku danmaku) {
    
    
        
    }

    @Override
    public void drawingFinished() {
    
    
        
    }
});
danmakuView.prepare(parser, mContext);  // 通过解析器解析弹幕数据

3、解析和添加弹幕数据
DanmakuFlameMaster支持从多种来源解析和添加弹幕数据,包括本地文件、URL、InputStream等。
例如,从本地文件解析和添加弹幕数据:

BaseDanmakuParser parser = new BiliDanmukuParser();
DefaultDanmakuContext danmakuContext = DefaultDanmakuContext.create();
danmakuView.prepare(parser, danmakuContext);
danmakuView.showFPS(true);  // 如果需要显示FPS,可以设置为true

try {
    
    
    FileInputStream fis = new FileInputStream("path/to/your/danmaku.xml");
    danmakuView.loadData(parser, fis);
} catch (FileNotFoundException e) {
    
    
    e.printStackTrace();
}

另外,DanmakuFlameMaster还提供了多种配置参数,例如字体、字号、显示区域等,开发者可以根据需要进行自定义设置。
4、控制弹幕的播放和暂停
DanmakuView提供了一些方法,用于控制弹幕的播放和暂停。例如:

danmakuView.start();  // 开始播放弹幕
danmakuView.pause();  // 暂停播放弹幕
danmakuView.resume();  // 恢复播放弹幕
danmakuView.stop();  // 停止播放弹幕
danmakuView.release();  // 释放资源

5、弹幕的发送和屏蔽
DanmakuFlameMaster还支持用户发送弹幕和屏蔽指定类型的弹幕。例如:
发送弹幕:

BaseDanmaku danmaku = danmakuContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL);
danmaku.text = "这是一条弹幕";
danmaku.padding = 5;
danmaku.textSize = 25f;
danmaku.textColor = Color.WHITE;
danmaku.setTime(danmakuView.getCurrentTime() + 1200);
danmakuView.addDanmaku(danmaku);

屏蔽指定类型的弹幕:

// 屏蔽顶部弹幕
danmakuContext.setDanmakuFilter(new IDanmakuFilter() {
    
    
    @Override
    public boolean filter(BaseDanmaku danmaku, int index, danmakuContext context) {
    
    
        if (danmaku.getType() == BaseDanmaku.TYPE_SCROLL_LR) {
    
    
            return false;
        }
        return true;
    }
});

ok了,DanmakuFlameMaster的使用就讲解到这里,还有很多玩法自己探索吧。
通过使用这个强大的弹幕库,开发者可以轻松实现弹幕的播放、发送和屏蔽等功能,为视频、直播等场景提供更加丰富的交互体验。

四、初步实现的完整代码

CustomBarrageView.kt

class CustomBarrageView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : ViewGroup(context, attrs, defStyleAttr) {
    
    

    // 弹幕池
    private var pool: Pools.SimplePool<TextView> = Pools.SimplePool(10)
    //弹幕的数据列表
    private var barrageList = mutableListOf<BarrageItem>()

    fun addBarrage(data: BarrageItem){
    
    
        //1、初始化弹幕子View
        //pool.acquire() 失败之后 就创建一个
        val childView = pool.acquire() ?: TextView(context).apply {
    
    
            text = data.text
            textSize = data.textSize
            setTextColor(ContextCompat.getColor(context, R.color.white))
        }
        //2、测量弹幕子View,也就是measure呗
        childView.measure(measuredWidth, measuredHeight)
        //3、添加弹幕子View
        addView(childView)
        //4、设置弹幕子View的布局,也就是layout呗
        //这里我想让他从右面到左面移动,上下的位置是随机的
        val left = measuredWidth
        val top = nextInt(1, measuredHeight) //kotlin的Random.nextInt函数
        childView.layout(left, top, left + childView.measuredWidth, top + childView.measuredHeight)
        //5、开启弹幕子View动画
        val anim : ValueAnimator = ValueAnimator.ofFloat(1.0f).apply {
    
    
            duration = 7000
            interpolator = LinearInterpolator() //线性的插值器
            addUpdateListener {
    
    
                val value = it.animatedFraction
                val left = (measuredWidth - value *(measuredWidth + childView.measuredWidth)).toInt()
                //通过改变布局来实现弹幕的滚动
                childView.layout(left, top, left + childView.measuredWidth, top + childView.measuredHeight)
            }

            addListener(onEnd = {
    
    
                removeView(childView)
                pool.release(childView)
                barrageList.remove(data)
            })
        }

        anim.start()
    }

    fun addBarrageList(dataList : MutableList<BarrageItem>){
    
    
        this.barrageList = dataList
        dataList.forEach {
    
    
            addBarrage(it)
        }
    }

    override fun onLayout(p0: Boolean, p1: Int, p2: Int, p3: Int, p4: Int) {
    
    

    }
}

BarrageItem.kt

//可以定义图片头像、字体颜色、等等。我用的TextView给大家演示,
//大家可以扩展到自己的自定义弹幕子View上
data class BarrageItem(var text: String, var textSize: Float = 16f)

效果如下:
在这里插入图片描述

总结

总结就是,后面一篇是自定义View实战《圆形头像》,之后给我点点赞。

猜你喜欢

转载自blog.csdn.net/weixin_45112340/article/details/131511429