Android自制精彩弹幕效果

发布时间 - 2026-01-11 02:38:59    点击率:

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

今天要实现的效果如下:

1.弹幕垂直方向固定

2.弹幕垂直方向随机

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

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

/**
 * Created by dell on 2016/9/28.
 */
public class DanmuView extends FrameLayout {
 private static final String TAG = "DanmuView";
 private static final long DEFAULT_ANIM_DURATION = 6000; //默认每个动画的播放时长
 private static final long DEFAULT_QUERY_DURATION = 3000; //遍历弹幕的默认间隔
 private LinkedList<View> mViews = new LinkedList<>();//弹幕队列
 private boolean isQuerying;
 private int mWidth;//弹幕的宽度
 private int mHeight;//弹幕的高度
 private Handler mUIHandler = new Handler();
 private boolean TopDirectionFixed;//弹幕顶部的方向是否固定
 private Handler mQueryHandler;
 private int mTopGravity = Gravity.CENTER_VERTICAL;//顶部方向固定时的默认对齐方式

 public void setHeight(int height) {
 mHeight = height;
 }

 public void setWidth(int width) {
 mWidth = width;
 }

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

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

 public void add(Danmu danmu) {
 addDanmuToQueue(danmu);
 }

 public DanmuView(Context context) {
 this(context, null);
 }

 public DanmuView(Context context, AttributeSet attrs) {
 this(context, attrs, 0);
 }

 public DanmuView(Context context, AttributeSet attrs, int defStyleAttr) {
 super(context, attrs, defStyleAttr);
 HandlerThread thread = new HandlerThread("query");
 thread.start();
 //循环取出弹幕显示
 mQueryHandler = new Handler(thread.getLooper()) {
 @Override
 public void handleMessage(Message msg) {
 final View view = mViews.poll();
 if (null != view) {
 mUIHandler.post(new Runnable() {
 @Override
 public void run() {
 //添加弹幕
 showDanmu(view);
 }
 });
 }
 sendEmptyMessageDelayed(0, DEFAULT_QUERY_DURATION);
 }
 };
 }

 /**
 * 将要展示的弹幕添加到队列中
 *
 * @param danmu
 */
 private void addDanmuToQueue(Danmu danmu) {
 if (null != danmu) {
 final View 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(new CropCircleTransformation(getContext())).into(headerIv);
 view.measure(0, 0);
 //添加弹幕到队列中
 mViews.offerLast(view);
 }
 }

 /**
 * 播放弹幕
 *
 * @param topDirectionFixed 弹幕顶部的方向是否固定
 */
 public void startPlay(boolean topDirectionFixed) {
 this.TopDirectionFixed = topDirectionFixed;
 if (mWidth == 0 || mHeight == 0) {
 getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
 @SuppressLint("NewApi")
 @Override
 public void onGlobalLayout() {
 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
 */
 private void showDanmu(final View view) {
 isQuerying = true;
 Log.d(TAG, "mWidth:" + mWidth + " mHeight:" + mHeight);
 final LayoutParams lp = new LayoutParams(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(new ValueAnimator.AnimatorUpdateListener() {
 @Override
 public void onAnimationUpdate(ValueAnimator animation) {
 lp.leftMargin = (int) animation.getAnimatedValue();
 view.setLayoutParams(lp);
 }
 });
 addView(view);//显示弹幕
 animator.setDuration(DEFAULT_ANIM_DURATION);
 animator.setInterpolator(new LinearInterpolator());
 animator.start();//开启动画
 animator.addListener(new AnimatorListenerAdapter() {
 @Override
 public void onAnimationEnd(Animator animation) {
 view.clearAnimation();
 existMarginValues.remove(view.getTag());//移除已使用过的顶部边距
 removeView(view);//移除弹幕
 animation.cancel();
 }
 });
 }

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

 private int getRandomTopMargin(View view) {
 //计算可用的行数
 linesCount = mHeight / view.getMeasuredHeight();
 if (linesCount <= 1) {
 linesCount = 1;
 }
 Log.d(TAG, "linesCount:" + linesCount);
 //检查重叠
 while (true) {
 int randomIndex = (int) (Math.random() * linesCount);
 int marginValue = 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);
 return marginValue;
 }
 }
 }
}

弹幕实体类:

/**
 * Created by dell on 2016/9/28.
 */
public class Danmu {
 private String headerUrl;//头像
 private String userName;//昵称
 private String info;//信息

 public String getHeaderUrl() {
 return headerUrl;
 }

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

 public String getUserName() {
 return userName;
 }

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

 public String getInfo() {
 return info;
 }

 public void setInfo(String info) {
 this.info = info;
 }
}

测试类,MainActivity

public class MainActivity extends AppCompatActivity {
 DanmuView mDanmuView;
 EditText mMsgEdt;
 Button mSendBtn;
 Handler mDanmuAddHandler;
 boolean continueAdd;
 int counter;

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

 @Override
 protected void onPause() {
 super.onPause();
 continueAdd = false;
 mDanmuAddHandler.removeMessages(0);
 }

 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 initView();
 initData();
 initListener();
 }

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

 private void initData() {
 List<Danmu> danmuList = new ArrayList<>();
 for (int i = 0; i < 3; i++) {
 Danmu danmu = new Danmu();
 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 = new HandlerThread("send danmu");
 ht.start();
 mDanmuAddHandler = new Handler(ht.getLooper()) {
 @Override
 public void handleMessage(Message msg) {
 runOnUiThread(new Runnable() {
 @Override
 public void run() {
 Danmu danmu = new Danmu();
 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);
 }
 }
 };
 }

 private void initListener() {
 //手动添加
 mSendBtn.setOnClickListener(new View.OnClickListener() {
 @Override
 public void onClick(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 = new Danmu();
 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);
 }
 });
 }
}

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


# Android  # 弹幕  # Android实现自定义的弹幕效果  # 很棒的Android弹幕效果实例  # Android实现视频弹幕功能  # Android自定义View实现弹幕效果  # Android简单实现弹幕效果  # Android Flutter实现弹幕效果  # 移除  # 我是  # 自定义  # 是一个  # 成了  # 不可以  # 遍历  # 你想  # 是从  # 写了  # 还需要  # 几点  # 还不错  # 用过  # 心血来潮  # 写过  # 图中  # 时长  # 大家多多 


相关栏目: 【 网站优化151355 】 【 网络推广146373 】 【 网络技术251813 】 【 AI营销90571


相关推荐: canvas 画布在主流浏览器中的尺寸限制详细介绍  Edge浏览器如何截图和滚动截图_微软Edge网页捕获功能使用教程【技巧】  Laravel路由Route怎么设置_Laravel基础路由定义与参数传递规则【详解】  Laravel如何实现用户角色和权限系统_Laravel角色权限管理机制  Laravel如何构建RESTful API_Laravel标准化API接口开发指南  Laravel怎么实现验证码(Captcha)功能  潮流网站制作头像软件下载,适合母子的网名有哪些?  C++时间戳转换成日期时间的步骤和示例代码  详解Nginx + Tomcat 反向代理 负载均衡 集群 部署指南  Laravel如何处理文件上传_Laravel Storage门面实现文件存储与管理  jQuery validate插件功能与用法详解  如何在云主机上快速搭建多站点网站?  Android仿QQ列表左滑删除操作  Claude怎样写结构化提示词_Claude结构化提示词写法【教程】  Laravel中DTO是什么概念_在Laravel项目中使用数据传输对象(DTO)  如何用PHP快速搭建高效网站?分步指南  如何快速搭建高效WAP手机网站?  Laravel N+1查询问题如何解决_Eloquent预加载(Eager Loading)优化数据库查询  网站制作价目表怎么做,珍爱网婚介费用多少?  如何登录建站主机?访问步骤全解析  海南网站制作公司有哪些,海口网是哪家的?  Laravel Blade模板引擎语法_Laravel Blade布局继承用法  Laravel队列任务超时怎么办_Laravel Queue Timeout设置详解  如何快速启动建站代理加盟业务?  如何制作一个表白网站视频,关于勇敢表白的小标题?  小视频制作网站有哪些,有什么看国内小视频的网站,求推荐?  长沙做网站要多少钱,长沙国安网络怎么样?  Laravel模型事件有哪些_Laravel Model Event生命周期详解  非常酷的网站设计制作软件,酷培ai教育官方网站?  ,怎么在广州志愿者网站注册?  如何快速搭建支持数据库操作的智能建站平台?  惠州网站建设制作推广,惠州市华视达文化传媒有限公司怎么样?  北京网页设计制作网站有哪些,继续教育自动播放怎么设置?  Windows10如何更改计算机工作组_Win10系统属性修改Workgroup  javascript基本数据类型及类型检测常用方法小结  专业型网站制作公司有哪些,我设计专业的,谁给推荐几个设计师兼职类的网站?  Laravel怎么集成Vue.js_Laravel Mix配置Vue开发环境  怎样使用JSON进行数据交换_它有什么限制  免费的流程图制作网站有哪些,2025年教师初级职称申报网上流程?  Laravel怎么进行数据库事务处理_Laravel DB Facade事务操作确保数据一致性  Laravel如何使用Gate和Policy进行授权?(权限控制)  个人网站制作流程图片大全,个人网站如何注销?  猎豹浏览器开发者工具怎么打开 猎豹浏览器F12调试工具使用【前端必备】  如何用西部建站助手快速创建专业网站?  phpredis提高消息队列的实时性方法(推荐)  香港服务器网站生成指南:免费资源整合与高速稳定配置方案  高性能网站服务器部署指南:稳定运行与安全配置优化方案  如何快速使用云服务器搭建个人网站?  Python正则表达式进阶教程_复杂匹配与分组替换解析  如何获取PHP WAP自助建站系统源码?