Android自制精彩弹幕效果
作者:mChenys 发布时间:2023-12-27 04:57:43
标签:Android,弹幕
好久没有写过文章,最近发现直播特别的火,很多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);
}
});
}
}
0
投稿
猜你喜欢
- java生成json时产生栈溢出错误环境java + hibernate +html本来,java中使用json事件很正常的事,但小心有的地
- 熟悉Eclipse的都知道Eclipse经常性的会出现一些莫名其妙的问题,有时候运行的好好的突然重启一下项目就莫名的报错,所以经常会用到cl
- 1.多节点无缝切换问题分布式节点中的服务宕机或者重启不影响客户端使用分布式节点中的服务宕机重启不影响业务服务内部通信如果在某个分布式系统中想
- 简介Java 在 1.5 引入了泛型机制,泛型本质是参数化类型,也就是说变量的类型是一个参数,在使用时再指定为具体类型。泛型可以用于类、接口
- 仿QQ侧滑删除效果图1.自定义listviewpublic class DragDelListView extends ListView {
- JNI简介JNI是Java Native Interface的缩写,它提供了若干的API实现了Java和其他语言的通信(主要是C&C
- 服装价格变动,触发淘宝发布活动和消费者购买衣服事件流public class EventStandard { &n
- 情况一:配置文件,无法被导出或者生效修改前:修改后:究其原因,这是由于Maven的约定大于配置,导致我们写的配置文件,无法被导出或者生效的问
- 本文实例为大家分享了C#点餐系统的具体代码,供大家参考,具体内容如下using System;using System.Collection
- 本文讲述了Java实现画线、矩形、椭圆、字符串功能的实例代码。分享给大家供大家参考,具体如下:import java.awt.Frame;
- PowerMockito 测试静态方法假如有下面一个类DemoStatic,它里面定义了各种静态方法,这些静态方法可能是一些Utilitie
- Java提示缺少返回值语句怎么办?这里我们给大家提供具体的解决方法。首先,以下面的程序为例,会看到在控制台有:错误:缺少返回语句的提示。找到
- 很多程序员都以自己写的代码的行数作为自己程序员阅历的一个标志,如何统计呢,以下是具体内容。小编,已经快学了两年编程了。昨天突发奇想,想统计下
- 建立Android项目,如果会的话特别简单,不会的话让自己去琢磨也需要一定的时间!小编之后将自己学习Android的经验给大家分享出来!1、
- 前言之前其实有从鸿洋的文章有了解过AS的模板开发,一直想做一些自己经常使用的模板,以减少重复代码工作,但是发现太费劲了,所以一直搁置。然后昨
- 一、雪花算法datacenterId重复问题华为云的服务器的/etc/hosts中都会生成一条 127.0.1.1 hostname的记录
- 线程概念进程:启动一个应用程序就叫一个进程。 接着又启动一个应用程序,这叫两个进程。每个进程都有一个独立的内存空间;进程也是程序的一次执行过
- Android安全加密专题文章索引Android安全加密:对称加密Android安全加密:非对称加密Android安全加密:消息摘要Mess
- 给对象中的包装类设置默认值处理方法如下主要适用于,对象中使用了包装类,但是不能给null需要有默认值的情况/**
- 一、什么是Spring Cloud?SpringCloud 对常见的分布式系统模式提供了简单易用的编程模型,帮助开发者构建弹性、可靠、协调的