Recyclerview实现的弹幕 (旧)

1字数 680阅读 6025

大部分代码在源码中已删,并优化到新版中。
新版简介可到: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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
  • 大观园里有十二钗,简书中美丽的女子可以说成千上万。就我比较熟悉的,就随手就可以列出很多: 至情至性的青衫湿旧 纯净...
  • 一生吃尽二王书,怀素以来君笑如。 狂扫媚流终大器,世人当恨贰臣嘘。 注:王铎,字觉斯。博古好学,精于诗文书画。草书...
  • 人是一个情感的动物,如果周围都是冷漠的,即便再美的食物也会变的无味。 以前总是爱看一些校园剧,每天都会幻想着自己也...
  • 今天答辩,总算对自己这些天的学习有了交代。