现在越来越多的Android APP都有悬浮窗的功能,而悬浮窗主要是使用Window来实现的,我们对Window的操作是通过 WindowManager 来完成的。在设置悬浮窗时,需要在清单文件中添加权限 <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
,除了设置清单文件,由于这个权限是危险权限,我们还需要在代码中进行动态的申请。可以去看我上一篇的文章 悬浮窗的权限申请
[https://blog.csdn.net/puppet_zz/article/details/79688849]
实现
Window有三种类型,是应用Window、子Window和系统Window,应用Window 对应一个 Acitivity,子 Window 不能单独存在,需要依附在特定的父 Window 中,比如常见的一些 Dialog 就是一个子 Window。系统 Window是需要声明权限才能创建的 Window,比如 Toast 和系统状态栏都是系统 Window。而我们要实现的悬浮窗也是系统Window。实现悬浮窗,我们通过WindowManager来进行一些设置,WindowManager是一个接口,其继承的ViewManager有三个方法。
public interface ViewManager{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}
这三个方法就是添加、更新、移除View。通过WindowManager来实现悬浮窗的代码如下:
WindowManager.LayoutParams layoutParams layoutParams = new WindowManager.LayoutParams();
WindowManager windowManager windowManager = (WindowManager) getApplication().getSystemService(WINDOW_SERVICE);
//设置Window的类型
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
//设置Window显示的模式
layoutParams.format = PixelFormat.RGBA_8888;
//设置Window不可获取焦点
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
//设置Window的位置(如果把gravity设置为CENTER,在设置x,y的位置将不起效果)
layoutParams.gravity = Gravity.LEFT|Gravity.TOP;
layoutParams.x = 0;
layoutParams.y = 0;
//设置Window的宽高
layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
//设置Window的布局
LayoutInflater inflater = LayoutInflater.from(getApplication());
View lavFloat= inflater.inflate(R.layout.float_layout, null);
这样就设置好悬浮窗了,同时我们需要悬浮窗随手的移动并移动,并且悬浮窗可以松手后自动贴边
lavFloat.setOnTouchListener(new View.OnTouchListener() {
int lastX = 0;
int lastY = 0;
int moveX = 0;
int moveY = 0;
int curX = 0;
int curY = 0;
boolean isMove;//是否在移动
boolean isWelt;//是否贴边
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = (int) event.getRawX();
lastY = (int) event.getRawY();
isMove = false;
return false;
case MotionEvent.ACTION_MOVE:
curX = (int) event.getRawX();
curY = (int) event.getRawY();
FloatService.this.layoutParams.x = curX - lavFloat.getMeasuredWidth() / 2;
FloatService.this.layoutParams.y = curY - lavFloat.getMeasuredHeight() / 2;
// 刷新
if (view!=null){
windowManager.updateViewLayout(view, FloatService.this.layoutParams);
}
return true;
case MotionEvent.ACTION_UP:
int finalX = (int) event.getRawX();
int finalY = (int) event.getRawY();
int clickX =Math.abs(finalX-lastX);
int clickY =Math.abs(finalY- lastY) ;
if (clickX < 20 || clickY < 20) {//滑动小于20 视为点击事件
isMove = false;
} else {
isMove = true;
}
//设置贴边时,有个弹簧动画
SpringForce springForce = new SpringForce(0)
.setDampingRatio(0.75f)
.setStiffness(SpringForce.STIFFNESS_HIGH);
SpringForce force = new SpringForce(1)
.setDampingRatio(0.75f)
.setStiffness(SpringForce.STIFFNESS_HIGH);
SpringAnimation animationX = new SpringAnimation(lavFloat, SpringAnimation.TRANSLATION_X).setSpring(springForce);
SpringAnimation animation = new SpringAnimation(lavFloat, SpringAnimation.TRANSLATION_X).setSpring(force);
if (finalY < lavFloat.getHeight()) {//触摸位置小于view的高度 (理解为屏幕顶部)
moveY = 0;
if (finalX <= screenWidth / 2) {//贴左边
moveX = 0;
} else {//贴右边
moveX = screenWidth;
}
}
if (finalY > screenHeight - lavFloat.getHeight()) {//触摸位置大于屏幕高度view的高度 (理解为屏幕底部)
moveY = screenHeight;
if (finalX <= screenWidth / 2) {//贴左边
moveX = 0;
} else {//贴右边
moveX = screenWidth;
}
}
if (finalY >= lavFloat.getHeight() && finalY <= screenHeight - lavFloat.getHeight()) {
moveY = finalY - lavFloat.getHeight() / 2;
if (finalX <= screenWidth / 2) {//贴左边
moveX = 0;
} else {//贴右边
moveX = screenWidth;
}
}
FloatService.this.layoutParams.x = moveX;
FloatService.this.layoutParams.y = moveY;
if (isMove) {
if (view!=null){
windowManager.updateViewLayout(view, FloatService.this.layoutParams);
}
}
isWelt = false;
if (moveX ==0) {
isWelt = true;
animationX.setStartValue((float) finalX);
animationX.start();
} else if (moveX ==screenWidth){
isWelt = true;
animation.setStartValue((float) finalX);
animation.start();
}
return isMove;//false 为点击 true 为移动
default:
break;
}
return false;//false才能有点击操作
}
});
同时设置对悬浮窗可以进行点击操作
lavFloat.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (isWelt) {//贴边才能点击
//点击逻辑
}
}
});
我们开启悬浮窗时可以在Activity中开启,也可以在Service中开启。可以根据具体需求具体分析,不过建议在Service中开启,因为Service有单独的生命周期,不与Activity周期重叠,可以更好的操作悬浮窗。有时候我们需要开启悬浮窗,有些时候也需要隐藏悬浮窗,如果在Service中设置的话,我们可以开启服务来开启悬浮窗,需要隐藏时,关闭服务来隐藏悬浮窗。关闭悬浮窗需要在服务销毁时移除悬浮窗。
@Override
public int onStartCommand(Intent intent, int flags, int startId) {//添加悬浮窗
openFloat();//就是上面的代码
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {//移除悬浮窗
super.onDestroy();
windowManager.removeViewImmediate(view);
}
当应用退到后台时,悬浮窗会在桌面显示的,我们应该让应用在后台时,悬浮窗进行隐藏。可以建立一个广播,来进行判断,在后台时,关闭服务。
悬浮窗的添加就基本完成,配合上一篇 悬浮窗的权限适配 就可以完整的来实现悬浮窗。