Recyclerview实现的弹幕 (旧)

大部分代码在源码中已删,并优化到新版中。
新版简介可到:http://www.jianshu.com/p/6649f5239aef


注意!由于版本更新,下方的代码大部分已不存在。
项目地址:https://github.com/xujiaji/DMView

大纲、效果图

RecyclerView实现弹幕.png

弹幕演示.gif

简介

最近公司项目需求要求实现弹幕,正是这次我写这个弹幕demo的原因。目前已经实现弹幕的添加,对实现部分的简单封装。可以通过调用addBarrage(String name, String msg, String pic)传递名字、消息、头像地址添加一个弹幕。弹幕重下至上添加一次(默认十行),填充完总行数后,优先填充下方已经滑动完的行。

思路

  1. 由于RecyclerView可以添加item动画
  2. 每一个弹幕是一个对象,初始化时isLive = true表示活动状态
  3. 当弹幕结束后isLive = false表示未活动状态(它的值由动画结束监听赋值)
  4. 当初始化十个弹幕后(默认十行),循环检测isLive是否是false,如果是那么重置内容,然后更新对应的行。

实现

1. 首先是动画怎么来

  • 动画实现拷贝了这个项目的几个类:https://github.com/wasabeef/recyclerview-animators
  • 主要是里面的动画父类:BaseItemAnimator
  • 然后创建了一个BaseItemAnimator的子类OverTotalLengthAnimator,实现从右到左的动画效果,添加了动画结束监听,详细代码如下所示:
package com.jiaji.dmview.recyclerview_item_anim;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPropertyAnimatorListener;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;

public class OverTotalLengthAnimator extends BaseItemAnimator {
    @Override
    protected void animateRemoveImpl(RecyclerView.ViewHolder holder) {
        Log.e("TAG", "animateRemoveImpl...........................");
    }

    @Override
    protected void preAnimateRemoveImpl(RecyclerView.ViewHolder holder) {
        Log.e("TAG", "preAnimateRemoveImpl...........................");
    }
//当添加数据后,调用notifyItemInserted会先执行这个方法,将item头部移动至右侧边缘
    @Override
    protected void preAnimateAddImpl(RecyclerView.ViewHolder holder) {
        Log.e("TAG", "preAnimateAddImpl...........................");
        ViewCompat.setTranslationX(holder.itemView, holder.itemView.getRootView().getWidth());
    }
//当执行完preAnimateAddImpl后,随后执行这个方法调用startAnimation实现从右至左的动画效果
    @Override
    protected void animateAddImpl(RecyclerView.ViewHolder holder) {
        Log.e("TAG", "animateAddImpl...........................");
        startAnimation(holder);
    }
//当填充完是个item后将不会添加,而是复用之前的弹幕对象,然后更新`notifyItemChanged`时调用这个方法。
    @Override
    public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromX, int fromY, int toX, int toY) {
        Log.e("TAG", "animateChange...........................");
//由于动画结束后隐藏了item,所以初始化要显示
        newHolder.itemView.setVisibility(View.VISIBLE);
        ViewCompat.setTranslationX(newHolder.itemView, newHolder.itemView.getRootView().getWidth());
        startAnimation(newHolder);
        return true;
    }

//开始动画,整个过程默认8秒,当动画结束后调用over结束监听
    private void startAnimation(final RecyclerView.ViewHolder holder) {
        ViewCompat.animate(holder.itemView)
                .translationX(-holder.itemView.getRootView().getWidth())
                .setDuration(8000)
                .setListener(new ViewPropertyAnimatorListener() {
                    @Override
                    public void onAnimationStart(View view) {
                        Log.e("TAG", "onAnimationStart");
                    }

                    @Override
                    public void onAnimationEnd(View view) {
                        holder.itemView.setVisibility(View.GONE);
                        if (onAnimListener != null) {
                            onAnimListener.over();
                        }
                        Log.e("TAG", "onAnimationEnd");
                    }

                    @Override
                    public void onAnimationCancel(View view) {
                        Log.e("TAG", "onAnimationCancel");
                    }
                })
                .setStartDelay(getAddDelay(holder))
                .start();
    }
    private OnAnimListener onAnimListener;
    public void setOnAnimListener(OnAnimListener l) {
        this.onAnimListener = l;
    }
    public interface OnAnimListener {
        void over();
    }
}

2.填充数据

  • 初始化RecyclerView,垂直布局,从下至上添加。
  • 判断是否能继续添加(是否大于10行),如不能则循环检测是否有动画结束的item,有则更新这个item。没有则添加到缓存list中,当每次动画结束后继续添加缓存list中的弹幕对象。
  • 之前实现部分都是实现在MainActivity中的,后来为了方便以后(我说万一哪天)要用这个,所以将其又写在了BarrageUtil里面。来看看BarrageUtil:
package com.jiaji.dmview.barrage;

import android.content.Context;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;

import com.jiaji.dmview.recyclerview_item_anim.OverTotalLengthAnimator;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by Administrator on 2016/6/7.
 */
public class BarrageUtil {
    private Context context;//上下文
    private RecyclerView rvBarrage;//展示弹幕的RecyclerView
    private List<BarrageEntity> barrageList;//填充RecyclerView的list集合
    private BarrageAdapter mBarrageAdapter;//弹幕适配器
    private OverTotalLengthAnimator anim;//弹幕动画
    private List<Integer> indexList;//保存当前出现的弹幕下标
    private List<BarrageEntity> barrageCache;//缓存当前屏幕满了时,添加不上的弹幕对象
    private LinearLayoutManager layoutManager;

    public BarrageUtil(Context context, RecyclerView rvBarrage) {
        this.context = context;
        this.rvBarrage = rvBarrage;
        init();
    }

    private void init() {
        barrageList = new ArrayList<>();
        barrageCache = new ArrayList<>();
        indexList = new ArrayList<>();
        layoutManager = new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true);
        rvBarrage.setLayoutManager(layoutManager);
        anim = new OverTotalLengthAnimator();
        rvBarrage.setItemAnimator(anim);
        mBarrageAdapter = new BarrageAdapter(barrageList);
        rvBarrage.setAdapter(mBarrageAdapter);
//当动画结束后就会调用
        anim.setOnAnimListener(new OverTotalLengthAnimator.OnAnimListener() {
            @Override
            public void over() {
                int index = indexList.get(0);//获取结束这个item的下标
                barrageList.get(index).over();//获取对应下标的对象,调用这个对象的over()方法,将这个对象的isLive设置为false
                indexList.remove(0);//删除运动下标集合中当前结束item的下标
                if (!barrageCache.isEmpty()) {//判断缓存的弹幕list是否有弹幕
                    BarrageEntity b = barrageCache.get(0);
                    addBarrage(b.getPname(), b.getChatStr(), b.getPic());
                    barrageCache.remove(0);
                }
            }
        });
    }

    public void addBarrage(String name, String msg, String pic) {
    //    Log.e("TAG", "visible_item_position = " + layoutManager.findFirstCompletelyVisibleItemPosition());
        boolean isAdd = false;
        if (barrageList.size() >= 10) {//如果大于10就不再添加
            for (int i = 0, len = barrageList.size(); i < len; i++) {
                BarrageEntity barrageEntity = barrageList.get(i);
                if (barrageEntity.isLive()) {
                    continue;
                }

                if (rvBarrage.isComputingLayout()) {//当RecyclerView正在计算时无法notifyItemChanged,有一定几率闪退,所以判断如果正在计算布局,那么则直接跳出循环
                    isAdd = false;
                    break;
                }
                barrageEntity.change(name, msg, pic);
                mBarrageAdapter.notifyItemChanged(i);
                indexList.add(i);
                isAdd = true;
                break;
            }
        } else {
            isAdd = true;
            BarrageEntity barrageEntity = new BarrageEntity(name, msg, pic);
            barrageList.add(barrageEntity);
            mBarrageAdapter.notifyItemInserted(barrageList.size() - 1);
            indexList.add(barrageList.size() - 1);
        }

        if (!isAdd) {//如果没有添加成功就添加到弹幕缓存list中
            barrageCache.add(new BarrageEntity(name, msg, pic));
        }
    }
}

3. 弹幕对象

package com.jiaji.dmview.barrage;

/**
 * Created by Administrator on 2016/6/6.
 */
public class BarrageEntity {
    private String pname;
    private String chatStr;
    private String pic;
    private boolean isLive;

    public BarrageEntity(String pname, String chatStr, String pic) {
        this.pname = pname;
        this.chatStr = chatStr;
        this.pic = pic;
        isLive = true;
    }

    public void change(String pname, String chatStr, String pic) {
        this.pname = pname;
        this.chatStr = chatStr;
        this.pic = pic;
        isLive = true;
    }

    public void over() {
        isLive = false;
    }

    public boolean isLive() {
        return isLive;
    }

    public void setLive(boolean live) {
        isLive = live;
    }

    public String getPname() {
        return pname;
    }

    public void setPname(String pname) {
        this.pname = pname;
    }

    public String getChatStr() {
        return chatStr;
    }

    public void setChatStr(String chatStr) {
        this.chatStr = chatStr;
    }

    public String getPic() {
        return pic;
    }

    public void setPic(String pic) {
        this.pic = pic;
    }
}

4.使用

package com.jiaji.dmview;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.RecyclerView;
import android.view.View;

import com.jiaji.dmview.barrage.BarrageUtil;

import java.util.Date;

public class MainActivity extends AppCompatActivity {
    private BarrageUtil mBarrageUtil;
    private RecyclerView rvBarrage;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        rvBarrage = (RecyclerView) findViewById(R.id.rvBarrage);
        mBarrageUtil = new BarrageUtil(this, rvBarrage);
    }

    public void onAddClick(View view) {
        mBarrageUtil.addBarrage(new Date().toString(), "聊天消息。。。。", "https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=150237755,4294706681&fm=116&gp=0.jpg");
    }
}

总结

目前没有看到有适合的弹幕案例,所以写了这个demo,当时想到的就只有①自定义布局添加弹幕子布局然后添加动画②就是这个demo,因为想到RecyclerView实现这样的动画更加容易写和理解。希望能帮助大家多一条实现弹幕思路。
网络图片加载使用了Glide:compile 'com.github.bumptech.glide:glide:3.7.0'

demo地址

https://github.com/xujiaji/DMView

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,398评论 25 707
  • 大观园里有十二钗,简书中美丽的女子可以说成千上万。就我比较熟悉的,就随手就可以列出很多: 至情至性的青衫湿旧 纯净...
    石竹阅读 186评论 2 7
  • 一生吃尽二王书,怀素以来君笑如。 狂扫媚流终大器,世人当恨贰臣嘘。 注:王铎,字觉斯。博古好学,精于诗文书画。草书...
    大气浩然阅读 528评论 1 3
  • 今天答辩,总算对自己这些天的学习有了交代。
    李_昀凇阅读 217评论 0 0
  • 图·文/大萌 生活中,我的朋友经常说我是一个十足的“文艺青年”,而我常常会笑着和他们说...
    牛友果星球大萌阅读 1,774评论 25 50