1、WindowManager介绍
WindowManager主要用来管理窗口的一些状态、属性、view增加、删除、更新、窗口顺序、消息收集和处理等。通过Context.getSystemService(Context.WINDOW_SERVICE)的方式可以获得WindowManager的实例.WindowManager继承自ViewManager,里面涉及到窗口管理的三个重要方法,分别是
- addView();
- updateViewLayout();
- removeView();
WindowManager中还有一个重要的静态类LayoutParams。通过它可以设置和获得当前窗口的一些属性。我们先来看看addView()方法,在addView中,会利用LayoutParams获得window的View属性,并为每个window创ViewRoot,ViewRoot是View和WindowManager之间的桥梁,真正把View传递给WindowManager的是通过ViewRoot的setView()方法,ViewRoot实现了View和WindowManager之间的消息传递。在将主窗口添加到WindowManger时,它首先会建立一个代理对象:
wm=(WindowManagerImpl)context.getSystemService(Context.WINDOW_SERVICE)
并且打开会话(IWindowSession),之后Window将通过该会话与WindowManager建立联系。
2、关于android 6.0前后的授权问题。
本文主要是给一个服务添加悬浮窗口,首先判断是否需要权限:
1、6.0前清单文件添加:<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
2、6.0后除清单文件添加上面权限外还得手动为应用添加权限:
(1)在设置->应用->点开应用->配置应用->在其他应用的上层显示 授予权限
(2)在代码中判断是否大于6.0版本,并判断是否有权限,若没有就跳到授权设置里。
(3)、将WindowManager 的类型设置为: params.type = WindowManager.LayoutParams.TYPE_TOAST;则不需要申请权限了
3、关于悬浮窗口的设置和实现以及一些属性设置
(1)、悬浮窗口在某一个activity里显示。
之前做过一个VR相册的项目,要求点击图片时候显示一张图片,为了不创建新的activity和布局文件,只用一个自定义空间来显示,采用了悬浮窗口,
relativeLayout =new RelativeLayout(this);
WindowManager.LayoutParams params = new WindowManager.LayoutParams();
params.width = WindowManager.LayoutParams.MATCH_PARENT;
params.height = WindowManager.LayoutParams.MATCH_PARENT;
params.format = PixelFormat.RGBA_8888;//背景透明
params.gravity = Gravity.LEFT | Gravity.TOP;
relativeLayout.addView(myBallPictureView);
wm.addView(relativeLayout, params);
然而发现了悬浮窗口的2个坑:
android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application,根据报错的原因,是窗口的令牌错误,然后查看我的windowManager的初始化为:
wm = (WindowManager)getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
既然是令牌错误,就发现可能是初始化wm时候发生了错误,后来通过activity和getApplicationContext()的contex是不一样的,一个是整个应用的,一个是此activity的,后来换作activity来获取WM,发现不再报错,且显示很流畅。getApplicationContext()除非添加了
params.type = WindowManager.LayoutParams.TYPE_TOAST;后才不报错,但会一直显示在屏幕上。。其实这里提醒我们能使用activity的context尽量别使用getApplicationContext(),还是有区别的(虽然大部分都时候获取的token其实是一样的)。
<2>、发现弹出了图像后按back键图像消失不了,为什么消失不了?通过log发现,当我按onBackPress()的时候发现,我覆写的onBackPress()方法更本没有执行,就联想到可能是WindowManager.LayoutParams 的Type属性问题了。后来将Type修改为:
params.flags= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
发现可以了。
这里也使得我们在使用悬浮窗口的时候主要的事项:WindowManager.LayoutParams的属性,和context的获取。下面介绍一下WindowManager.LayoutParams的Type和flag2个重要的属性:
Type是添加窗口的类型:窗口以系统的什么类型的窗口显示:可参考:http://realgodo.iteye.com/blog/1780176。用的较多的就是第二部分提到的TYPE_TOAST。此时会显示在屏幕上,
flag窗口的焦点属性:
FLAG_NOT_FOCUSABLE ;此时不会固定在屏幕上,按返回键是有效的。
WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; 此时窗口固定在屏幕上,返回键是无效的。
(2) 悬浮窗口在所有屏幕上显示
具体参看上面的关于Type的博客。
(3),应用内显示悬浮窗口
这里为了方便管理,通常是设置一个service来实现,当应用前台时候调用服务显示窗口,当应用后台时候调用服务显示窗口。
(4)、桌面显示,应用内不现实:这个和上面相反,也可以用服务来实现。
由于时间紧,直接给出(3)代码,其中有注解:
Activity:
package com.example.user.floatingview;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity implements
View.OnClickListener {
private Button btn_show;
private Button btn_hide;
private int floatShow = 0;
private boolean hasGetPermission = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn_show = (Button) findViewById(R.id.btn_show);
btn_hide = (Button) findViewById(R.id.btn_hide);
btn_show.setOnClickListener(this);
btn_hide.setOnClickListener(this);
}
public void onClick(View v)
{
switch (v.getId())
{
case R.id.btn_show:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){//判断api版本,若低于6.0就不需要动态授权检测权限,将canDrawOverlays版本disable掉
hasGetPermission =Settings.canDrawOverlays(this);
if (!hasGetPermission) {
Toast.makeText(this, "当前无权限,请授权!", Toast.LENGTH_SHORT).show();
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, 1024);
}
else {
floatShow = 1;
Toast.makeText(this, "后台显示浮动窗口", Toast.LENGTH_SHORT).show();
}
}
break;
case R.id.btn_hide:
floatShow = -1;
Toast.makeText(this,"已取消后台显示浮动窗口",Toast.LENGTH_SHORT).show();
break;
}
}
@Override
protected void onPause()
{
super.onPause();
if(hasGetPermission) {
if (floatShow == 1) {
Intent show = new Intent(this, TopWindowService.class);
show.putExtra(TopWindowService.OPERATION,
TopWindowService.OPERATION_SHOW);
startService(show);
} else if (floatShow == -1) {
Intent hide = new Intent(this, TopWindowService.class);
hide.putExtra(TopWindowService.OPERATION,
TopWindowService.OPERATION_HIDE);
startService(hide);
}
}
}
@Override
protected void onResume() {
super.onResume();
if(hasGetPermission&&floatShow == 1) {
Intent hide = new Intent(this, TopWindowService.class);
hide.putExtra(TopWindowService.OPERATION,
TopWindowService.OPERATION_HIDE);
startService(hide);
}
}
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == 1024&&(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)){
if(!Settings.canDrawOverlays(this)) {
Toast.makeText(this, "权限授予失败,无法开启悬浮窗", Toast.LENGTH_SHORT).show();
hasGetPermission = false;
} else {
Toast.makeText(this, "权限授予成功!", Toast.LENGTH_SHORT).show();
floatShow = 1;
}
}
}
}
View Code
service:
package com.example.user.floatingview;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.WindowManager;
import android.widget.Button;
public class TopWindowService extends Service
{
public static final String OPERATION = "operation";
public static final int OPERATION_SHOW = 100;
public static final int OPERATION_HIDE = 101;
private static final int HANDLE_CHECK_ACTIVITY = 200;
private boolean isAdded = false; // 是否已增加悬浮窗
private static WindowManager wm;
private static WindowManager.LayoutParams params;
private Button btn_floatView;
@Override
public IBinder onBind(Intent intent)
{
return null;
}
@Override
public void onCreate()
{
super.onCreate();
createFloatView();
}
@Override
public void onDestroy()
{
super.onDestroy();
}
@Override
public void onStart(Intent intent, int startId)
{
super.onStart(intent, startId);
int operation = intent.getIntExtra(OPERATION, OPERATION_SHOW);
switch (operation)
{
case OPERATION_SHOW:
mHandler.sendEmptyMessage(OPERATION_SHOW);
break;
case OPERATION_HIDE:
mHandler.sendEmptyMessage(OPERATION_HIDE);
break;
}
}
private Handler mHandler = new Handler()
{
@Override
public void handleMessage(Message msg)
{
switch (msg.what)
{
case OPERATION_SHOW:
Log.v("zp","receive show message ");
if (!isAdded)
{
wm.addView(btn_floatView, params);
isAdded = true;
}
break;
case OPERATION_HIDE:
Log.v("zp","receive hide message ");
if (isAdded)
{
wm.removeView(btn_floatView);
isAdded = false;
}
break;
}
}
};
/**
* 创建悬浮窗
*/
private void createFloatView()
{
btn_floatView = new Button(getApplicationContext());
btn_floatView.setText("hello");
btn_floatView.setBackgroundResource(R.drawable.earth);
btn_floatView.setAlpha(0.5f);//设置透明
wm = (WindowManager) getApplicationContext().getSystemService(
Context.WINDOW_SERVICE);
params = new WindowManager.LayoutParams();
// 设置window type
params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
// 若设置为TYPE_TOAST 则不需要申请权限
//params.type = WindowManager.LayoutParams.TYPE_TOAST;
/*
* 如果设置为params.type = WindowManager.LayoutParams.TYPE_PHONE; 那么优先级会降低一些,
* 即拉下通知栏不可见
*/
params.format = PixelFormat.RGBA_8888; // 设置图片格式
// 设置Window flag
params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
/*
* 下面的flags属性的效果形同“锁定”。 悬浮窗不可触摸,不接受任何事件,同时不影响后面的事件响应。
* wmParams.flags=LayoutParams.FLAG_NOT_TOUCH_MODAL |
* LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_NOT_TOUCHABLE;
*/
// 设置悬浮窗的长得宽
params.width = 150;
params.height = 120;
// 设置悬浮窗的Touch监听
btn_floatView.setOnTouchListener(new OnTouchListener()
{
int lastX, lastY;
int paramX, paramY;
public boolean onTouch(View v, MotionEvent event)
{
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
lastX = (int) event.getRawX();
lastY = (int) event.getRawY();
paramX = params.x;
paramY = params.y;
break;
case MotionEvent.ACTION_MOVE:
int dx = (int) event.getRawX() - lastX;
int dy = (int) event.getRawY() - lastY;
params.x = paramX + dx;
params.y = paramY + dy;
// 更新悬浮窗位置
wm.updateViewLayout(btn_floatView, params);
break;
}
return true;
}
});
wm.addView(btn_floatView, params);
isAdded = true;
}
}
View Code
main_activity.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<Button
android:id="@+id/btn_show"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="显示桌面悬浮窗" />
<Button
android:id="@+id/btn_hide"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="隐藏桌面悬浮窗" />
</LinearLayout>
View Code