【原创】仿视频网站弹幕效果

一、需求

开发一个类似bilibili的视频弹幕效果。网上有bilibili的开源项目。那么我们要实现一个简易的,应该怎么办呢?
有办法的,先看效果:

弹幕.png

二、分析

最直接的办法是自定义一个ScreenView作为幕布,然后绘制一个个的子弹(每一个view,暂且这么叫),但是想想一个个的draw,效率应该不高。
换成自定义ViewGroup,然后创建一个个的子弹view add进去,确定子弹的left和top就好了,然后view自己去执行动画,起始和终点位置也很好确定:

起点:就是屏幕宽度
终点:距屏幕左边子弹(每一个view,暂且这么叫)宽度的长度

子弹的left很好确定,就是屏幕宽度,但是top怎么办?
我们可以按幕布的高度去随机一个值,但是随机值得话有风险,子弹会重叠。
那就先按子弹的高度去划分屏幕,把屏幕分成固定的行数,然后判断随机的值落在哪一行。
还有一个问题,子弹和子弹之间是有空隙的,随机值落在空隙之间怎么办?
这个很好处理,落在空隙之间的数值全部计为-1,只要是-1就重新random。
说了这么多一步步的看代码怎么实现吧。

三、代码实现

3.1 子弹view

别的不管,先画布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="horizontal"
              android:layout_width="wrap_content"
              android:layout_height="36dp"
              android:background="@drawable/bg_bullet_view"
              android:padding="4dp"
              android:gravity="center_vertical"
    >
    <ImageView
        android:id="@+id/iv_head_view"
        android:layout_width="28dp"
        android:layout_height="28dp"
        android:src="@mipmap/headview"
        />

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="很好看"
        android:textSize="14sp"
        android:layout_marginLeft="4dp"
        />

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/zan"
        android:layout_marginLeft="10dp"
        />

    <TextView
        android:id="@+id/tv_zan"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="254"
        android:textSize="12sp"
        android:layout_marginRight="6dp"
        android:layout_marginLeft="2dp"
        />
</LinearLayout>

一个头像,一个标题加上一个赞数,很常见的样式。再看子弹view的代码:

public class BulletView extends LinearLayout {

    private ImageView ivHeadView;           //头像
    private TextView tvTitle;               //标题
    private TextView tvZan;                 //赞数
    private ObjectAnimator animator;        //动画
    private float animatedValue;            //记录当前动画的移动值
    private Bullet bullet;                  //子弹数据
    private int line;                       //记录所在的行数
    private Point startPoint;               //开始点
    private Point endPoint;                 //终点
    private OnAllShowInScreen listener;     //动画监听
    private boolean hasClear = false;       //标记  标记是否移除了起始位置的子弹
}

这里最重要的是一个属性动画属性,就是说每一个子弹都有一个动作,从屏幕右边移动到屏幕左边。先不管动画,先看怎么把子弹添加进幕布。

3.2 幕布的创建

在幕布里用到一个生产者消费者的知识,我们需要开启一个线程相当于是消费者,一直消费子弹,而主线程就要不断的添加子弹,类似于生产者。
主线程我们可以手动控制添加,不需要等待,但是消费者不一样,没有子弹的时候,他需要等待,我们添加了新的子弹去唤醒他消费,每消费一个子弹,我们就往幕布增加一个子弹view。
这是第一种需要等待的情况,还有一种是子弹过多,屏幕上所有的行数都有子弹正在执行开始动画的时候,消费子弹的也需要等待。注意是开始动画而不是所有行数都有动画的时候,这是为了避免新添加的子弹view覆盖正在执行开始动画的子弹view。
挑主要的代码看:

public class BulletScreenView extends FrameLayout {
    ...
        
    private List<Bullet> bullets = new ArrayList<>();           //子弹仓库
    private List<Rect> rightRect = new ArrayList<>();           //随机的高度值所在的rect
    private List<List<Bullet>> lines = new ArrayList<>();       //每一行正在执行开始动画的子弹
    private List<Bullet> lineBullets = new ArrayList<>();       //所有行正在执行开始动画的子弹
    
    // 锁
    private final Lock lock = new ReentrantLock();
    // 消费者状态
    private final Condition consumer = lock.newCondition();
    
    ...
}

幕布view中这四个变量非常重要,第四个和第三也不是重复,避免了大量遍历。
一个个看,bullets就是存储所有添加进来的子弹,看暴漏的方法:

    /**
     * 添加子弹
     * @param bs
     */
    public void addBullet(List<Bullet> bs) {
        lock.lock();
        bullets.addAll(bs);
        consumer.signal();
        lock.unlock();
    }

前后加锁,只要有新子弹进来就去通知消费者线程,看消费者线程做了啥:

    /**
     * 消耗线程
     */
    public class ConsumerThread extends Thread {
        @Override
        public void run() {
            while (true){
                lock.lock();
                while (bullets.size() == 0 || lineBullets.size() == lineCount) {
                    try {
                        consumer.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                Bullet bullet = bullets.remove(0);
                initBulletView(bullet);
                lineBullets.add(bullet);
                handler.obtainMessage(0, bullet).sendToTarget();

                lock.unlock();
            }
        }
    }

死循环里面,还是加锁,然后while下的判断条件,当子弹仓库为空或者所有行开始动画个数等于个数的时候,消费者进入等待,否则就去取第一个子弹,然后初始化子弹view,这个时候需要把这个子弹view添加进所有行开始动画的lineBullets里面记录下来,还发出去一个handler消息。

  1. 初始化子弹view做了啥;
  2. handler做了啥。
    首先看第一个代码:
    /**
     * 初始化子弹view
     * @param bullet
     */
    private void initBulletView(Bullet bullet) {
        //随机高度
        double randomHeight = Math.random() * mHeight;
        //判断在哪一行
        int currentLine = seekLine(randomHeight);
        //如果行数等于-1 说明不合法 循环取
        while (currentLine == -1){
            randomHeight = Math.random() * mHeight;
            currentLine = seekLine(randomHeight);
        }
        //如果这一行没有开始动画,直接初始化子弹
        if (lines.get(currentLine).size() == 0) {
            BulletView bulletView = new BulletView(mContext);
            LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
            // 找到当前行的top值
            params.topMargin = seekLineTop(randomHeight);
            bulletView.setLine(currentLine);
            //设置动画监听
            bulletView.setListener(new BulletView.OnAllShowInScreen() {
                @Override
                public void onAllShow(BulletView bulletView) {
                    lock.lock();
                    int measuredWidth1 = bulletView.getMeasuredWidth();
                    // 如果动画执行到 子弹完全暴漏在幕布上 的位置,那么这一行的 开始动画记录要清掉了
                    if (bulletView.getAnimatedValue() < mWidth - measuredWidth1) {
                        //是否清除过了,因为动画后续会一直进入这里  但是清除只需要清一次
                        if(!bulletView.isHasClear()) {
                            int line = bulletView.getLine();
                            //当前行记录动画清除
                            lines.get(line).clear();
                            //总开始动画记录也清掉
                            lineBullets.remove(bulletView.getBullet());
                            consumer.signal();
                            bulletView.setHasClear(true);
                        }
                    }
                    lock.unlock();
                }
            });
            bulletView.setLayoutParams(params);
            // 记录 当前行 开始动画
            lines.get(currentLine).add(bullet);
            bullet.setBulletView(bulletView);
        } else {
            //否则调用自己 重新选择行数
            initBulletView(bullet);
        }
    }

动画回调里面有一个consumer.signal();就是一旦有行数空一个位置出来,就去通知消费者送一个子弹过来。
那么handler做了啥呢?

    /**
     * 主线程刷新UI
     */
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 0:
                    Bullet obj = (Bullet) msg.obj;
                    BulletView bulletView = obj.getBulletView();
                    bulletView.setData(obj);
                    int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
                    bulletView.measure(spec, spec);
                    int measuredWidth = bulletView.getMeasuredWidth();
                    bulletView.startAnim(new Point(mWidth, y), new Point(-measuredWidth, y), 5000);
                    addView(bulletView);
                    break;
            }
        }
    };

设置数据之后,计算出这时候子弹的长度就比较精确了。然后开启子弹的动画,并把子弹add进布局。子弹的view我们已经记录在子弹里面了。到这里就是完整的思路了。

更多代码看GayHub,有兴趣试一试。

很多视频的弹幕触摸还有暂停,松开继续跑的需求没加了,用记录的动画位置就可以做到,不写了。感谢看到这里。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容