Android自制精彩彈幕效果
好久沒有寫過文章,最近發(fā)現(xiàn)直播特別的火,很多app都集成了直播的功能,發(fā)現(xiàn)有些直播是帶有彈幕的,效果還不錯(cuò),今天心血來潮,特地寫了篇制作彈幕的文章.
今天要實(shí)現(xiàn)的效果如下:
1.彈幕垂直方向固定

2.彈幕垂直方向隨機(jī)

上面效果圖中白色的背景就是彈幕本身,是一個(gè)自定義的FrameLayout,我這里是為了更好的展示彈幕的位置才設(shè)置成了白色,當(dāng)然如果是疊加在VideoView上的話,就需要設(shè)置成透明色了.
制作彈幕需要考慮以下幾點(diǎn)問題:
1.彈幕的大小可以隨意調(diào)整
2.彈幕內(nèi)移動(dòng)的item(或者稱字幕)出現(xiàn)的位置,水平方向是從屏幕右邊移動(dòng)到屏幕左邊,垂直方向是不能超出彈幕本身的高度的.
3.字幕移除屏幕后,需要將對(duì)應(yīng)item(字幕)從其父容器(彈幕)中移除.
4.如果字幕出現(xiàn)的垂直方向的高度是隨機(jī)的,那么還需要避免字幕重疊的情況.
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; //默認(rèn)每個(gè)動(dòng)畫的播放時(shí)長(zhǎng)
private static final long DEFAULT_QUERY_DURATION = 3000; //遍歷彈幕的默認(rèn)間隔
private LinkedList<View> mViews = new LinkedList<>();//彈幕隊(duì)列
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;//頂部方向固定時(shí)的默認(rèn)對(duì)齊方式
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();
//循環(huán)取出彈幕顯示
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);
}
};
}
/**
* 將要展示的彈幕添加到隊(duì)列中
*
* @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);
//添加彈幕到隊(duì)列中
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);
}
}
}
/**
* 顯示彈幕,包括動(dòng)畫的執(zhí)行
*
* @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);
//設(shè)置item水平滾動(dòng)的動(dòng)畫
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();//開啟動(dòng)畫
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.clearAnimation();
existMarginValues.remove(view.getTag());//移除已使用過的頂部邊距
removeView(view);//移除彈幕
animation.cancel();
}
});
}
//記錄當(dāng)前仍在顯示狀態(tài)的彈幕的垂直方向位置(避免重復(fù))
private Set<Integer> existMarginValues = new HashSet<>();
private int linesCount;
private int range = 10;
private int getRandomTopMargin(View view) {
//計(jì)算可用的行數(shù)
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;
}
}
}
}
彈幕實(shí)體類:
/**
* 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;
}
}
測(cè)試類,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則隨機(jī)
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("我是彈幕啊,不要問我為什么不可以那么長(zhǎng)!!!");
danmuList.add(danmu);
}
mDanmuView.add(danmuList);
//下面是模擬每秒添加一個(gè)彈幕的過程
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);
}
});
//繼續(xù)添加
if (continueAdd) {
sendEmptyMessageDelayed(0, 1000);
}
}
};
}
private void initListener() {
//手動(dòng)添加
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, "親,你想發(fā)送什么啊?", 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);
}
});
}
}
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android編程自定義Notification實(shí)例分析
這篇文章主要介紹了Android編程自定義Notification的用法,結(jié)合實(shí)例形式簡(jiǎn)單分析了自定義Notification的具體功能與實(shí)現(xiàn)技巧,需要的朋友可以參考下2015-12-12
Android UI控件RatingBar實(shí)現(xiàn)自定義星星評(píng)分效果
這篇文章主要為大家詳細(xì)介紹了Android UI控件RatingBar實(shí)現(xiàn)自定義星星評(píng)分效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-02-02
Android實(shí)現(xiàn)拍照添加時(shí)間水印
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)拍照添加時(shí)間水印,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
android 解決ViewPager加載大量圖片內(nèi)存溢出問題
本篇文章是介紹 android 解決ViewPager加載大量圖片內(nèi)存溢出問題,并附有代碼實(shí)例,希望能幫到有需要的小伙伴2016-07-07
Android的TextView與Html相結(jié)合的具體方法
Android的TextView與Html相結(jié)合的具體方法,需要的朋友可以參考一下2013-06-06
Android媒體通知欄多系統(tǒng)適配實(shí)例講解
對(duì)于Android來說其中一項(xiàng)很方便的操作便是下拉菜單,下拉菜單欄可以快捷打開某項(xiàng)設(shè)置,這篇文章主要給大家介紹了關(guān)于Android通知欄增加快捷開關(guān)的功能實(shí)現(xiàn),需要的朋友可以參考下2023-04-04

