千锋教育论坛

查看: 6002|回复: 20

Android自制弹幕的实现

[复制链接]

13

主题

130

帖子

214

积分

牛人

Rank: 3Rank: 3

发表于 17-3-31 11:59:13 | 显示全部楼层 |阅读模式
Android自制弹幕的实现

1.弹幕垂直方向固定,效果如下

1.gif

2.弹幕垂直方向随机,效果如下

2.gif

上面效果图中白色的背景就是弹幕本身,是一个自定义的FrameLayout,这里为了更好的展示弹幕的位置才设置成了白色。如果是叠加在VideoView上的话,就需要设置成透明色了。

制作弹幕需要考虑以下几点问题:
1.弹幕的大小可以随意调整
2.弹幕内移动的item(或者称字幕)出现的位置,水平方向是从屏幕右边移动到屏幕左边,垂直方向是不能超出弹幕本身的高度的.
3.字幕移除屏幕后,需要将对应item(字幕)从其父容器(弹幕)中移除.
4.如果字幕出现的垂直方向的高度是随机的,那么还需要避免字幕重叠的情况.



13

主题

130

帖子

214

积分

牛人

Rank: 3Rank: 3

 楼主| 发表于 17-3-31 12:00:17 | 显示全部楼层
弹幕自定义view的代码
  1. /**
  2. * Created by dell on 2016/9/28.
  3. */
  4. public class DanmuView extends FrameLayout {
  5.     private static final String TAG = "DanmuView";
  6.     private static final long DEFAULT_ANIM_DURATION = 6000; //默认每个动画的播放时长
  7.     private static final long DEFAULT_QUERY_DURATION = 3000; //遍历弹幕的默认间隔
  8.     private LinkedList<view> mViews = new LinkedList<>();//弹幕队列
  9.     private boolean isQuerying;
  10.     private int mWidth;//弹幕的宽度
  11.     private int mHeight;//弹幕的高度
  12.     private Handler mUIHandler = new Handler();
  13.     private boolean TopDirectionFixed;//弹幕顶部的方向是否固定
  14.     private Handler mQueryHandler;
  15.     private int mTopGravity = Gravity.CENTER_VERTICAL;//顶部方向固定时的默认对齐方式

  16.     public void setHeight(int height) {
  17.         mHeight = height;
  18.     }

  19.     public void setWidth(int width) {
  20.         mWidth = width;
  21.     }

  22.     public void setTopGravity(int gravity) {
  23.         this.mTopGravity = gravity;
  24.     }

  25.     public void add(List<danmu> danmuList) {
  26.         for (int i = 0; i < danmuList.size(); i++) {
  27.             Danmu danmu = danmuList.get(i);
  28.             addDanmuToQueue(danmu);
  29.         }
  30.     }

  31.     public void add(Danmu danmu) {
  32.         addDanmuToQueue(danmu);
  33.     }

  34.     public DanmuView(Context context) {
  35.         this(context, null);
  36.     }

  37.     public DanmuView(Context context, AttributeSet attrs) {
  38.         this(context, attrs, 0);
  39.     }

  40.     public DanmuView(Context context, AttributeSet attrs, int defStyleAttr) {
  41.         super(context, attrs, defStyleAttr);
  42.         HandlerThread thread = new HandlerThread("query");
  43.         thread.start();
  44.         //循环取出弹幕显示
  45.         mQueryHandler = new Handler(thread.getLooper()) {
  46.             @Override
  47.             public void handleMessage(Message msg) {
  48.                 final View view = mViews.poll();
  49.                 if (null != view) {
  50.                     mUIHandler.post(new Runnable() {
  51.                         @Override
  52.                         public void run() {
  53.                             //添加弹幕
  54.                             showDanmu(view);
  55.                         }
  56.                     });
  57.                 }
  58.                 sendEmptyMessageDelayed(0, DEFAULT_QUERY_DURATION);
  59.             }
  60.         };
  61.     }

  62.     /**
  63.      * 将要展示的弹幕添加到队列中
  64.      *
  65.      * @param danmu
  66.      */
  67.     private void addDanmuToQueue(Danmu danmu) {
  68.         if (null != danmu) {
  69.             final View view = View.inflate(getContext(), R.layout.layout_danmu, null);
  70.             TextView usernameTv = (TextView) view.findViewById(R.id.tv_username);
  71.             TextView infoTv = (TextView) view.findViewById(R.id.tv_info);
  72.             ImageView headerIv = (ImageView) view.findViewById(R.id.iv_header);
  73.             usernameTv.setText(danmu.getUserName());//昵称
  74.             infoTv.setText(danmu.getInfo());//信息
  75.             Glide.with(getContext()).//头像
  76.                     load(danmu.getHeaderUrl()).
  77.                     transform(new CropCircleTransformation(getContext())).into(headerIv);
  78.             view.measure(0, 0);
  79.             //添加弹幕到队列中
  80.             mViews.offerLast(view);
  81.         }
  82.     }

  83.     /**
  84.      * 播放弹幕
  85.      *
  86.      * @param topDirectionFixed 弹幕顶部的方向是否固定
  87.      */
  88.     public void startPlay(boolean topDirectionFixed) {
  89.         this.TopDirectionFixed = topDirectionFixed;
  90.         if (mWidth == 0 || mHeight == 0) {
  91.             getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
  92.                 @SuppressLint("NewApi")
  93.                 @Override
  94.                 public void onGlobalLayout() {
  95.                     getViewTreeObserver().removeOnGlobalLayoutListener(this);
  96.                     if (mWidth == 0) mWidth = getWidth() - getPaddingLeft() - getPaddingRight();
  97.                     if (mHeight == 0) mHeight = getHeight() - getPaddingTop() - getPaddingBottom();
  98.                     if (!isQuerying) {
  99.                         mQueryHandler.sendEmptyMessage(0);
  100.                     }
  101.                 }
  102.             });
  103.         } else {
  104.             if (!isQuerying) {
  105.                 mQueryHandler.sendEmptyMessage(0);
  106.             }
  107.         }
  108.     }

  109.     /**
  110.      * 显示弹幕,包括动画的执行
  111.      *
  112.      * @param view
  113.      */
  114.     private void showDanmu(final View view) {
  115.         isQuerying = true;
  116.         Log.d(TAG, "mWidth:" + mWidth + " mHeight:" + mHeight);
  117.         final LayoutParams lp = new LayoutParams(view.getMeasuredWidth(), view.getMeasuredHeight());
  118.         lp.leftMargin = mWidth;
  119.         if (TopDirectionFixed) {
  120.             lp.gravity = mTopGravity | Gravity.LEFT;
  121.         } else {
  122.             lp.gravity = Gravity.LEFT | Gravity.TOP;
  123.             lp.topMargin = getRandomTopMargin(view);
  124.         }
  125.         view.setLayoutParams(lp);
  126.         view.setTag(lp.topMargin);
  127.         //设置item水平滚动的动画
  128.         ValueAnimator animator = ValueAnimator.ofInt(mWidth, -view.getMeasuredWidth());
  129.         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
  130.             @Override
  131.             public void onAnimationUpdate(ValueAnimator animation) {
  132.                 lp.leftMargin = (int) animation.getAnimatedValue();
  133.                 view.setLayoutParams(lp);
  134.             }
  135.         });
  136.         addView(view);//显示弹幕
  137.         animator.setDuration(DEFAULT_ANIM_DURATION);
  138.         animator.setInterpolator(new LinearInterpolator());
  139.         animator.start();//开启动画
  140.         animator.addListener(new AnimatorListenerAdapter() {
  141.             @Override
  142.             public void onAnimationEnd(Animator animation) {
  143.                 view.clearAnimation();
  144.                 existMarginValues.remove(view.getTag());//移除已使用过的顶部边距
  145.                 removeView(view);//移除弹幕
  146.                 animation.cancel();
  147.             }
  148.         });
  149.     }

  150.     //记录当前仍在显示状态的弹幕的垂直方向位置(避免重复)
  151.     private Set<integer> existMarginValues = new HashSet<>();
  152.     private int linesCount;
  153.     private int range = 10;

  154.     private int getRandomTopMargin(View view) {
  155.         //计算可用的行数
  156.         linesCount = mHeight / view.getMeasuredHeight();
  157.         if (linesCount <= 1) {
  158.             linesCount = 1;
  159.         }
  160.         Log.d(TAG, "linesCount:" + linesCount);
  161.         //检查重叠
  162.         while (true) {
  163.             int randomIndex = (int) (Math.random() * linesCount);
  164.             int marginValue = randomIndex * (mHeight / linesCount);
  165.             //边界检查
  166.             if (marginValue > mHeight - view.getMeasuredHeight()) {
  167.                 marginValue = mHeight - view.getMeasuredHeight() - range;
  168.             }
  169.             if (marginValue == 0) {
  170.                 marginValue = range;
  171.             }
  172.             if (!existMarginValues.contains(marginValue)) {
  173.                 existMarginValues.add(marginValue);
  174.                 Log.d(TAG, "marginValue:" + marginValue);
  175.                 return marginValue;
  176.             }
  177.         }
  178.     }
  179. }</integer></danmu></view>
复制代码

13

主题

130

帖子

214

积分

牛人

Rank: 3Rank: 3

 楼主| 发表于 17-3-31 12:03:25 | 显示全部楼层


弹幕实体类:
  1. /**
  2. * Created by dell on 2016/9/28.
  3. */
  4. public class Danmu {
  5.     private String headerUrl;//头像
  6.     private String userName;//昵称
  7.     private String info;//信息

  8.     public String getHeaderUrl() {
  9.         return headerUrl;
  10.     }

  11.     public void setHeaderUrl(String headerUrl) {
  12.         this.headerUrl = headerUrl;
  13.     }

  14.     public String getUserName() {
  15.         return userName;
  16.     }

  17.     public void setUserName(String userName) {
  18.         this.userName = userName;
  19.     }

  20.     public String getInfo() {
  21.         return info;
  22.     }

  23.     public void setInfo(String info) {
  24.         this.info = info;
  25.     }
  26. }
复制代码


13

主题

130

帖子

214

积分

牛人

Rank: 3Rank: 3

 楼主| 发表于 17-3-31 12:04:18 | 显示全部楼层
测试类,MainActivity
  1. public class MainActivity extends AppCompatActivity {
  2.     DanmuView mDanmuView;
  3.     EditText mMsgEdt;
  4.     Button mSendBtn;
  5.     Handler mDanmuAddHandler;
  6.     boolean continueAdd;
  7.     int counter;

  8.     @Override
  9.     protected void onResume() {
  10.         super.onResume();
  11.         mDanmuView.startPlay(true);//true表示弹幕的垂直方向是固定的,false则随机
  12.         continueAdd = true;
  13.         mDanmuAddHandler.sendEmptyMessageDelayed(0, 6000);
  14.     }

  15.     @Override
  16.     protected void onPause() {
  17.         super.onPause();
  18.         continueAdd = false;
  19.         mDanmuAddHandler.removeMessages(0);
  20.     }

  21.     @Override
  22.     protected void onCreate(Bundle savedInstanceState) {
  23.         super.onCreate(savedInstanceState);
  24.         setContentView(R.layout.activity_main);
  25.         initView();
  26.         initData();
  27.         initListener();
  28.     }

  29.     private void initView() {
  30.         mDanmuView = (DanmuView) findViewById(R.id.danmuView);
  31.         mMsgEdt = (EditText) findViewById(R.id.edt_msg);
  32.         mSendBtn = (Button) findViewById(R.id.btn_send);
  33.     }

  34.     private void initData() {
  35.         List<Danmu> danmuList = new ArrayList<>();
  36.         for (int i = 0; i < 3; i++) {
  37.             Danmu danmu = new Danmu();
  38.             danmu.setHeaderUrl("http://tupian.qqjay.com/tou3/2016/0725/cb00091099ffbf09f4861f2bbb5dd993.jpg");
  39.             danmu.setUserName("Mr.chen" + i);
  40.             danmu.setInfo("我是弹幕啊,不要问我为什么不可以那么长!!!");
  41.             danmuList.add(danmu);
  42.         }
  43.         mDanmuView.add(danmuList);

  44.         //下面是模拟每秒添加一个弹幕的过程
  45.         HandlerThread ht = new HandlerThread("send danmu");
  46.         ht.start();
  47.         mDanmuAddHandler = new Handler(ht.getLooper()) {
  48.             @Override
  49.             public void handleMessage(Message msg) {
  50.                 runOnUiThread(new Runnable() {
  51.                     @Override
  52.                     public void run() {
  53.                         Danmu danmu = new Danmu();
  54.                         danmu.setHeaderUrl("http://tupian.qqjay.com/tou3/2016/0803/87a8b262a5edeff0e11f5f0ba24fb22f.jpg");
  55.                         danmu.setUserName("Mr.new" + (counter++));
  56.                         danmu.setInfo("新的弹幕啊!!!新的弹幕啊!!!新的弹幕啊!!!新的弹幕啊!!!");
  57.                         mDanmuView.add(danmu);
  58.                     }
  59.                 });
  60.                 //继续添加
  61.                 if (continueAdd) {
  62.                     sendEmptyMessageDelayed(0, 1000);
  63.                 }
  64.             }
  65.         };
  66.     }

  67.     private void initListener() {
  68.         //手动添加
  69.         mSendBtn.setOnClickListener(new View.OnClickListener() {
  70.             @Override
  71.             public void onClick(View v) {
  72.                 String msg = mMsgEdt.getText().toString().trim();
  73.                 if (TextUtils.isEmpty(msg)) {
  74.                     Toast.makeText(MainActivity.this, "亲,你想发送什么啊?", Toast.LENGTH_SHORT).show();
  75.                     return;
  76.                 }
  77.                 mMsgEdt.setText("");
  78.                 Danmu danmu = new Danmu();
  79.                 danmu.setHeaderUrl("http://img0.imgtn.bdimg.com/it/u=2198087564,4037394230&fm=11&gp=0.jpg");
  80.                 danmu.setUserName("I'am good man");
  81.                 danmu.setInfo("我是新人:" + msg);
  82.                 mDanmuView.add(danmu);
  83.             }
  84.         });
  85.     }
  86. }
复制代码


12

主题

198

帖子

451

积分

牛人

Rank: 3Rank: 3

发表于 17-3-31 12:07:34 | 显示全部楼层
好久没去关注了,今天再去看看,谢谢了!

1

主题

136

帖子

304

积分

牛人

Rank: 3Rank: 3

发表于 17-3-31 12:38:52 | 显示全部楼层
给力啊,不过一分辛勤一分收获

0

主题

137

帖子

301

积分

牛人

Rank: 3Rank: 3

发表于 17-3-31 12:44:08 | 显示全部楼层
自家人来顶贴,不要问我是谁

20

主题

197

帖子

454

积分

牛人

Rank: 3Rank: 3

发表于 17-3-31 13:14:24 | 显示全部楼层
默默的顶一下

10

主题

205

帖子

464

积分

牛人

Rank: 3Rank: 3

发表于 17-3-31 13:55:11 | 显示全部楼层
千锋,we are family…

32

主题

195

帖子

424

积分

牛人

Rank: 3Rank: 3

发表于 17-3-31 14:50:39 | 显示全部楼层
留名求浮云,万一火了呢?
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

联系我们

电话:400-654-7778
交流群:523516910

点击咨询

学习资料

千锋教育

订阅|小黑屋|手机版|千锋教育论坛 ( 京ICP备12003911号-3

GMT+8, 20-4-3 12:20 , Processed in 0.348668 second(s), 45 queries .

Powered by 千锋教育 X3.2

© 2001-2015

快速回复 返回顶部 返回列表