android 弹幕动画,Android自制精彩弹幕效果

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

今天要实现的效果如下:

1.弹幕垂直方向固定

338230754e4a3a319b496129deb7582d.gif

2.弹幕垂直方向随机

2da1a96b437090bacc22cd873c3de445.gif

上面效果图中白色的背景就是弹幕本身,是一个自定义的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 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 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 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 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);

}

});

}

}

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值