Android悬浮窗使用小结
16lz
2022-05-11
Android的窗口体系中,WindowManager占有非常重要的地位,它封装了添加、移除、更新窗口的方法,它是Activity、View的更加底层的管理类,使用WindowManager的其中一个例子就是制作悬浮窗或悬浮球之类的悬浮组件,这种悬浮组件不依赖某个Activity,它可以在任何界面显示(只要你愿意)。
这篇文章将对如何使用悬浮球做简单总结,即使在android6.0下(android6.0使用动态权限管理),它也可以正常工作。1.使用ButterKnife
ButterKnife可以方便的获取到xml中定义的view的实例,比findViewById方便多了,使用ButterKnife非常简单,可以总结为3步吧:
- 添加对应依赖
compile 'com.jakewharton:butterknife:7.0.0'
- 声明id与对应的View
@Bind(R.id.start) Button start; @Bind(R.id.stop) Button stop; @Bind(R.id.bind) Button bind; @Bind(R.id.unbind) Button unbind;
- 获取View
在onCreate中调用它就可以。通过这句调用,start,stop,bind,unbind几个Button都被实例化了。ButterKnife.bind(this);
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); } } };
这样就不会有这个问题了。 更多相关文章
- android OTG (USB读写,U盘读写)最全使用相关总结
- adb使用-详细教程(Awesome Adb)
- 使用Android(安卓)Studio搭建Android集成开发环境
- Android多进程使用及其带来的问题
- Android(安卓)各大网络请求库的比较及实战
- Android中程序与Service交互的方式(三)-总结
- 箭头函数的基础使用
- NPM 和webpack 的基础使用
- Python list sort方法的具体使用