每次看电视剧的时候,总觉得弹幕比电视剧内容有趣,于是很想在手机上面实现弹幕,显示应用程序所接受的消息。前段时间上网查了资料,几乎找不到相关的Demo,而自己也不咋会,所以就耽搁了。然而,拥有一个大神朋友真的是一件很爽的事情,他在很早以前就实现了这个功能,然后我就移花接木了(已经获得大神授权),嘿嘿。当然我也添加了自己的想法进去。具体思路就是监听程序消息,通过浮动窗口显示。好了,不多说废话了,我先来分析一下具体的实现过程。
1、继承NotificationListenerService服务,重写内部的一个方法:
@Override
public void onNotificationPosted(StatusBarNotification sbn) {}
通过StatusBarNotification 对象获取应用程序接受到的消息
NotificationListenerService详细解说可以访问我的博客http://blog.csdn.net/u012925323/article/details/48103197
2、自写View,并且将View添加到浮动窗口上面去,适配手机屏幕,获取弹幕能够显示的最大行数,用一个整形数组对每行设置权重(权重最小的优先显示),利用valueAnimator类对单个弹幕的位置进行设置,然后在View上进行实时更新,最后自写接口,当弹幕移除屏幕后再移除View。
3、针对特定程序进行监听,过滤掉多余的消息。
4、针对选中的监听程序进行消息过滤(这部分我在本博客中没有实现,可以自己上网搜,就是将关注的内容存入数据库,每次对NotificationListenerService接收到的消息进行判断,如果你想要具体代码,就留下的的qq邮箱)。
5、不要被代码量吓到了,关键地方我都有注释的,只要仔细读,很容易理解。
这是主要的代码类:
1、AddBarrageActivity类
package com.tielizi.mynotification;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.view.View;
import android.widget.LinearLayout;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class AddBarrageActivity extends Activity implements View.OnClickListener{
private LinearLayout addBarrageLayout,open_barrage;
private SharedPreferences spf;//存储本地程序
private Set<String> stringSet;//SharedPreferences中的存储的监听程序集合
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_add_barrage);
startService(new Intent(AddBarrageActivity.this, MyNotificationService.class));
spf = PreferenceManager.getDefaultSharedPreferences(this);
addBarrageLayout = (LinearLayout) findViewById(R.id.add_barrage_layout);
open_barrage = (LinearLayout) findViewById(R.id.open_barage);
open_barrage.setOnClickListener(this);
addBarrageLayout.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.open_barage://打开手机上的服务,这个必须手动打开,不然app将接受不到消息
Intent intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS);
startActivity(intent);
break;
case R.id.add_barrage_layout://添加监听的程序
List<Map<String,Object>> list_map = new ArrayList<Map<String,Object>>();
Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
List<ResolveInfo> resolveInfos = this.getPackageManager().queryIntentActivities(mainIntent, 0);
for(ResolveInfo resolveInfo :resolveInfos){
Map<String, Object> map = new HashMap<String, Object>();
map.put("name", resolveInfo.loadLabel(this.getPackageManager()).toString());
map.put("package", resolveInfo.activityInfo.packageName);
System.err.println(resolveInfo.activityInfo.packageName);
list_map.add(map);
}
stringSet = spf.getStringSet("barrage_listener_list",new HashSet<String>());
String[] title_list = new String[list_map.size()];
final String[] package_list = new String[list_map.size()];
boolean[] check_list = new boolean[list_map.size()];
int i=0;
String package_name;
for(Map<String,Object> each:list_map){
title_list[i] = (String)each.get("name");
package_name = (String)each.get("package");
package_list[i] =package_name;
if(stringSet.contains(package_name)){
check_list[i] = true;
}else{
check_list[i] = false;
}
i++;
}
new AlertDialog.Builder(this)
.setTitle("弹幕应用列表")
.setMultiChoiceItems(title_list, check_list,new DialogInterface.OnMultiChoiceClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i, boolean b) {
if (b == true) {
if (stringSet != null) {
stringSet.add(package_list[i]);
}
} else {
if (stringSet != null) {
stringSet.remove(package_list[i]);
}
}
}
})
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
spf.edit().putStringSet("barrage_listener_list", stringSet).commit();
AddBarrageActivity.this.startService(new Intent(AddBarrageActivity.this,MyNotificationService.class));
}
})
.show();
break;
}
}
}
2、MyNotificationService 类(这个非常重要)
package com.tielizi.mynotification;
import android.app.Service;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.preference.PreferenceManager;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import android.view.Gravity;
import android.view.WindowManager;
import android.widget.Toast;
import java.util.HashSet;
import java.util.Set;
public class MyNotificationService extends NotificationListenerService {
private SharedPreferences spf;//存储本地程序
private Set<String> stringSet;//SharedPreferences中的存储的监听程序集合
private final int FIRST_POST = 0;//弹幕View添加到浮动窗口后第一次获取消息
private final int POST = 1;//弹幕View添加到浮动窗口后第二次获取消息
private final int REMOVE = 3;//移除弹幕View
MyBarrageView myBarrageView;//自定义的View
private WindowManager.LayoutParams layoutParams;
private WindowManager windowManager;
private boolean isFirstAddBarrageView;//对是否第一次添加弹幕进行标记
private MyHandler handler = new MyHandler();//在主线程中更新UI
private SQLiteDatabase dbRead;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i("px", "Srevice is open" + "-----");
spf = PreferenceManager.getDefaultSharedPreferences(this);
stringSet = spf.getStringSet("barrage_listener_list",new HashSet<String>());
isFirstAddBarrageView = true;
windowManager = (WindowManager)getSystemService(WINDOW_SERVICE);
layoutParams = new WindowManager.LayoutParams();
layoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
layoutParams.format = 1;
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE|
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN|
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
// layoutParams.flags = 40;
layoutParams.y = 0;
layoutParams.gravity = Gravity.START |
Gravity.TOP;
layoutParams.height = WindowManager.LayoutParams.MATCH_PARENT;
layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
if(myBarrageView == null){
myBarrageView = new MyBarrageView(MyNotificationService.this);//注册监听器
}
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onNotificationPosted(StatusBarNotification sbn) {//获取程序消息,这里必须用handler对UI进行更新,不然会报异常
String messageInfo = sbn.getNotification().tickerText.toString();
if(stringSet.contains(sbn.getPackageName())){
Message message = handler.obtainMessage();
if(isFirstAddBarrageView){
message.what = FIRST_POST;
message.obj = messageInfo;
}else{
message.what = POST;
message.obj = messageInfo;
}
handler.sendMessage(message);
}
}
class MyHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case FIRST_POST:
windowManager.addView(myBarrageView, layoutParams);
Message message = handler.obtainMessage();
message.what = POST;
message.obj = msg.obj;
handler.sendMessage(message);
isFirstAddBarrageView = false;
Toast.makeText(MyNotificationService.this, (String) msg.obj, Toast.LENGTH_SHORT).show();
break;
case POST:
final BarrageItemBean barrageItemBean = new BarrageItemBean();
barrageItemBean.setText((String)msg.obj);
myBarrageView.post(new Runnable() {
@Override
public void run() {
myBarrageView.addBarrageItem(barrageItemBean);
}
});
Toast.makeText(MyNotificationService.this, (String) msg.obj, Toast.LENGTH_SHORT).show();
break;
case REMOVE:
windowManager.removeView(myBarrageView);
isFirstAddBarrageView = true;
Toast.makeText(MyNotificationService.this, "移除BarrageView", Toast.LENGTH_SHORT).show();
break;
}
}
}
}
3、MyBarrageView类
package com.tielizi.mynotification;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* Created by Administrator on 2015/8/21.
*/
public class MyBarrageView extends View {
private int width;
private MyBarrageRemoveListener myBarrageRemoveListener;
private Paint paint;
private List<BarrageItemBean> barrageItemBeans;
private int separateScreenHeightLine;
private int[] barrageLines;
private int line_height;
private Rect mBound;//框住文字
private static final String[] COLORS= new String[]{//字体颜色数组
"#ee339900",
"#ee990000",
"#eeFF3366",
"#ee3366FF",
"#ee00cc33",
"#eeF9F9F9",
"#eeF9F9F9",
"#eeF9F9F9",
"#eeCCCCCC",
"#eeFF0099",
"#eeFF3030",
"#eeffcc00",
"#eeFF9900",
"#ee00bbdd",
"#ee00bbdd",
"#eeFF3333",
"#ee33FF33",
"#ee3333FF",
};
public MyBarrageView(Context context) {
super(context);
init();
}
public MyBarrageView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public MyBarrageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
// public MyLockView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
// super(context, attrs, defStyleAttr, defStyleRes);
// init();
// }要求api为21时才有这个构造函数,当前api是19
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {//当View实例化之后才调用这个方法测量手机屏幕宽高
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//可用行数
width = getMeasuredWidth();
separateScreenHeightLine = getMeasuredHeight() / line_height;
barrageLines = new int[separateScreenHeightLine];
}
@Override
protected void onDraw(Canvas canvas) {//在画布上更新
int barrageItemBeansSize = barrageItemBeans.size();
for(int i = 0; i < barrageItemBeansSize; i++){
BarrageItemBean barrageItemBean = barrageItemBeans.get(i);
paint.setColor(barrageItemBean.getColor());
canvas.drawText(barrageItemBean.getText(),barrageItemBean.getX(),barrageItemBean.getY(),paint);
}
}
public void setMyBarrageRemoveListener(MyBarrageRemoveListener myBarrageRemoveListener){//z注册监听器
this.myBarrageRemoveListener = myBarrageRemoveListener;
}
public void addBarrageItem(final BarrageItemBean barrageItemBean){//添加弹幕,并对弹幕Bean属性进行设置
barrageItemBean.setX(getMeasuredWidth());
paint.getTextBounds(barrageItemBean.getText(), 0, barrageItemBean.getText().length(), mBound);
barrageItemBean.setTextLength(mBound.width());
barrageItemBean.setColor(Color.parseColor(COLORS[(new Random()).nextInt(COLORS.length)]));
final int line = getBarrageShowLine();
Log.i("px line", line + "");
barrageItemBean.setY(line_height*line);
barrageItemBean.setBarrageLine(line);
barrageLines[line-1]++;//设置显示权重
barrageItemBeans.add(barrageItemBean);
width = barrageItemBean.getX();
ValueAnimator valueAnimator = ValueAnimator.ofInt(width, -barrageItemBean.getTextLength());//设置数在区间上逐渐变化
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {//这个不会在组件上添加动态特效,只是获取当前时刻区间内变化的数
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
int x = (Integer) valueAnimator.getAnimatedValue();
barrageItemBean.setX(x);
invalidate();
}
});
valueAnimator.setDuration(4000+barrageItemBean.getTextLength()*2);//针对弹幕长短设置区间内数逐渐变化的时间
valueAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator arg0) {
// TODO Auto-generated method stub
}
@Override
public void onAnimationRepeat(Animator arg0) {
// TODO Auto-generated method stub
}
@Override
public void onAnimationEnd(Animator arg0) {
barrageItemBeans.remove(barrageItemBean);
barrageLines[line-1]--;//权重减 1
if(barrageItemBeans.size()==0){
if(myBarrageRemoveListener != null){
myBarrageRemoveListener.removeFloatWindow();
}
}
}
@Override
public void onAnimationCancel(Animator arg0) {
// TODO Auto-generated method stub
}
});
valueAnimator.start();
}
private void init(){
paint = new Paint();
barrageItemBeans = new ArrayList<BarrageItemBean>();
// paint.setTextSize(50);
paint.setAntiAlias(true);
paint.setAlpha(50);
paint.setShadowLayer(2, 2, 2, Color.DKGRAY);
mBound = new Rect();
//将整形转化为sp
int textSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 28, getResources().getDisplayMetrics());
paint.setTextSize(textSize);
paint.getTextBounds("获取已经设置字体大小的paint高度存入Rect中", 0, 25, mBound);
line_height = mBound.height();
invalidate();
}
private int getBarrageShowLine(){//获取当前最小权重
int length = barrageLines.length;
int min = barrageLines[0];//获取最小标记数
int minIndex = 0;//最小标记行数
for(int i = 1; i < length; i++){
if(min > barrageLines[i]){
min = barrageLines[i];
minIndex = i;
}
}
return minIndex+1;
}
}
以上就是弹幕的主要代码。
这里要特变注意我们的配置文件AndroidManifest.xml(特别是权限的添加):
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.tielizi.mynotification" >
<uses-permission android:name="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".AddBarrageActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".MyNotificationService"
android:label="MyNotificationService"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
</application>
</manifest>
好了现在我们上效果图: