Android的窗口体系中,WindowManager占有非常重要的地位,它封装了添加、移除、更新窗口的方法,它是Activity、View的更加底层的管理类,使用WindowManager的其中一个例子就是制作悬浮窗或悬浮球之类的悬浮组件,这种悬浮组件不依赖某个Activity,它可以在任何界面显示(只要你愿意)。

   这篇文章将对如何使用悬浮球做简单总结,即使在android6.0下(android6.0使用动态权限管理),它也可以正常工作。

1.使用ButterKnife

ButterKnife可以方便的获取到xml中定义的view的实例,比findViewById方便多了,使用ButterKnife非常简单,可以总结为3步吧:

  1. 添加对应依赖
  1. compile 'com.jakewharton:butterknife:7.0.0'

  1. 声明id与对应的View
  1.     @Bind(R.id.start) Button start;    @Bind(R.id.stop) Button stop;    @Bind(R.id.bind) Button bind;    @Bind(R.id.unbind) Button unbind;
     
     

  1. 获取View
  1. ButterKnife.bind(this);
    在onCreate中调用它就可以。通过这句调用,start,stop,bind,unbind几个Button都被实例化了。

2.使用Service

我希望悬浮窗是在Service中被显示出来的,并且它的管理也在Service中实现。Service有两种启动方式,对应了不同的用途,使用bindService方式启动的Service可以过得一个Ibinder的实例,使用这个实例可以操作Service的所有Public方法,一般用于Activity和Service交互比较频繁的场合下,所以我们这里使用startService启动Service比较合理。 分析清楚以后,开始实现代码:

2.1Service

public class FlowWindowService extends Service {    private final String TAG = "FlowWindowService";    @Override    public void onCreate() {        super.onCreate();        Log.d(TAG,"onCreate");    }    @Nullable    @Override    public IBinder onBind(Intent intent) {        Log.d(TAG,"onBind");        return null;    }    @Override    public int onStartCommand(Intent intent, int flags, int startId) {        Log.d(TAG,"onStartCommand");        showFlowWindow();        return super.onStartCommand(intent, flags, startId);    }    @Override    public void onDestroy() {        super.onDestroy();        Log.d(TAG,"onDestroy");    }    public void showFlowWindow(){        Log.v(TAG,"showFlowWindow");        WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);        Button button = new Button(getApplicationContext());        button.setText("flow");        button.setBackgroundColor(Color.RED);        button.setWidth(100);        button.setHeight(100);        WindowManager.LayoutParams params = new WindowManager.LayoutParams();        params.type = WindowManager.LayoutParams.TYPE_PHONE;        params.format = PixelFormat.RGBA_8888;        params.gravity = Gravity.LEFT | Gravity.TOP;        params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;        params.width = 100;        params.height = 100;        params.x = 300;        params.y = 300;        windowManager.addView(button, params);    }}

这里一次性把整个Service都贴出来了。这段代码在Service的onStartCommand方法中创建了悬浮窗,创建悬浮窗非常简单,使用WindowManager的addView添加一个View就好了,在Android6.0之前,是这样的,Android6.0使用动态权限,会有点不同,这里先使用21版本的sdk避免android6.0动态权限的问题,一切测试OK了再使用Android6.0的动态权限获取响应权限。 悬浮窗的参数设置中,type是个比较重要的参数,它决定了你悬浮窗的优先级 服务不要忘记在Manifest中声明:

2.2Activity中启动Service

Activity非常简单,它里面只有四个Button:
<?xml version="1.0" encoding="utf-8"?>    
MainActivity中使用ButterKnife实例化四个Button,并设置触摸事件监听器:
public class MainActivity extends AppCompatActivity {    @Bind(R.id.start) Button start;    @Bind(R.id.stop) Button stop;    @Bind(R.id.bind) Button bind;    @Bind(R.id.unbind) Button unbind;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        ButterKnife.bind(this);        start.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                    startService(new Intent(MainActivity.this,FlowWindowService.class));            }        });        stop.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                stopService(new Intent(MainActivity.this,FlowWindowService.class));        }        });        bind.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                bindService(new Intent(MainActivity.this,FlowWindowService.class),connectionService, Context.BIND_AUTO_CREATE);            }        });        unbind.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                unbindService(connectionService);            }        });    }    ServiceConnection connectionService = new ServiceConnection() {        @Override        public void onServiceConnected(ComponentName name, IBinder service) {        }        @Override        public void onServiceDisconnected(ComponentName name) {        }    };

bind和unbind按钮知识我测试bindService和startService之间的差别用的,不用理会。最后声明一个权限:

至此,界面如下:

点击start,启动服务:
悬浮窗出现。退出应用后它还在:

退出应用后悬浮窗还在是因为我们没有在Service退出的时候移除悬浮窗,在Service的onDestroy中移除即可:
    @Override    public void onDestroy() {        super.onDestroy();        Log.d(TAG,"onDestroy");        windowManager.removeView(button);    }
这意味着之前的代码需要略作修改,button和windowManager都必须是类中定义的,而不是方法中定义的。

这是Android6.0之前的步骤,Android6.0这么弄就不可以了,原因是Android6.0使用动态权限策略。

2.3动态申请权限

不过也很简单,代码如下:
   private static final int REQUEST_PERMISSION_CODE = 1;    private void requestCameraPermission() {        requestPermissions(new String[]{Manifest.permission.SYSTEM_ALERT_WINDOW}, REQUEST_PERMISSION_CAMERA_CODE);    }    @Override    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {        super.onRequestPermissionsResult(requestCode, permissions, grantResults);        if (requestCode == REQUEST_PERMISSION_CODE) {            int grantResult = grantResults[0];            boolean granted = grantResult == PackageManager.PERMISSION_GRANTED;            Log.i(TAG, "onRequestPermissionsResult granted=" + granted);        }    }
使用requestPermisson方法申请SYSTEM_ALERT_WINDOW权限,REQUEST_PERMISSON_CODE是一个整数,用来表示这次请求,它的值随意。requestPermissions会导致onRequestPermissionsResult方法被回调,在这个方法中我们就可以知道我们是不是申请到了权限。最后, 在MainActivity的onCreate方法中申请权限即可。
    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        ButterKnife.bind(this);        requestPermission();

如果权限申请失败,很有可能是应用程序的权限太低,尝试一下用系统签名文件给它签名,然后就OK了。另外,在Android的模拟器上是可以直接申请权限成功的。

2.4事件监听

我们简单给它添加单击事件处理,每次点击切换一下它的背景色:
    button.setOnClickListener(new View.OnClickListener() {            int count = 0;            @Override            public void onClick(View v) {               if((count++)%2==0){                   button.setBackgroundColor(Color.GREEN);               }else {                   button.setBackgroundColor(Color.RED);               }            }        });

3 移动悬浮框

移动悬浮框也很简单,使用windowManager的updateViewLayout即可,下面的代码制作了一个往复移动的悬浮框:
    Handler myHandler = new Handler();    Runnable runnable = new Runnable() {        boolean direct = true;        @Override        public void run() {            Log.v(TAG,"params.x: "+params.x);            if(direct){                params.x+=10;                if(params.x>800){                    direct = false;                }            }else {                params.x-=10;                if(params.x<100){                    direct = true;                }            }            windowManager.updateViewLayout(button,params);            if(!button.isAttachedToWindow()){                Log.v(TAG,"not attach to window");            }else{                myHandler.postDelayed(this,50);            }        }    };    public void moveFlowButton(){        Log.v(TAG,"moveFlowButton");        myHandler.postDelayed(runnable,500);    }

使用Handler提供定时器,效果如下:

4.解决not attached to window manager

 这个程序运行的时候,点击stop按钮停止服务,会导致上面的问题,完整的log如下:
   Process: com.konka.flowwindowtest, PID: 19478               java.lang.IllegalArgumentException: View=android.widget.Button{2553221 VFED..C.. ......I. 0,0-200,200} not attached to window manager               at android.view.WindowManagerGlobal.findViewLocked(WindowManagerGlobal.java:456)               at android.view.WindowManagerGlobal.updateViewLayout(WindowManagerGlobal.java:368)               at android.view.WindowManagerImpl.updateViewLayout(WindowManagerImpl.java:99)               at com.konka.flowwindowtest.FlowWindowService$1.run(FlowWindowService.java:89)               at android.os.Handler.handleCallback(Handler.java:751)               at android.os.Handler.dispatchMessage(Handler.java:95)               at android.os.Looper.loop(Looper.java:154)               at android.app.ActivityThread.main(ActivityThread.java:5969)               at java.lang.reflect.Method.invoke(Native Method)               at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:801)               at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:691)
这个问题应该是Service已经销毁了,但是button还想要更新位置造成的,所以应该在销毁Service前先取消handler更新button的动画。 首先定义一个线程退出标志:
Boolean destoryStatus = false;
在onDestory中是它为true即可:
        synchronized (this){            destoryStatus = true;        }
run方法如下:
    Runnable runnable = new Runnable() {        boolean direct = true;        @Override        public void run() {            if(!destoryStatus){                Log.v(TAG,"params.x: "+params.x);                if(direct){                    params.x+=10;                    if(params.x>800){                        direct = false;                    }                }else {                    params.x-=10;                    if(params.x<100){                        direct = true;                    }                }                windowManager.updateViewLayout(button,params);                myHandler.postDelayed(this,50);            }        }    };
这样就不会有这个问题了。




更多相关文章

  1. android OTG (USB读写,U盘读写)最全使用相关总结
  2. adb使用-详细教程(Awesome Adb)
  3. 使用Android(安卓)Studio搭建Android集成开发环境
  4. Android多进程使用及其带来的问题
  5. Android(安卓)各大网络请求库的比较及实战
  6. Android中程序与Service交互的方式(三)-总结
  7. 箭头函数的基础使用
  8. NPM 和webpack 的基础使用
  9. Python list sort方法的具体使用

随机推荐

  1. Google Android源代码结构
  2. Android读取/dev/graphics/fb0 屏幕截图
  3. android 上传json串时,json key 被替换为
  4. Android开发之多线程处理、Handler详解
  5. Android(安卓)Studio和SDK的下载安装以及
  6. Android(安卓)Jni调用浅述
  7. Android(安卓)调起本地qq,发起临时会话
  8. android 多屏幕适配 : 第一部分
  9. android 面试(一)
  10. Android--获取SHA1的坑