Android悬浮窗的简单实现
目录
概述
原理
Android的界面绘制,都是通过 WindowManager 的服务来实现的。 WindowManager 实现了 ViewManager 接口,可以通过获取 WINDOW_SERVICE 系统服务得到。而 ViewManager 接口有 addView 方法,我们就是通过这个方法将悬浮窗控件加入到屏幕中去。
为了让悬浮窗与Activity
脱离,使其在应用处于后台时悬浮窗仍然可以正常运行,使用Service
来启动悬浮窗并做为其背后逻辑支撑。
权限
在 API Level >= 23 的时候,需要在AndroidManefest.xml文件中声明权限 SYSTEM_ALERT_WINDOW 才能在其他应用上绘制控件。
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
除了这个权限外,我们还需要在系统设置里面对本应用进行设置悬浮窗权限。该权限在应用中需要启动 Settings.ACTION_MANAGE_OVERLAY_PERMISSION 来让用户手动设置权限。
if (!Settings.canDrawOverlays(this)) {
showError("当前无权限,请授权");
startActivityForResult(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())), 0);
} else {
startService(new Intent(MainActivity.this, FloatingService.class));
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == 0) {
if (!Settings.canDrawOverlays(this)) {
showError("授权失败");
} else {
showMsg("授权成功");
startService(new Intent(MainActivity.this, FloatingService.class));
}
}
}
LayoutParam
WindowManager 的 addView 方法有两个参数,一个是需要加入的控件对象,另一个参数是 WindowManager.LayoutParam 对象。
这里需要着重说明的是 LayoutParam 里的 type 变量。这个变量是用来指定窗口类型的。在设置这个变量时,需要注意一个坑,那就是需要对不同版本的Android系统进行适配。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
}
实例
AndroidManifest.xml
添加权限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
添加Service组件
<service android:name=".MediaFloatService"/>
MyApplication
public class PubApplication extends Application {
//设置一个全局变量来判断悬浮窗是否已经开启
public static Boolean isMediaFloatShow = false;
}
MediaFloatService
public class MediaFloatService extends Service {
private WindowManager windowManager;
private WindowManager.LayoutParams layoutParams;
private View mView;
private ViewFloatMediaBinding floatView;
private ArrayList<String> lstFilePaths;
private int currentIndex;
private int screenWidth;
private int screenHeight;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
//加载窗口布局
initWindow(intent);
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
//移除窗口布局
windowManager.removeView(mView);
super.onDestroy();
}
}
加载窗口布局
private void initWindow(Intent intent) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Settings.canDrawOverlays(this)) {
//获取WindowManager服务
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
//取得屏幕尺寸
DisplayMetrics dm = new DisplayMetrics();
windowManager.getDefaultDisplay().getMetrics(dm);
screenWidth = dm.widthPixels;
screenHeight = dm.heightPixels;
//设置LayoutParams
layoutParams = new WindowManager.LayoutParams();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
}
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_FULLSCREEN
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
layoutParams.format = PixelFormat.RGBA_8888; //背景透明效果
layoutParams.width = 512; //悬浮窗口长宽值,单位为 px 而非 dp
layoutParams.height = 450;
layoutParams.gravity = 51; //想要x,y生效,一定要指定Gravity为top和left //Gravity.TOP | Gravity.LEFT
layoutParams.x = 100; //启动位置
layoutParams.y = 100;
//加载悬浮窗布局
floatView = ViewFloatMediaBinding.inflate(LayoutInflater.from(MediaFloatService.this));
mView = floatView.getRoot();
//mView.setAlpha((float) 0.9);
//设定悬浮窗控件
floatView.ivFloatMediaClose.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
MyApplication.isMediaFloatShow = false;
//切记添加关闭服务按钮事件,调用 stopSelf() 方法以关闭悬浮窗。
stopSelf();
}
});
//接收传值
Bundle bundle = intent.getExtras();
lstFilePaths = bundle.getStringArrayList("lstFilePaths");
currentIndex = bundle.getInt("currentIndex");
floatView.ivFloatMediaPrev.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//判断是否是第一个,如果是则跳到最后循环播放
if (currentIndex == 0) currentIndex = lstFilePaths.size() - 1;
else currentIndex = currentIndex - 1;
showImage();
//Glide.with(MediaFloatService.this).load(lstFilePaths.get(currentIndex)).into(floatView.ivFloatMediaShow);
}
});
floatView.ivFloatMediaNext.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//判断是否是最后一个,如果是则跳到最前循环播放
if (currentIndex == lstFilePaths.size() - 1) currentIndex = 0;
else currentIndex = currentIndex + 1;
showImage();
//Glide.with(MediaFloatService.this).load(lstFilePaths.get(currentIndex)).into(floatView.ivFloatMediaShow);
}
});
BitmapFactory.Options options = getBitmapOptions(lstFilePaths.get(currentIndex));
layoutParams.width = Math.min(options.outWidth, screenWidth); //Math.min取得两个数据中的最小值
layoutParams.height = Math.min(options.outHeight, screenHeight);
Glide.with(this).load(lstFilePaths.get(currentIndex)).into(floatView.ivFloatMediaShow);
//单击事件是否显示控制按钮
floatView.ivFloatMediaShow.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (floatView.grpFloatMediaControl.getVisibility() == View.INVISIBLE)
floatView.grpFloatMediaControl.setVisibility(View.VISIBLE);
else floatView.grpFloatMediaControl.setVisibility(View.INVISIBLE);
}
});
//提交布局
windowManager.addView(mView, layoutParams);
}
}
}
取得屏幕尺寸
DisplayMetrics dm = new DisplayMetrics();
windowManager.getDefaultDisplay().getMetrics(dm);
screenWidth = dm.widthPixels;
screenHeight = dm.heightPixels;
根据路径取得图片尺寸
private BitmapFactory.Options getBitmapOptions(String filepath) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(filepath, options);
return options;
}
变更显示图片
变更图片时悬浮窗口也要根据变更后的图片再做调整,不然窗口大小还是上一图片的尺寸比例
private void showImage() {
BitmapFactory.Options options = getBitmapOptions(lstFilePaths.get(currentIndex));
layoutParams.width = Math.min(options.outWidth, screenWidth); //Math.min取得两个数据中的最小值
layoutParams.height = Math.min(options.outHeight, screenHeight);
Glide.with(this).load(lstFilePaths.get(currentIndex)).into(floatView.ivFloatMediaShow);
windowManager.updateViewLayout(mView, layoutParams);
}
窗口拖动与缩放
窗口拖动
floatView.ivFloatMediaShow.setOnTouchListener(new View.OnTouchListener() {
private int dX;
private int dY;
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
dX = (int) motionEvent.getRawX();
dY = (int) motionEvent.getRawY();
break;
case MotionEvent.ACTION_MOVE:
int nX = (int) motionEvent.getRawX();
int nY = (int) motionEvent.getRawY();
int cW = nX - dX;
int cH = nY - dY;
dX = nX;
dY = nY;
layoutParams.x = layoutParams.x + cW;
layoutParams.y = layoutParams.y + cH;
windowManager.updateViewLayout(mView, layoutParams);
break;
default:
break;
}
return true;
}
});
单击双击
floatView.ivFloatMediaShow.setOnTouchListener(new View.OnTouchListener() {
private int sX;
private int sY;
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
sX = (int) motionEvent.getRawX();
sY = (int) motionEvent.getRawY();
break;
case MotionEvent.ACTION_UP:
//如果抬起时的位置和按下时的位置大致相同视作单击事件
int nX2 = (int) motionEvent.getRawX();
int nY2 = (int) motionEvent.getRawY();
int cW2 = nX2 - sX;
int cH2 = nY2 - sY;
//间隔值可能为负值,所以要取绝对值进行比较
if (Math.abs(cW2) < 3 && Math.abs(cH2) < 3) view.performClick();
break;
default:
break;
}
return true;
}
});
双指缩放
floatView.ivFloatMediaShow.setOnTouchListener(new View.OnTouchListener() {
private ScaleGestureDetector scaleGestureDetector = new ScaleGestureDetector(MediaFloatService.this, new myScale());
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
isP2Down = true; //双指按下
break;
case MotionEvent.ACTION_MOVE:
if (motionEvent.getPointerCount() == 2) {
//双指缩放
scaleGestureDetector.onTouchEvent(motionEvent);
}
break;
case MotionEvent.ACTION_UP:
isP2Down = false; //双指抬起
break;
default:
break;
}
return true;
}
});
ScaleGestureDetector
private float initSapcing = 0;
private int initWidth;
private int initHeight;
private int initX;
private int initY;
private boolean isP2Down = false;
private class myScale extends ScaleGestureDetector.SimpleOnScaleGestureListener {
@Override
public boolean onScale(ScaleGestureDetector detector) {
// detector.getCurrentSpan();//两点间的距离跨度
// detector.getCurrentSpanX();//两点间的x距离
// detector.getCurrentSpanY();//两点间的y距离
// detector.getFocusX(); //
// detector.getFocusY(); //
// detector.getPreviousSpan(); //上次
// detector.getPreviousSpanX();//上次
// detector.getPreviousSpanY();//上次
// detector.getEventTime(); //当前事件的事件
// detector.getTimeDelta(); //两次事件间的时间差
// detector.getScaleFactor(); //与上次事件相比,得到的比例因子
if (isP2Down) {
//双指按下时最初间距
initSapcing = detector.getCurrentSpan();
//双指按下时窗口大小
initWidth = layoutParams.width;
initHeight = layoutParams.height;
//双指按下时窗口左顶点位置
initX = layoutParams.x;
initY = layoutParams.y;
isP2Down = false;
}
float scale = detector.getCurrentSpan() / initSapcing; //取得缩放比
int newWidth = (int) (initWidth * scale);
int newHeight = (int) (initHeight * scale);
//判断窗口缩放后是否超出屏幕大小
if (newWidth < screenWidth && newHeight < screenHeight) {
layoutParams.width = newWidth;
layoutParams.height = newHeight;
layoutParams.x = initX;
layoutParams.y = initY;
//缩放后图片会失真重新载入图片
Glide.with(MediaFloatService.this)
.load(lstFilePaths.get(currentIndex))
.into(floatView.ivFloatMediaShow);
//提交更新布局
windowManager.updateViewLayout(mView, layoutParams);
}
return true;
//return super.onScale(detector);
}
}
完整代码
floatView.ivFloatMediaShow.setOnTouchListener(new View.OnTouchListener() {
private int dX;
private int dY;
private int sX;
private int sY;
private ScaleGestureDetector scaleGestureDetector = new ScaleGestureDetector(MediaFloatService.this, new myScale());
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
dX = (int) motionEvent.getRawX();
dY = (int) motionEvent.getRawY();
sX = (int) motionEvent.getRawX();
sY = (int) motionEvent.getRawY();
isP2Down = true; //双指按下
break;
case MotionEvent.ACTION_MOVE:
if (motionEvent.getPointerCount() == 1) {
//单指拖动
int nX = (int) motionEvent.getRawX();
int nY = (int) motionEvent.getRawY();
int cW = nX - dX;
int cH = nY - dY;
dX = nX;
dY = nY;
layoutParams.x = layoutParams.x + cW;
layoutParams.y = layoutParams.y + cH;
windowManager.updateViewLayout(mView, layoutParams);
} else if (motionEvent.getPointerCount() == 2) {
//双指缩放
scaleGestureDetector.onTouchEvent(motionEvent);
}
break;
case MotionEvent.ACTION_UP:
isP2Down = false; //双指抬起
//如果抬起时的位置和按下时的位置大致相同视作单击事件
int nX2 = (int) motionEvent.getRawX();
int nY2 = (int) motionEvent.getRawY();
int cW2 = nX2 - sX;
int cH2 = nY2 - sY;
//间隔值可能为负值,所以要取绝对值进行比较
if (Math.abs(cW2) < 3 && Math.abs(cH2) < 3) view.performClick();
break;
default:
break;
}
return true;
}
});
实例2
private WindowManager windowManager;
private WindowManager.LayoutParams layoutParams;
private View floatView;
if(Settings.canDrawOverlays(this)){
windowManager = (WindowManager)getSystemService(WINDOW_SERVICE);
layoutParams = new WindowManager.LayoutParams();
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
}
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
layoutParams.format = PixelFormat.RGBA_8888; //背景透明效果
layoutParams.width = 512; //悬浮窗口长宽值,单位为 px 而非 dp
layoutParams.height = 450;
layoutParams.gravity = Gravity.TOP | Gravity.LEFT; //启动位置
layoutParams.x = 100;
layoutParams.y = 100;
floatView = LayoutInflater.from(this).inflate(R.layout.note_float_view, null);
floatView.setAlpha((float) 0.9);
windowManager.addView(floatView, layoutParams);
}
常见问题
起始位置设置无效
想要x,y生效,一定要指定Gravity为top和left,想要居中就设置为 Center
layoutParams.gravity = Gravity.TOP | Gravity.LEFT;
layoutParams.x = 100; //启动位置
layoutParams.y = 100;
获取状态栏高度
private int statusBarHeight = -1;
//获取状态栏高度
private void getStatusBarHeight(){
mView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
if(resourceId > 0){
statusBarHeight = getResources().getDimensionPixelSize(resourceId);
}
}