悬浮窗的实现需要用到service,这样按下Home退出到桌面的时候,悬浮窗还可以作为程序后台,继续显示在屏幕上,如果不清后台,它会一直在那里哈。注意要在AndroidManifest.xml里注册service哈
我们借助WindowManager来生成悬浮窗
WindowManager的三个最常用方法为:
addView 添加View
addView(View view, WindowManager.LayoutParams params);
View就是要添加到windowmanager中的对象,而params是窗口的设置参数,这个我们讲到代码阶段再说。removeView 移除View removeView(View view); 从windowmanager中移除对象。
updateViewLayout刷新View updateViewLayout(View view,
ViewGroup.LayoutParams params); 也是两个参数,一个View一个params,参考addView。
悬浮窗的布局就是通过addView添加、悬浮窗更改位置通过updateViewLayout进行刷新、关闭悬浮窗时调用removeView。
我们需要申请权限
悬浮窗需要在别的应用之上显示控件,很显然,这需要某些权限才可以。
在API Level >= 23的时候,需要在AndroidManefest.xml文件中声明权限SYSTEM_ALERT_WINDOW才能在其他应用上绘制控件。
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
除了这个权限外,我们还需要在系统设置里面对本应用进行设置悬浮窗权限。该权限在应用中需要启动Settings.ACTION_MANAGE_OVERLAY_PERMISSION来让用户手动设置权限。
startActivityForResult(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())), REQUEST_CODE);
窗口类型很重要
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;
}
在Android 8.0之前,悬浮窗口设置可以为TYPE_PHONE,这种类型是用于提供用户交互操作的非应用窗口。
而Android 8.0对系统和API行为做了修改,如果需要实现在其他应用和窗口上方显示提醒窗口,那么必须为TYPE_APPLICATION_OVERLAY的类型。
具体代码分析
layout
这里实现3种悬浮窗:悬浮Button(java代码直接实现,不写入layout),悬浮imagView,悬浮surfaceView
activity_main.xml就在主活动排3个按钮,点击按钮打开对应的悬浮窗
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:orientation="vertical">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="start floating button"
android:onClick="startFloatingButtonService" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="start floating image display"
android:onClick="startFloatingImageDisplayService"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="start floating video player"
android:onClick="startFloatingVideoService"/>
</LinearLayout>
image_display.xml是imgeView类型的悬浮窗
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<ImageView
android:id="@+id/image_display_imageview"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
video_display.xml是surfaceView类型的悬浮窗
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<SurfaceView
android:id="@+id/video_display_surfaceview"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</FrameLayout>
如果使用FrameLayout就可以在悬浮窗上面加控件了Ho~
在启动服务之前,需要在活动里先判断一下当前是否允许开启悬浮窗。
MainActivity.java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == 0) {
if (!Settings.canDrawOverlays(this)) {
Toast.makeText(this, "授权失败", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "授权成功", Toast.LENGTH_SHORT).show();
startService(new Intent(MainActivity.this, FloatingButtonService.class));
}
} else if (requestCode == 1) {
if (!Settings.canDrawOverlays(this)) {
Toast.makeText(this, "授权失败", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "授权成功", Toast.LENGTH_SHORT).show();
startService(new Intent(MainActivity.this, FloatingImageDisplayService.class));
}
} else if (requestCode == 2) {
if (!Settings.canDrawOverlays(this)) {
Toast.makeText(this, "授权失败", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "授权成功", Toast.LENGTH_SHORT).show();
startService(new Intent(MainActivity.this, FloatingVideoService.class));
}
}
}
public void startFloatingButtonService(View view) {
if (FloatingButtonService.isStarted) {
return;
}
if (!Settings.canDrawOverlays(this)) {
Toast.makeText(this, "当前无权限,请授权", Toast.LENGTH_SHORT);
startActivityForResult(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())), 0);
} else {
startService(new Intent(MainActivity.this, FloatingButtonService.class));
}
}
public void startFloatingImageDisplayService(View view) {
if (FloatingImageDisplayService.isStarted) {
return;
}
if (!Settings.canDrawOverlays(this)) {
Toast.makeText(this, "当前无权限,请授权", Toast.LENGTH_SHORT);
startActivityForResult(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())), 1);
} else {
startService(new Intent(MainActivity.this, FloatingImageDisplayService.class));
}
}
public void startFloatingVideoService(View view) {
if (FloatingVideoService.isStarted) {
return;
}
if (!Settings.canDrawOverlays(this)) {
Toast.makeText(this, "当前无权限,请授权", Toast.LENGTH_SHORT);
startActivityForResult(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())), 2);
} else {
startService(new Intent(MainActivity.this, FloatingVideoService.class));
}
}
}
悬浮Button的service
FloatingButtonService.java
public class FloatingButtonService extends Service {
public static boolean isStarted = false;
private WindowManager windowManager;
private WindowManager.LayoutParams layoutParams;
private Button button;
@Override
public void onCreate() {
super.onCreate();
isStarted = true;
// 获取WindowManager服务
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
// 设置LayoutParam
layoutParams = new WindowManager.LayoutParams();
//设置type
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
}
//设置效果为背景透明.
layoutParams.format = PixelFormat.RGBA_8888;
//设置窗口初始停靠位置.
layoutParams.gravity = Gravity.LEFT | Gravity.TOP;
//设置flags.不可聚焦及不可使用按钮对悬浮窗进行操控.
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
//设置悬浮窗口长宽数据.
//注意,这里的width和height均使用px而非dp
//如果你想完全对应布局设置,需要先获取到机器的dpi
//px与dp的换算为px = dp * (dpi / 160).
layoutParams.width = 500;
layoutParams.height = 100;
layoutParams.x = 300;
layoutParams.y = 300;
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
showFloatingWindow();
return super.onStartCommand(intent, flags, startId);
}
private void showFloatingWindow() {
if (Settings.canDrawOverlays(this)) {
// 新建悬浮窗控件
button = new Button(getApplicationContext());
button.setText("Floating Window");
button.setBackgroundColor(Color.BLUE);
// 将悬浮窗控件添加到WindowManager
windowManager.addView(button, layoutParams);
button.setOnTouchListener(new FloatingOnTouchListener());
}
}
//拖动悬浮窗
private class FloatingOnTouchListener implements View.OnTouchListener {
private int x;
private int y;
@Override
public boolean onTouch(View view, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
x = (int) event.getRawX();
y = (int) event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
int nowX = (int) event.getRawX();
int nowY = (int) event.getRawY();
int movedX = nowX - x;
int movedY = nowY - y;
x = nowX;
y = nowY;
layoutParams.x = layoutParams.x + movedX;
layoutParams.y = layoutParams.y + movedY;
// 更新悬浮窗控件布局,只有调用了这个方法,悬浮窗的位置才会发生改变
windowManager.updateViewLayout(view, layoutParams);
break;
default:
break;
}
return false;
}
}
}
format 用于设置显示的格式。RGBA_8888是透明型,也是最常用到的。
flags 这是很重要的一个设置。FLAG_NOT_FOCUSABLE设置了不可聚焦。同时经常用的还有FLAG_WATCH_OUTSIDE_TOUCH,这个设置可以让悬浮窗接收到外部点击事件,如果你想在之后做小悬浮窗点击变大,再点击悬浮窗之外又变回小悬浮窗。这个可以用到。多个FLAG的话可以用|来连接,如
params.flags = FLAG_NOT_FOCUSABLE | FLAG_WATCH_OUTSIDE_TOUCH
gravity 用于设置窗口的初始停靠位置。我们设置的是让它初始在最左&最上方生成,后面两句是定义这里的xy值都为0。
width&height用于设置悬浮窗口的大小,建议设置成和布局一样大最好。小于布局会把里面的组件进行挤压。
接下来是悬浮ImagView的service了,它还可以每隔两秒更换一张图片
FloatingImageDisplayService.java
public class FloatingImageDisplayService extends Service {
public static boolean isStarted = false;
private WindowManager windowManager;
private WindowManager.LayoutParams layoutParams;
private View displayView;
private int[] images;
private int imageIndex = 0;
private Handler changeImageHandler;
@Override
public void onCreate() {//创建服务
super.onCreate();
isStarted = true;
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.format = PixelFormat.RGBA_8888;
layoutParams.gravity = Gravity.LEFT | Gravity.TOP;
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
layoutParams.width = 500;
layoutParams.height = 500;
layoutParams.x = 300;
layoutParams.y = 300;
images = new int[] {
R.drawable.image_01,
R.drawable.image_02,
R.drawable.image_03,
R.drawable.image_04,
R.drawable.image_05,
};
//借助Handler定时传递消息机制来实现定时切换图片的机制
changeImageHandler = new Handler(this.getMainLooper(),changeImageCallback);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
showFloatingWindow();//启动服务
return super.onStartCommand(intent, flags, startId);
}
private void showFloatingWindow() {
if (Settings.canDrawOverlays(this)) { //如果允许了悬浮窗
LayoutInflater layoutInflater = LayoutInflater.from(this);//获取LayoutInflater对象
displayView = layoutInflater.inflate(R.layout.image_display, null);
displayView.setOnTouchListener(new FloatingOnTouchListener());
ImageView imageView = displayView.findViewById(R.id.image_display_imageview);//初始化第一张图
imageView.setImageResource(images[imageIndex]);
windowManager.addView(displayView, layoutParams);
changeImageHandler.sendEmptyMessageDelayed(0, 2000);//第一次延迟2秒发送空消息,编号为0
}
}
private Handler.Callback changeImageCallback = new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
if (msg.what == 0) {//Handler接收消息判断
imageIndex++;
if (imageIndex >= 5) {
imageIndex = 0;
}
if (displayView != null) {
((ImageView) displayView.findViewById(R.id.image_display_imageview)).setImageResource(images[imageIndex]);//更新UI图
}
changeImageHandler.sendEmptyMessageDelayed(0, 2000);//图换完了,再发一条消息
}
return false;//否则没有接受消息
}
};
//悬浮窗拖动操作部分代码完全不变
private class FloatingOnTouchListener implements View.OnTouchListener {
private int x;
private int y;
@Override
public boolean onTouch(View view, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
x = (int) event.getRawX();
y = (int) event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
int nowX = (int) event.getRawX();
int nowY = (int) event.getRawY();
int movedX = nowX - x;
int movedY = nowY - y;
x = nowX;
y = nowY;
layoutParams.x = layoutParams.x + movedX;
layoutParams.y = layoutParams.y + movedY;
windowManager.updateViewLayout(view, layoutParams);
break;
default:
break;
}
return false;
}
}
}
最后是SurfaceView的service了,我们用它来实现播放小视频
大同小异
public class FloatingVideoService extends Service {
public static boolean isStarted = false;
private WindowManager windowManager;
private WindowManager.LayoutParams layoutParams;
private MediaPlayer mediaPlayer;
private View displayView;
@Override
public void onCreate() {
super.onCreate();
isStarted = true;
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.format = PixelFormat.RGBA_8888;
layoutParams.gravity = Gravity.LEFT | Gravity.TOP;
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
layoutParams.width = 800;
layoutParams.height = 450;
layoutParams.x = 300;
layoutParams.y = 300;
mediaPlayer = new MediaPlayer();//创建媒体播放器对象
}
@Nullable//表示可以为空
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
showFloatingWindow();
return super.onStartCommand(intent, flags, startId);
}
private void showFloatingWindow() {
if (Settings.canDrawOverlays(this)) {
LayoutInflater layoutInflater = LayoutInflater.from(this);
displayView = layoutInflater.inflate(R.layout.video_display, null);
displayView.setOnTouchListener(new FloatingOnTouchListener());
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
SurfaceView surfaceView = displayView.findViewById(R.id.video_display_surfaceview);
final SurfaceHolder surfaceHolder = surfaceView.getHolder();
surfaceHolder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
mediaPlayer.setDisplay(surfaceHolder);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
});
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mediaPlayer.start();
}
});
try {
mediaPlayer.setDataSource(this, Uri.parse("https://raw.githubusercontent.com/dongzhong/ImageAndVideoStore/master/Bruno%20Mars%20-%20Treasure.mp4"));
mediaPlayer.prepareAsync();
}
catch (IOException e) {
Toast.makeText(this, "无法打开视频源", Toast.LENGTH_LONG).show();
}
windowManager.addView(displayView, layoutParams);
}
}
//拖动悬浮窗部分代码 完全不变
private class FloatingOnTouchListener implements View.OnTouchListener {
private int x;
private int y;
@Override
public boolean onTouch(View view, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
x = (int) event.getRawX();
y = (int) event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
int nowX = (int) event.getRawX();
int nowY = (int) event.getRawY();
int movedX = nowX - x;
int movedY = nowY - y;
x = nowX;
y = nowY;
layoutParams.x = layoutParams.x + movedX;
layoutParams.y = layoutParams.y + movedY;
windowManager.updateViewLayout(view, layoutParams);
break;
default:
break;
}
return true;
}
}
}