Android弹幕效果

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

制作弹幕需要考虑以下几点问题:

1.弹幕的大小可以随意调整

2.弹幕内移动的item(或者称字幕)出现的位置,水平方向是从屏幕右边移动到屏幕左边,垂直方向是不能超出弹幕本身的高度的.

3.字幕移除屏幕后,需要将对应item(字幕)从其父容器(弹幕)中移除.

4.如果字幕出现的垂直方向的高度是随机的,那么还需要避免字幕重叠的情况.

ok,下面是弹幕自定义view的代码:

publicclassDanmuView extendsFrameLayout {

 privatestaticfinalString TAG = "DanmuView";

 privatestaticfinallongDEFAULT_ANIM_DURATION = 6000; //默认每个动画的播放时长

 privatestaticfinallongDEFAULT_QUERY_DURATION = 3000; //遍历弹幕的默认间隔

 privateLinkedList<View> mViews = newLinkedList<>();//弹幕队列

 privatebooleanisQuerying;

 privateintmWidth;//弹幕的宽度

 privateintmHeight;//弹幕的高度

 privateHandler mUIHandler = newHandler();

 privatebooleanTopDirectionFixed;//弹幕顶部的方向是否固定

 privateHandler mQueryHandler;

 privateintmTopGravity = Gravity.CENTER_VERTICAL;//顶部方向固定时的默认对齐方式

 publicvoidsetHeight(intheight) {

  mHeight = height;

 }

 publicvoidsetWidth(intwidth) {

  mWidth = width;

 }

 publicvoidsetTopGravity(intgravity) {

  this.mTopGravity = gravity;

 }

 publicvoidadd(List<Danmu> danmuList) {

  for(inti = 0; i < danmuList.size(); i++) {

   Danmu danmu = danmuList.get(i);

   addDanmuToQueue(danmu);

  }

 }

 publicvoidadd(Danmu danmu) {

  addDanmuToQueue(danmu);

 }

 publicDanmuView(Context context) {

  this(context, null);

 }

 publicDanmuView(Context context, AttributeSet attrs) {

  this(context, attrs, 0);

 }

 publicDanmuView(Context context, AttributeSet attrs, intdefStyleAttr) {

  super(context, attrs, defStyleAttr);

  HandlerThread thread = newHandlerThread("query");

  thread.start();

  //循环取出弹幕显示

  mQueryHandler = newHandler(thread.getLooper()) {

   @Override

   publicvoidhandleMessage(Message msg) {

    finalView view = mViews.poll();

    if(null!= view) {

     mUIHandler.post(newRunnable() {

      @Override

      publicvoidrun() {

       //添加弹幕

       showDanmu(view);

      }

     });

    }

    sendEmptyMessageDelayed(0, DEFAULT_QUERY_DURATION);

   }

  };

 }

 /**

  * 将要展示的弹幕添加到队列中

  *

  * @param danmu

  */

 privatevoidaddDanmuToQueue(Danmu danmu) {

  if(null!= danmu) {

   finalView view = View.inflate(getContext(), R.layout.layout_danmu, null);

   TextView usernameTv = (TextView) view.findViewById(R.id.tv_username);

   TextView infoTv = (TextView) view.findViewById(R.id.tv_info);

   ImageView headerIv = (ImageView) view.findViewById(R.id.iv_header);

   usernameTv.setText(danmu.getUserName());//昵称

   infoTv.setText(danmu.getInfo());//信息

   Glide.with(getContext()).//头像

     load(danmu.getHeaderUrl()).

     transform(newCropCircleTransformation(getContext())).into(headerIv);

   view.measure(0, 0);

   //添加弹幕到队列中

   mViews.offerLast(view);

  }

 }


 /**

  * 播放弹幕

  *

  * @param topDirectionFixed 弹幕顶部的方向是否固定

  */

 publicvoidstartPlay(booleantopDirectionFixed) {

  this.TopDirectionFixed = topDirectionFixed;

  if(mWidth == 0|| mHeight == 0) {

   getViewTreeObserver().addOnGlobalLayoutListener(newViewTreeObserver.OnGlobalLayoutListener() {

    @SuppressLint("NewApi")

    @Override

    publicvoidonGlobalLayout() {

     getViewTreeObserver().removeOnGlobalLayoutListener(this);

     if(mWidth == 0) mWidth = getWidth() - getPaddingLeft() - getPaddingRight();

     if(mHeight == 0) mHeight = getHeight() - getPaddingTop() - getPaddingBottom();

     if(!isQuerying) {

      mQueryHandler.sendEmptyMessage(0);

     }

    }

   });

  } else{

   if(!isQuerying) {

    mQueryHandler.sendEmptyMessage(0);

   }

  }

 }


 /**

  * 显示弹幕,包括动画的执行

  *

  * @param view

  */

 privatevoidshowDanmu(finalView view) {

  isQuerying = true;

  Log.d(TAG, "mWidth:"+ mWidth + " mHeight:"+ mHeight);

  finalLayoutParams lp = newLayoutParams(view.getMeasuredWidth(), view.getMeasuredHeight());

  lp.leftMargin = mWidth;

  if(TopDirectionFixed) {

   lp.gravity = mTopGravity | Gravity.LEFT;

  } else{

   lp.gravity = Gravity.LEFT | Gravity.TOP;

   lp.topMargin = getRandomTopMargin(view);

  }

  view.setLayoutParams(lp);

  view.setTag(lp.topMargin);

  //设置item水平滚动的动画

  ValueAnimator animator = ValueAnimator.ofInt(mWidth, -view.getMeasuredWidth());

  animator.addUpdateListener(newValueAnimator.AnimatorUpdateListener() {

   @Override

   publicvoidonAnimationUpdate(ValueAnimator animation) {

    lp.leftMargin = (int) animation.getAnimatedValue();

    view.setLayoutParams(lp);

   }

  });

  addView(view);//显示弹幕

  animator.setDuration(DEFAULT_ANIM_DURATION);

  animator.setInterpolator(newLinearInterpolator());

  animator.start();//开启动画

  animator.addListener(newAnimatorListenerAdapter() {

   @Override

   publicvoidonAnimationEnd(Animator animation) {

    view.clearAnimation();

    existMarginValues.remove(view.getTag());//移除已使用过的顶部边距

    removeView(view);//移除弹幕

    animation.cancel();

   }

  });

 }


 //记录当前仍在显示状态的弹幕的垂直方向位置(避免重复)

 privateSet<Integer> existMarginValues = newHashSet<>();

 privateintlinesCount;

 privateintrange = 10;


 privateintgetRandomTopMargin(View view) {

  //计算可用的行数

  linesCount = mHeight / view.getMeasuredHeight();

  if(linesCount <= 1) {

   linesCount = 1;

  }

  Log.d(TAG, "linesCount:"+ linesCount);

  //检查重叠

  while(true) {

   intrandomIndex = (int) (Math.random() * linesCount);

   intmarginValue = randomIndex * (mHeight / linesCount);

   //边界检查

   if(marginValue > mHeight - view.getMeasuredHeight()) {

    marginValue = mHeight - view.getMeasuredHeight() - range;

   }

   if(marginValue == 0) {

    marginValue = range;

   }

   if(!existMarginValues.contains(marginValue)) {

    existMarginValues.add(marginValue);

    Log.d(TAG, "marginValue:"+ marginValue);

    returnmarginValue;

   }

  }

 }

}

弹幕实体类:

/**

 * Created by dell on 2016/9/28.

 */

publicclassDanmu {

 privateString headerUrl;//头像

 privateString userName;//昵称

 privateString info;//信息


 publicString getHeaderUrl() {

  returnheaderUrl;

 }


 publicvoidsetHeaderUrl(String headerUrl) {

  this.headerUrl = headerUrl;

 }


 publicString getUserName() {

  returnuserName;

 }


 publicvoidsetUserName(String userName) {

  this.userName = userName;

 }


 publicString getInfo() {

  returninfo;

 }


 publicvoidsetInfo(String info) {

  this.info = info;

 }

}


测试类,MainActivity


publicclassMainActivity extendsAppCompatActivity {

 DanmuView mDanmuView;

 EditText mMsgEdt;

 Button mSendBtn;

 Handler mDanmuAddHandler;

 booleancontinueAdd;

 intcounter;


 @Override

 protectedvoidonResume() {

  super.onResume();

  mDanmuView.startPlay(true);//true表示弹幕的垂直方向是固定的,false则随机

  continueAdd = true;

  mDanmuAddHandler.sendEmptyMessageDelayed(0, 6000);

 }


 @Override

 protectedvoidonPause() {

  super.onPause();

  continueAdd = false;

  mDanmuAddHandler.removeMessages(0);

 }


 @Override

 protectedvoidonCreate(Bundle savedInstanceState) {

  super.onCreate(savedInstanceState);

  setContentView(R.layout.activity_main);

  initView();

  initData();

  initListener();

 }


 privatevoidinitView() {

  mDanmuView = (DanmuView) findViewById(R.id.danmuView);

  mMsgEdt = (EditText) findViewById(R.id.edt_msg);

  mSendBtn = (Button) findViewById(R.id.btn_send);

 }


 privatevoidinitData() {

  List<Danmu> danmuList = newArrayList<>();

  for(inti = 0; i < 3; i++) {

   Danmu danmu = newDanmu();

   danmu.setHeaderUrl("http://tupian.qqjay.com/tou3/2016/0725/cb00091099ffbf09f4861f2bbb5dd993.jpg");

   danmu.setUserName("Mr.chen"+ i);

   danmu.setInfo("我是弹幕啊,不要问我为什么不可以那么长!!!");

   danmuList.add(danmu);

  }

  mDanmuView.add(danmuList);


  //下面是模拟每秒添加一个弹幕的过程

  HandlerThread ht = newHandlerThread("send danmu");

  ht.start();

  mDanmuAddHandler = newHandler(ht.getLooper()) {

   @Override

   publicvoidhandleMessage(Message msg) {

    runOnUiThread(newRunnable() {

     @Override

     publicvoidrun() {

      Danmu danmu = newDanmu();

      danmu.setHeaderUrl("http://tupian.qqjay.com/tou3/2016/0803/87a8b262a5edeff0e11f5f0ba24fb22f.jpg");

      danmu.setUserName("Mr.new"+ (counter++));

      danmu.setInfo("新的弹幕啊!!!新的弹幕啊!!!新的弹幕啊!!!新的弹幕啊!!!");

      mDanmuView.add(danmu);

     }

    });

    //继续添加

    if(continueAdd) {

     sendEmptyMessageDelayed(0, 1000);

    }

   }

  };

 }


 privatevoidinitListener() {

  //手动添加

  mSendBtn.setOnClickListener(newView.OnClickListener() {

   @Override

   publicvoidonClick(View v) {

    String msg = mMsgEdt.getText().toString().trim();

    if(TextUtils.isEmpty(msg)) {

     Toast.makeText(MainActivity.this, "亲,你想发送什么啊?", Toast.LENGTH_SHORT).show();

     return;

    }

    mMsgEdt.setText("");

    Danmu danmu = newDanmu();

    danmu.setHeaderUrl("http://img0.imgtn.bdimg.com/it/u=2198087564,4037394230&fm=11&gp=0.jpg");

    danmu.setUserName("I'am good man");

    danmu.setInfo("我是新人:"+ msg);

    mDanmuView.add(danmu);

   }

  });

 }

}

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

推荐阅读更多精彩内容