查看: 3096|回复: 0

[Android教程] Android自制精彩弹幕效果

发表于 2018-1-14 11:37:41

好久没有写过文章,最近发现直播特别的火,很多app都集成了直播的功能,发现有些直播是带有弹幕的,效果还不错,今天心血来潮,特地写了篇制作弹幕的文章.

今天要实现的效果如下:

1.弹幕垂直方向固定

这里写图片描述

2.弹幕垂直方向随机

这里写图片描述

上面效果图中白色的背景就是弹幕本身,是一个自定义的FrameLayout,我这里是为了更好的展示弹幕的位置才设置成了白色,当然如果是叠加在VideoView上的话,就需要设置成透明色了.
制作弹幕需要考虑以下几点问题:
1.弹幕的大小可以随意调整
2.弹幕内移动的item(或者称字幕)出现的位置,水平方向是从屏幕右边移动到屏幕左边,垂直方向是不能超出弹幕本身的高度的.
3.字幕移除屏幕后,需要将对应item(字幕)从其父容器(弹幕)中移除.
4.如果字幕出现的垂直方向的高度是随机的,那么还需要避免字幕重叠的情况.

ok,下面是弹幕自定义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. }
复制代码

弹幕实体类:

  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. }
  27. 测试类,MainActivity
  28. public class MainActivity extends AppCompatActivity {
  29. DanmuView mDanmuView;
  30. EditText mMsgEdt;
  31. Button mSendBtn;
  32. Handler mDanmuAddHandler;
  33. boolean continueAdd;
  34. int counter;
  35. @Override
  36. protected void onResume() {
  37. super.onResume();
  38. mDanmuView.startPlay(true);//true表示弹幕的垂直方向是固定的,false则随机
  39. continueAdd = true;
  40. mDanmuAddHandler.sendEmptyMessageDelayed(0, 6000);
  41. }
  42. @Override
  43. protected void onPause() {
  44. super.onPause();
  45. continueAdd = false;
  46. mDanmuAddHandler.removeMessages(0);
  47. }
  48. @Override
  49. protected void onCreate(Bundle savedInstanceState) {
  50. super.onCreate(savedInstanceState);
  51. setContentView(R.layout.activity_main);
  52. initView();
  53. initData();
  54. initListener();
  55. }
  56. private void initView() {
  57. mDanmuView = (DanmuView) findViewById(R.id.danmuView);
  58. mMsgEdt = (EditText) findViewById(R.id.edt_msg);
  59. mSendBtn = (Button) findViewById(R.id.btn_send);
  60. }
  61. private void initData() {
  62. List<Danmu> danmuList = new ArrayList<>();
  63. for (int i = 0; i < 3; i++) {
  64. Danmu danmu = new Danmu();
  65. danmu.setHeaderUrl("http://tupian.qqjay.com/tou3/2016/0725/cb00091099ffbf09f4861f2bbb5dd993.jpg");
  66. danmu.setUserName("Mr.chen" + i);
  67. danmu.setInfo("我是弹幕啊,不要问我为什么不可以那么长!!!");
  68. danmuList.add(danmu);
  69. }
  70. mDanmuView.add(danmuList);
  71. //下面是模拟每秒添加一个弹幕的过程
  72. HandlerThread ht = new HandlerThread("send danmu");
  73. ht.start();
  74. mDanmuAddHandler = new Handler(ht.getLooper()) {
  75. @Override
  76. public void handleMessage(Message msg) {
  77. runOnUiThread(new Runnable() {
  78. @Override
  79. public void run() {
  80. Danmu danmu = new Danmu();
  81. danmu.setHeaderUrl("http://tupian.qqjay.com/tou3/2016/0803/87a8b262a5edeff0e11f5f0ba24fb22f.jpg");
  82. danmu.setUserName("Mr.new" + (counter++));
  83. danmu.setInfo("新的弹幕啊!!!新的弹幕啊!!!新的弹幕啊!!!新的弹幕啊!!!");
  84. mDanmuView.add(danmu);
  85. }
  86. });
  87. //继续添加
  88. if (continueAdd) {
  89. sendEmptyMessageDelayed(0, 1000);
  90. }
  91. }
  92. };
  93. }
  94. private void initListener() {
  95. //手动添加
  96. mSendBtn.setOnClickListener(new View.OnClickListener() {
  97. @Override
  98. public void onClick(View v) {
  99. String msg = mMsgEdt.getText().toString().trim();
  100. if (TextUtils.isEmpty(msg)) {
  101. Toast.makeText(MainActivity.this, "亲,你想发送什么啊?", Toast.LENGTH_SHORT).show();
  102. return;
  103. }
  104. mMsgEdt.setText("");
  105. Danmu danmu = new Danmu();
  106. danmu.setHeaderUrl("http://img0.imgtn.bdimg.com/it/u=2198087564,4037394230&fm=11&gp=0.jpg");
  107. danmu.setUserName("I'am good man");
  108. danmu.setInfo("我是新人:" + msg);
  109. mDanmuView.add(danmu);
  110. }
  111. });
  112. }
  113. }
复制代码

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持程序员之家。



回复

使用道具 举报