WindowManager实现可移动可点击(可只在应用中显示)悬浮窗
概述
最近项目中要求在每个界面加个客服悬浮窗,点击可以进入网易七鱼的聊天窗口,于是研究了下WindowManager添加悬浮窗的功能。
实现
要实现悬浮窗功能首先在清单文件中给予相应权限:
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
然后在第一个显示悬浮窗的Activity(我这里是MainActivity)添加悬浮窗:
private WindowManager wm;
private WindowManager.LayoutParams wmParams;
private float mStartX, mStartY;//悬浮窗移动开始的点,位置在悬浮窗中心
private long mDownTime, mUpTime;//点击悬浮窗落手的时间和抬手的时间
private View mView;//悬浮窗视图
private boolean isFloatViewNotAdded = true;//悬浮窗没有被添加为true
private int mCanMoveHeight;//悬浮窗可移动高度
private int mCanMoveWidth;//悬浮窗可移动宽度
先得到一个WindowManager对象:
wm = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
获取悬浮窗布局界面:
mView = LayoutInflater.from(this).inflate(R.layout.kefu, null);
给定悬浮窗布局参数:
wmParams = new WindowManager.LayoutParams(
DensityUtil.dp2px(this,52),
DensityUtil.dp2px(this,52),
WindowManager.LayoutParams.TYPE_TOAST,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
PixelFormat.TRANSLUCENT
);
其中第3个参数TYPE_TOAST在Android7.1.1版本及以上会报错,所以这里要判断一下:
if(Build.VERSION.SDK_INT > 24) {
wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;
}
然后调用wm.addView(mView, wmParams);
就能够成功添加悬浮窗。
但是在Android6.0以后和Android 4.4 ~ Android 5.1.1中的某些机型还有权限问题,这里我参考了这篇:Android 悬浮窗权限各机型各系统适配大全http://blog.csdn.net/self_study/article/details/52859790。
稍微修改一下就直接拿来用了:
if (FloatWindowUtils.checkPermission(this)){
if (isFloatViewNotAdded){
wm.addView(mView, wmParams);
isFloatViewNotAdded = false;
}
}else{
FloatWindowUtils.applyPermission(this);
}
如图所示,我要让刚开始显示的悬浮窗在界面的右下角,并且移动后自动贴到右边,所以在addView之前先设置wmParams的x值和y值,不设置的话x和y默认为0,也就是在屏幕除了状态栏之外的中心(悬浮窗不能移动到状态栏上,设置状态栏消失后也一样)
mCanMoveWidth = getResources().getDisplayMetrics().widthPixels / 2 - DensityUtil.dip2px(MainActivity.this, 26);
mCanMoveHeight = (getResources().getDisplayMetrics().heightPixels - StatusBarCompat.getStatusBarHeight(this)) / 2 - DensityUtil.dip2px(MainActivity.this, 26);
wmParams.x = mCanMoveWidth;
wmParams.y = mCanMoveHeight;
因为wmParams的x和y值指的是悬浮窗中心的位置,所以wmParams的x值设为屏幕宽度减去控件本身宽度/2,y值设为屏幕高度减去状态栏高度后的一半再减去控件本身高度/2,这样刚开始悬浮窗就在右下角了。
然后就是给悬浮窗设置点击事件:
mView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "点击悬浮窗", Toast.LENGTH_SHORT).show();
}
});
给悬浮窗设置触摸事件:
mView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// 当前值以屏幕左上角为原点
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mStartX = event.getRawX();
mStartY = event.getRawY();
mDownTime = System.currentTimeMillis();
break;
case MotionEvent.ACTION_MOVE:
wmParams.x += event.getRawX() - mStartX;
wmParams.y += event.getRawY() - mStartY;
if (wmParams.y < -mCanMoveHeight){
wmParams.y = -mCanMoveHeight;
}
if (wmParams.y > mCanMoveHeight){
wmParams.y = mCanMoveHeight;
}
wm.updateViewLayout(mView, wmParams);
mStartX = event.getRawX();
mStartY = event.getRawY();
break;
case MotionEvent.ACTION_UP:
wmParams.x = mCanMoveWidth;
mStartX = wmParams.x;
wm.updateViewLayout(mView, wmParams);
mUpTime = System.currentTimeMillis();
return mUpTime-mDownTime>200;
}
// 消耗触摸事件
return false;
}
});
这里的onTouch方法中返回false悬浮窗的点击事件能被触发,返回true就不能被触发了,所以判断点击的时间,若超过200ms,返回true,不触发点击事件。这样就既可以移动又可点击了。
如果只想要在应用中显示悬浮窗,返回桌面就不显示,可以在Activity生命周期中执行onStop时调用removeView来移除,执行onResume时重新添加,通过定义一个布尔变量isFloatViewNotAdded来判断悬浮窗是否被添加。这里我定义了两个方法showFloatView和removeFloatView分别在onResume和onStop中执行:
public void showFloatView(Context context){
if (FloatWindowUtils.checkPermission(this)){
if (isFloatViewNotAdded){
wm.addView(mView, wmParams);
isFloatViewNotAdded = false;
}
}else{
FloatWindowUtils.applyPermission(this);
}
}
public void removeFloatView(Context context){
if (!isFloatViewNotAdded){
wm.removeView(mView);
isFloatViewNotAdded = true;
}
}
@Override
protected void onResume() {
super.onResume();
showFloatView(this);
}
@Override
protected void onStop() {
super.onStop();
removeFloatView(this);
}
然后注册一个广播接收者,让其他Activity可以通过发送广播来控制悬浮窗添加和移除:
private LocalBroadcastManager mLocalBroadcastManager;
private BroadcastReceiver mBroadcastReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals("ACTION_ADD_FLOATVIEW")){
showFloatView(MainActivity.this);
}else if (intent.getAction().equals("ACTION_REMOVE_FLOATVIEW")){
removeFloatView(MainActivity.this);
}
}
};
mLocalBroadcastManager = LocalBroadcastManager.getInstance(this);
IntentFilter filter = new IntentFilter();
filter.addAction("ACTION_ADD_FLOATVIEW");
filter.addAction("ACTION_REMOVE_FLOATVIEW");
mLocalBroadcastManager.registerReceiver(mBroadcastReceiver,filter);
initView();
}
注意:当MainActivity跳转到第二个Activity(Main2Activity)后,会先执行Main2Activity的onResume然后才会执行MainActivity的onStop,所以即使这里在Main2Activity的onResume中发送广播添加悬浮窗,在MainActivity的onStop也会移除悬浮窗,也就是说从MainActivity跳转到Main2Activity悬浮窗会消失。这里可以在AndroidManifest.xml文件中给Main2Activity设置个主题,主题样式中添加这一条:
<item name="android:windowIsTranslucent">true</item>
这样跳转到Main2Activity后,MainActivity就不会再执行onStop,悬浮窗也不会消失。
最后注意在MainAcitivity的onDestory方法中取消注册广播。
@Override
protected void onDestroy() {
super.onDestroy();
mLocalBroadcastManager.unregisterReceiver(mBroadcastReceiver);
}