有时候,我们可能会需要制作一个始终悬浮的窗口显示一些关键信息。它独立于我们的页面,可以在不妨碍用户操作的情况下显示信息。这里我们就学习一下悬浮窗的做法。
1.WindowManager的常用方法
- getDefaultDisplay:获取默认的显示屏信息。通常可用该方法获取屏幕分辨率。
- addView:往窗口添加视图。第二个参数为WindowManager.LayoutParams对象。
- updateViewLayout:更新指定视图的布局参数。第二个参数为WindowManager.LayoutParams对象。
- removeView:从窗口移除指定视图。
下面是窗口布局参数WindowManager.LayoutParams的常用属性
- alpha:窗口的透明度,取值为0.0到1.0。0.0表示全透明,1.0表示不透明。
- gravity:内部视图的对齐方式。取值同View的setGravity方法。
- x和y:分别表示窗口左上角的X坐标和Y坐标。
- width和height:分别表示窗口的宽度和高度。
- format:窗口的像素点格式。取值见PixelFormat类中的常量定义,一般取值PixelFormat.RGBA_8888。
- type:窗口的显示类型,常用的显示类型见下表。
WindowManager类的窗口显示类型 | 说明 |
TYPE_SYSTEM_ALERT | 系统警告提示 |
TYPE_SYSTEM_ERROR | 系统错误提示 |
TYPE_SYSTEM_OVERLAY | 页面顶层提示 |
TYPE_SYSTEM_DIALOG | 系统对话框 |
TYPE_STATUS_BAR | 状态栏 |
TYPE_TOAST | 短暂通知Toast |
- flags:窗口的行为准则,对于悬浮窗来说,一般设置为FLAG_NOT_FOCUSABLE。常用的窗口标志位说明见下表。
WindowManager类的窗口标志位 | 说明 |
FLAG_NOT_FOCUSABLE | 不能抢占焦点,即不接受任何按键或按钮事件 |
FLAG_NOT_TOUCHABLE | 不接受触摸屏事件。悬浮窗一般不设置该标志,因为一旦设置该标志,将无法拖动悬浮窗。 |
FLAG_NOT_TOUCH_MODAL | 当窗口允许获得焦点时(即没有设置FLAG_NOT_FOCUSABLE标志),仍然将窗口之外的按键事件发送给后面的窗口处理。否则它将独占所有的按键事件,而不管它们是不是发生在窗口范围之内。 |
FLAG_LAYOUT_IN_SCREEN | 允许窗口占满整个屏幕 |
FLAG_LAYOUT_NO_LIMITS | 允许窗口扩展到屏幕之外 |
FLAG_WATCH_OUTSIDE_TOUCH | 如果设置了FLAG_NOT_TOUCH_MODAL标志,则当按键动作发生在窗口之外时,将接收到一个MotionEvent.ACTION_OUTSIDE事件 |
2.自定义悬浮窗与对话框区别
- 悬浮窗是可以拖动的,对话框则不可拖动。
- 悬浮窗不妨碍用户触摸窗外的区域,对话框则不让用户操作框外的控件。
- 悬浮窗独立于Activity页面,即当页面退出后,悬浮窗仍停留在屏幕上;而对话框与Activity页面是共存关系,一旦页面退出则对话框也消失了。
3.悬浮窗制作步骤
- 在AndroidManifest.xml中声明系统窗口权限,即增加下面这句:
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
- 在自定义悬浮窗控件中,要设置触摸监听器,并根据用户的手势滑动来相应调整窗口位置,以实现悬浮窗的拖动功能。
- 合理设置悬浮窗的窗口参数,主要是把窗口参数的显示类型设置为TYPE_SYSTEM_ALERT或者TYPE_SYSTEM_ERROR,另外还要设置标志位为FLAG_NOT_FOCUSABLE。
- 在构造悬浮窗实例时,要传入Application的上下文Context,这是为了保证即使退出Activity,也不会关闭悬浮窗。因为Application对象在App运行过程中是始终存在着的,而Activity对象只在打开页面时有效,一旦退出页面则Activity的上下文就立刻回收(这会导致依赖于该上下文的悬浮窗也一块被回收了)。
4.代码示例
首先需要一个悬浮窗类
FloatWindow.java
@SuppressLint("ClickableViewAccessibility")
public class FloatWindow extends View {
private final static String TAG = "FloatWindow";
private Context mContext; // 声明一个上下文对象
private WindowManager wm; // 声明一个窗口管理器对象
private static WindowManager.LayoutParams wmParams;
public View mContentView; // 声明一个内容视图对象
private float mScreenX, mScreenY; // 触摸点在屏幕上的横纵坐标
private float mLastX, mLastY; // 上次触摸点的横纵坐标
private float mDownX, mDownY; // 按下点的横纵坐标
private boolean isShowing = false; // 是否正在显示
public FloatWindow(Context context) {
super(context);
// 从系统服务中获取窗口管理器,后续将通过该管理器添加悬浮窗
wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
if (wmParams == null) {
wmParams = new WindowManager.LayoutParams();
}
mContext = context;
}
// 设置悬浮窗的内容布局
public void setLayout(int layoutId) {
// 从指定资源编号的布局文件中获取内容视图对象
mContentView = LayoutInflater.from(mContext).inflate(layoutId, null);
// 接管悬浮窗的触摸事件,使之即可随手势拖动,又可处理点击动作
mContentView.setOnTouchListener(new OnTouchListener() {
// 在发生触摸事件时触发
public boolean onTouch(View v, MotionEvent event) {
mScreenX = event.getRawX();
mScreenY = event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: // 手指按下
mDownX = mScreenX;
mDownY = mScreenY;
break;
case MotionEvent.ACTION_MOVE: // 手指移动
updateViewPosition(); // 更新视图的位置
break;
case MotionEvent.ACTION_UP: // 手指松开
updateViewPosition(); // 更新视图的位置
// 响应悬浮窗的点击事件
if (Math.abs(mScreenX - mDownX) < 3
&& Math.abs(mScreenY - mDownY) < 3) {
if (mListener != null) {
mListener.onFloatClick(v);
}
}
break;
}
mLastX = mScreenX;
mLastY = mScreenY;
return true;
}
});
}
// 更新悬浮窗的视图位置
private void updateViewPosition() {
// 此处不能直接转为整型,因为小数部分会被截掉,重复多次后就会造成偏移越来越大
wmParams.x = Math.round(wmParams.x + mScreenX - mLastX);
wmParams.y = Math.round(wmParams.y + mScreenY - mLastY);
// 通过窗口管理器更新内容视图的布局参数
wm.updateViewLayout(mContentView, wmParams);
}
// 显示悬浮窗
public void show() {
if (mContentView != null) {
// 设置为TYPE_SYSTEM_ALERT类型,才能悬浮在其它页面之上
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
// 注意TYPE_SYSTEM_ALERT从Android8.0开始被舍弃了
wmParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
} else {
// 从Android8.0开始悬浮窗要使用TYPE_APPLICATION_OVERLAY
wmParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
}
wmParams.format = PixelFormat.RGBA_8888;
wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
wmParams.alpha = 1.0f; // 1.0为完全不透明,0.0为完全透明
// 对齐方式为靠左且靠上,因此悬浮窗的初始位置在屏幕的左上角
wmParams.gravity = Gravity.LEFT | Gravity.TOP;
wmParams.x = 0;
wmParams.y = 0;
// 设置悬浮窗的宽度和高度为自适应
wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
// 添加自定义的窗口布局,然后屏幕上就能看到悬浮窗了
wm.addView(mContentView, wmParams);
isShowing = true;
}
}
// 关闭悬浮窗
public void close() {
if (mContentView != null) {
// 移除自定义的窗口布局
wm.removeView(mContentView);
isShowing = false;
}
}
// 判断悬浮窗是否打开
public boolean isShow() {
return isShowing;
}
private FloatClickListener mListener; // 声明一个悬浮窗的点击监听器对象
// 设置悬浮窗的点击监听器
public void setOnFloatListener(FloatClickListener listener) {
mListener = listener;
}
// 定义一个悬浮窗的点击监听器接口,用于触发点击行为
public interface FloatClickListener {
void onFloatClick(View v);
}
}
FlowUtil.java
public class FlowUtil {
private final static float divUnit = 1024.00f;
private final static int cmpUnit = 1000;
public final static String ZEROB = "0B";
// 格式化流量字符串
public static String BToShowString(long flowB, int decimal) {
NumberFormat ddf1 = NumberFormat.getNumberInstance();
ddf1.setMaximumFractionDigits(decimal);
if (flowB <= 0) {
return ZEROB;
}
if (flowB < cmpUnit) {
return flowB + "B";
}
if (flowB / cmpUnit < cmpUnit) {
double res = (double) flowB / divUnit;
return ddf1.format(res) + "K";
}
if (flowB / cmpUnit / cmpUnit < cmpUnit) {
double res = (double) flowB / divUnit;
res /= divUnit;
return ddf1.format(res) + "M";
}
double res = (double) flowB / divUnit;
res /= divUnit;
res /= divUnit;
return ddf1.format(res) + "G";
}
}
PermissionUtil.java
public class PermissionUtil {
public static void gotoPermission(Context context) {
String brand = Build.BRAND;//手机厂商
if (TextUtils.equals(brand.toLowerCase(), "redmi") || TextUtils.equals(brand.toLowerCase(), "xiaomi")) {
PermissionUtil.gotoMiuiPermission(context);//小米
} else if (TextUtils.equals(brand.toLowerCase(), "meizu")) {
PermissionUtil.gotoMeizuPermission(context);
} else if (TextUtils.equals(brand.toLowerCase(), "huawei") || TextUtils.equals(brand.toLowerCase(), "honor")) {
PermissionUtil.gotoHuaweiPermission(context);
} else {
context.startActivity(PermissionUtil.getAppDetailSettingIntent(context));
}
}
/**
* 跳转到miui的权限管理页面
*/
private static void gotoMiuiPermission(Context context) {
try { // MIUI 8
Intent localIntent = new Intent("miui.intent.action.APP_PERM_EDITOR");
localIntent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.PermissionsEditorActivity");
localIntent.putExtra("extra_pkgname", context.getPackageName());
context.startActivity(localIntent);
} catch (Exception e) {
try { // MIUI 5/6/7
Intent localIntent = new Intent("miui.intent.action.APP_PERM_EDITOR");
localIntent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.AppPermissionsEditorActivity");
localIntent.putExtra("extra_pkgname", context.getPackageName());
context.startActivity(localIntent);
} catch (Exception e1) { // 否则跳转到应用详情
context.startActivity(getAppDetailSettingIntent(context));
}
}
}
/**
* 跳转到魅族的权限管理系统
*/
private static void gotoMeizuPermission(Context context) {
try {
Intent intent = new Intent("com.meizu.safe.security.SHOW_APPSEC");
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.putExtra("packageName", BuildConfig.APPLICATION_ID);
context.startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
context.startActivity(getAppDetailSettingIntent(context));
}
}
/**
* 华为的权限管理页面
*/
private static void gotoHuaweiPermission(Context context) {
try {
Intent intent = new Intent();
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
ComponentName comp = new ComponentName("com.huawei.systemmanager", "com.huawei.permissionmanager.ui.MainActivity");//华为权限管理
intent.setComponent(comp);
context.startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
context.startActivity(getAppDetailSettingIntent(context));
}
}
/**
* 获取应用详情页面intent(如果找不到要跳转的界面,也可以先把用户引导到系统设置页面)
*/
private static Intent getAppDetailSettingIntent(Context context) {
Intent localIntent = new Intent();
localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
localIntent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
localIntent.setData(Uri.fromParts("package", context.getPackageName(), null));
return localIntent;
}
}
TrafficService.java
@SuppressLint("SetTextI18n")
public class TrafficService extends Service {
private final static String TAG = "TrafficService";
private FloatWindow mFloatWindow; // 声明一个悬浮窗对象
private TextView tv_traffic;
public static int OPEN = 0; // 打开悬浮窗
public static int CLOSE = 1; // 关闭悬浮窗
private long curRx; // 当前接收的流量
private long curTx; // 当前发送的流量
private final int delayTime = 2000; // 刷新的间隔时间
// 创建一个处理器对象
private Handler mHandler = new Handler();
// 定义一个流量刷新任务
private Runnable mRefresh = new Runnable() {
public void run() {
if (mFloatWindow != null && mFloatWindow.isShow() &&
(TrafficStats.getTotalRxBytes() > curRx || TrafficStats.getTotalTxBytes() > curTx)) {
// 平均一下接收的流量和发送的流量
long flow = ((TrafficStats.getTotalRxBytes() - curRx) + (TrafficStats
.getTotalTxBytes() - curTx)) / 2;
String desc = String.format("即时流量: %s/S", FlowUtil.BToShowString(flow, 0));
tv_traffic.setText(desc);
// 获取接收流量的总字节数
curRx = TrafficStats.getTotalRxBytes();
// 获取发送流量的总字节数
curTx = TrafficStats.getTotalTxBytes();
}
// 延迟若干秒后再次启动流量刷新任务
mHandler.postDelayed(this, delayTime);
}
};
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
if (mFloatWindow == null) {
// 创建一个新的悬浮窗
mFloatWindow = new FloatWindow(MyApplication.getInstance());
// 设置悬浮窗的布局内容
mFloatWindow.setLayout(R.layout.float_traffic);
// 从布局文件中获取展示即时流量的文本视图
tv_traffic = mFloatWindow.mContentView.findViewById(R.id.tv_traffic);
}
// 获取接收流量的总字节数
curRx = TrafficStats.getTotalRxBytes();
// 获取发送流量的总字节数
curTx = TrafficStats.getTotalTxBytes();
// 立即启动流量刷新任务
mHandler.post(mRefresh);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null) {
// 从意图中解包获得操作类型
int type = intent.getIntExtra("type", OPEN);
if (type == OPEN) { // 打开
if (mFloatWindow != null && !mFloatWindow.isShow()) {
tv_traffic.setText("即时流量: 0B/S");
mFloatWindow.show(); // 显示悬浮窗
}
} else if (type == CLOSE) { // 关闭
if (mFloatWindow != null && mFloatWindow.isShow()) {
mFloatWindow.close(); // 关闭悬浮窗
}
stopSelf(); // 停止自身服务
}
}
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
// 移除股指刷新任务
mHandler.removeCallbacks(mRefresh);
}
}
float_traffic.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical" >
<TextView
android:id="@+id/tv_traffic"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#99000000"
android:textColor="#FFFFFF"
android:textSize="15sp" />
</LinearLayout>
MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
private TextView tv_open;
private TextView tv_close;
private boolean backFromSetting;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv_open = findViewById(R.id.tv_open);
tv_close = findViewById(R.id.tv_close);
tv_open.setOnClickListener(this);
tv_close.setOnClickListener(this);
}
@Override
public void onClick(View v) {
Intent intent = null;
switch (v.getId()){
case R.id.tv_open:
// 下面携带打开类型启动流量服务
if (checkAlertWindowsPermission(this)){
//已经允许
intent = new Intent(this, TrafficService.class);
intent.putExtra("type", TrafficService.OPEN);
startService(intent);
}else{
//已经禁止
new AlertDialog.Builder(this).setTitle("需要打开悬浮窗权限,是否现在前往")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
//点击确定触发的事件
backFromSetting = true;
PermissionUtil.gotoPermission(MainActivity.this);
}
})
.setNegativeButton("返回", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
//点击取消触发的事件
}
}).show();
}
break;
case R.id.tv_close:
// 下面携带关闭类型启动流量服务
intent = new Intent(this, TrafficService.class);
intent.putExtra("type", TrafficService.CLOSE);
startService(intent);
break;
}
}
@Override
protected void onResume() {
super.onResume();
if (backFromSetting){
backFromSetting = false;
if (checkAlertWindowsPermission(this)){
//已经允许
Intent intent = new Intent(this, TrafficService.class);
intent.putExtra("type", TrafficService.OPEN);
startService(intent);
}else{
//已经禁止
ToastUtil.toastWord(this,"您未允许悬浮窗权限,无法打开悬浮窗");
}
}
}
/**
* 判断 悬浮窗口权限是否打开
* @param context
* @return true 允许 false禁止
*/
public boolean checkAlertWindowsPermission(Context context) {
try {
Object object = context.getSystemService(Context.APP_OPS_SERVICE);
if (object == null) {
return false;
}
Class localClass = object.getClass();
Class[] arrayOfClass = new Class[3];
arrayOfClass[0] = Integer.TYPE;
arrayOfClass[1] = Integer.TYPE;
arrayOfClass[2] = String.class;
Method method = localClass.getMethod("checkOp", arrayOfClass);
if (method == null) {
return false;
}
Object[] arrayOfObject1 = new Object[3];
arrayOfObject1[0] = 24;
arrayOfObject1[1] = Binder.getCallingUid();
arrayOfObject1[2] = context.getPackageName();
int m = ((Integer) method.invoke(object, arrayOfObject1));
return m == AppOpsManager.MODE_ALLOWED;
} catch (Exception ex) {
}
return false;
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tv_open"
android:layout_width="100dp"
android:layout_height="100dp"
android:text="打开流量"
android:gravity="center"
android:background="@android:color/holo_green_light"/>
<TextView
android:id="@+id/tv_close"
android:layout_width="100dp"
android:layout_height="100dp"
android:text="关闭流量"
android:layout_marginTop="100dp"
android:gravity="center"
android:background="@android:color/holo_green_light"/>
</LinearLayout>
添加权限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
清单文件AndroidManifest.xml的Application节点下配置Service
<service android:name=".TrafficService"/>
这样就完成了一个悬浮窗的制作