引言:
很多时候我们会安装一些具备拥有悬浮窗的app,悬浮窗可以随时地去使用app的的快捷功能而不必回到app主界面,本篇就来详细了解悬浮窗是如何创建、定义、修改的。
实现步骤:
1、创建悬浮窗布局文件
2、创建悬浮窗服务
3、请求系统悬浮窗权限并启动悬浮窗
4、在AndroidManifest中声明权限
代码实现:
1、首先先创建悬浮窗布局文件,这里我用了一个ImageView来显示悬浮窗的窗口图标(可以用其它更为复杂布局):
创建一个floating_window.xm:
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/iv_icon"
android:layout_width="70dp"
android:layout_height="70dp"
android:scaleType="fitCenter"
android:src="@drawable/fw_icon"/>
2、创建悬浮窗服务
创建一个service:FloatingWindowService
创建管理器用来管理、加载布局用于显示、设置布局属性、设置触摸监听等。
这里的触摸监听实现拖动中的变量需要说明一下:
我们触摸悬浮窗时手指下那个点的坐标,和拖动过程中手指下那个点的坐标就是我们移动悬浮窗的基准,知道手指的触摸点位移就可以同步悬浮窗位移了。
注意:
如果不在触摸事件中松开时刻对窗口进行触摸时间判断,来判断是点击还是拖动的话,我们设置的点击监听就无法得到监听,因为点击就是触摸的一种嘛,谁知道你是点击还是触摸呢。
不要忘记在AndroidManifest中声明服务
<service android:name=".FloatingWindowService" />
package com.itheima.screenshot;
import android.app.Service;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.Toast;
public class FloatingWindowService extends Service {//这是一个Android服务类,继承了Service,用于后台执行
private WindowManager windowManager;//声明一个WindowManager对象,用于管理悬浮窗
private View floatingView;//声明一个View视图,用于表示悬浮窗视图
private long MaxClickTime = 200;//最长点击时间200ms,这里我们触摸时间不超过最大点击事件判断时间时会认为是点击事件、超过为触摸事件
private long startTouchTime;//触摸开始时间
public FloatingWindowService() {
}
@Override
public void onCreate() {//服务被创建时执行
super.onCreate();
Log.d("wang","onCreate已调用,服务被创建");
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);//获取系服务,并转为WindowManager服务,管理悬浮窗的的添加、更新、移除
//用LayoutInflater将悬浮窗的布局转换为View对象,它即为悬浮窗窗口要显示的内容,这里将它作为悬浮窗的图标
floatingView = LayoutInflater.from(this).inflate(R.layout.floating_window,null);
//创建WindowManager.LayoutParams对象,设置悬浮窗布局参数
final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
70,//宽
70,//高
//设置悬浮窗类型,Android 8.0 (API26)以上用TYPE_APPLICATION_OVERLAY,以下用TYPE_PHONE
Build.VERSION.SDK_INT>=Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_PHONE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,//设置不可聚焦,触摸事件不会传给悬浮窗
PixelFormat.TRANSLUCENT
);
params.gravity = Gravity.TOP|Gravity.START;//设置悬浮窗为屏幕左上角
//悬浮窗的初始偏移位置
params.x = 0;
params.y = 100;
windowManager.addView(floatingView,params);//将悬浮窗视图添加到窗口管理器中
Log.d("wang","windowManager.addView(floatingView,params),浮窗视图已被添加到窗口管理器中");
floatingView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("wang","点击");
Toast.makeText(getApplicationContext(),"我生气了,不要点我!",Toast.LENGTH_SHORT).show();
}
});
floatingView.setOnTouchListener(new View.OnTouchListener() {//给悬浮窗设置触摸监听、用来处理拖动事件,实现悬浮窗拖动时改变悬浮窗位置
private int initialX,initialY;//声明每次拖动前的位置的坐标
private float initialTouchX,initialTouchY;//声明
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN://触摸发生时
startTouchTime = System.currentTimeMillis();
initialX = params.x;initialY = params.y;//悬浮窗的坐标
Log.d("Touch","悬浮窗坐标:"+initialY+","+initialY);
initialTouchX = event.getRawX();initialTouchY = event.getRawY();//触摸点坐标
Log.d("Touch","触摸点坐标:"+initialTouchX+","+initialTouchY);
return true;
case MotionEvent.ACTION_MOVE://移动发生时
int detailX = (int) (event.getRawX()-initialTouchX);int detailY = (int) (event.getRawY()-initialTouchY);//触摸点位移量
Log.d("Touch","触摸位移量:"+detailX+","+detailY);
params.x = initialX+detailX; params.y = initialY+detailY;
Log.d("Touch","悬浮窗新坐标:"+params.x+","+params.y);
windowManager.updateViewLayout(floatingView,params);//更新悬浮窗位置
return true;
case MotionEvent.ACTION_UP://触摸松开时
//计算触摸持续时间
long touchTime = System.currentTimeMillis()-startTouchTime;
if(touchTime<MaxClickTime){
v.performClick();
}
}
return false;
}
});
}
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public void onDestroy() {
super.onDestroy();
if(floatingView != null){
windowManager.removeView(floatingView);
Log.d("wang","onDestroy");
}
}
}
3、在主界面请求系统悬浮窗权限、并开启悬浮窗服务
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button btn_openFloatWindow;
private ActivityResultLauncher<Intent> register;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn_openFloatWindow = findViewById(R.id.btn_openFloatWindow);
btn_openFloatWindow.setOnClickListener(this);//设置点击事件监听
register = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result-> {
if(Settings.canDrawOverlays(this)){//获取了该权限Settings.canDrawOverlays
startFloatingWindowService();//打开悬浮窗服务
}else {//未获取了该权限Settings.canDrawOverlays
Toast.makeText(this,"需要悬浮窗权限",Toast.LENGTH_SHORT).show();
}
});
}
@Override
public void onClick(View v) {
if(v.getId()==btn_openFloatWindow.getId()){
getPermission();//获取悬浮窗权限,并打开悬浮窗
}
}
private void startFloatingWindowService(){
Intent intent = new Intent(this,FloatingWindowService.class);
startService(intent);
}
private void getPermission(){
if(!Settings.canDrawOverlays(this)){//没有Settings.canDrawOverlays权限
//Settings.ACTION_MANAGE_OVERLAY_PERMISSION:允许在其他应用上覆盖显示,Uri.parse("package:" + getPackageName())注明请求的包名
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName()));
//发起权限请求
register.launch(intent);//请求结束后会回调registerForActivityResult方法处理请求结果
}else {//已经有Settings.canDrawOverlays权限
startFloatingWindowService();//打开悬浮窗服务
}
}
}
4、在AndroidManifest中声明权限
<!-- 悬浮窗要覆盖在其他应用之上所以要申请SYSTEM_ALERT_WINDOW权限 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />