Android仿视频网站弹幕效果

龙旋

共 7666字,需浏览 16分钟

 ·

2021-08-23 12:47

一、需求

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

有办法的,先看效果:


二、分析

最直接的办法是自定义一个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我们已经记录在子弹里面了。到这里就是完整的思路了。


源码地址:
https://github.com/rjpacket/PropertyAnim


到这里就结束啦。
浏览 23
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报