每次看电视剧的时候,总觉得弹幕比电视剧内容有趣,于是很想在手机上面实现弹幕,显示应用程序所接受的消息。前段时间上网查了资料,几乎找不到相关的Demo,而自己也不咋会,所以就耽搁了。然而,拥有一个大神朋友真的是一件很爽的事情,他在很早以前就实现了这个功能,然后我就移花接木了(已经获得大神授权),嘿嘿。当然我也添加了自己的想法进去。具体思路就是监听程序消息,通过浮动窗口显示。好了,不多说废话了,我先来分析一下具体的实现过程。

1、继承NotificationListenerService服务,重写内部的一个方法:

@Override
  public void onNotificationPosted(StatusBarNotification sbn) {}

通过StatusBarNotification 对象获取应用程序接受到的消息

NotificationListenerService详细解说可以访问我的博客

2、自写View,并且将View添加到浮动窗口上面去,适配手机屏幕,获取弹幕能够显示的最大行数,用一个整形数组对每行设置权重(权重最小的优先显示),利用valueAnimator类对单个弹幕的位置进行设置,然后在View上进行实时更新,最后自写接口,当弹幕移除屏幕后再移除View。

3、针对特定程序进行监听,过滤掉多余的消息。

4、针对选中的监听程序进行消息过滤(这部分我在本博客中没有实现,可以自己上网搜,就是将关注的内容存入数据库,每次对NotificationListenerService接收到的消息进行判断,如果你想要具体代码,就留下的的qq邮箱)。

5、不要被代码量吓到了,关键地方我都有注释的,只要仔细读,很容易理解。

这是主要的代码类:

Android弹幕的实现上下 手机弹幕下_Android弹幕的实现上下

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>

好了现在我们上效果图:

Android弹幕的实现上下 手机弹幕下_应用_02