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之间的差别用的,不用理会。最后声明一个权限:

至此,界面如下: Android悬浮窗使用小结_第1张图片

点击start,启动服务: Android悬浮窗使用小结_第2张图片
悬浮窗出现。退出应用后它还在: Android悬浮窗使用小结_第3张图片

退出应用后悬浮窗还在是因为我们没有在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提供定时器,效果如下: Android悬浮窗使用小结_第4张图片

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. androidManifest.xml之uses-permission(权限-安全控制)
  2. Android手机访问Django测试服务器方法
  3. android Map 使用方法
  4. Android实现屏幕旋转方法总结
  5. android: 静态XML和动态加载XML混合使用,以及重写Layout控件
  6. #菜鸟之旅#Android Studio开发JNI工程——Native调用Java方法获
  7. Android 如何在xmL 里面动态设置padding
  8. Android 使用Theme实现动态切换主题详细教程

随机推荐

  1. Android使用Handler实时更新UI
  2. 浅谈android的selector 背景选择器
  3. x86平台编译Android
  4. Android(安卓)Call Log and SMS Delete
  5. android实现富文本
  6. Android中的数据绑定框架DataBinding(对
  7. Android 特殊界面效果之——透明界面
  8. Android(安卓)-- 保存文件
  9. 计算器——第一个Android小项目
  10. Android 使用Lottie的三个小技巧