一直都感觉悬浮窗是个非常好用的东西,无论在什么界面上都能显示不受activity等限制。但是随着Android版本的不断升级,使用却变得越来越麻烦。
坑1、3.0之后获取正在运行的任务需要声明权限了;
在AndroidManifest.xml中声明权限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
坑2、5.0之后原先获取正在运行任务的getRunningTasks被废弃了,可以使用usage代替,但查看应用使用情况需要手动开启权限了;
如果想实现类似于360悬浮窗的效果,只在当前界面是桌面的时候才显示悬浮窗,在其它界面情况下不显示,那我们需要判断当前是否是在桌面。5.0之后可以通过UsageStatsManager获取一段时间内运行的程序,我们可以通过这来判断当前是否是在桌面上。
/**
* 获取所有桌面应用
*
* @param context
* @return
*/
public static List<String> getHomeList(Context context) {
List<String> list = new ArrayList<String>();
PackageManager packageManager = context.getPackageManager();
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_HOME);
List<ResolveInfo> resolveInfoList = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resolveInfoList) {
list.add(resolveInfo.activityInfo.packageName);
}
Log.i("homeList",list.toString());
return list;
}
/**
* 当前界面是否是桌面
* 分5.0之前和之后版本
*
* @param context
* @return
*/
public static boolean isHome(Context context) {
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
String packname = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//5.0之后getRunningTasks废弃,引入usage
//<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" tools:ignore="ProtectedPermissions" />
UsageStatsManager usageStatsManager = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
long currentTime = System.currentTimeMillis();
List<UsageStats> usageStatsList = usageStatsManager.queryUsageStats(UsageStatsManager.INTERVAL_BEST, currentTime - 2000, currentTime);
if (usageStatsList != null || usageStatsList.isEmpty()) {
SortedMap<Long, UsageStats> usageStatsSortedMap = new TreeMap<Long, UsageStats>();
for (UsageStats usageStats : usageStatsList) {
usageStatsSortedMap.put(usageStats.getLastTimeUsed(), usageStats);
}
if (!usageStatsSortedMap.isEmpty()) {
packname = usageStatsSortedMap.get(usageStatsSortedMap.lastKey()).getPackageName();
}
}
} else {
//5.0之前可直接使用getRunningTasks
//<uses-permission android:name="android.permission.GET_TASKS"/>
List<ActivityManager.RunningTaskInfo> runningTaskInfoList = activityManager.getRunningTasks(1);
packname = runningTaskInfoList.get(0).topActivity.getPackageName();
}
Log.i("isHome", packname == null ? "null" : packname);
return getHomeList(context).contains(packname);
}
在5.0之后我获取了当前时间之前2秒到当前时间运行在界面上的应用程序,但是我发现,如果我停留在一个界面超过2秒,我获取到的应用使用列表为空,我就无法获取最近一次使用的程序的包名。想了个非常傻的方法,另外又加了一个获取最近2秒内运行在界面上的程序包名的方法,如果一直停留就返回null,在后台服务判断当前是否在桌面运行时多加一个判断。
/**
* 获取最近2秒内开始运行在界面上的程序包名
* 若最近2秒一直停留在某一界面,则返回null
* @param context
* @return
*/
public static String getLatestPackage(Context context){
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
String packname = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//5.0之后getRunningTasks废弃,引入usage
//<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" tools:ignore="ProtectedPermissions" />
UsageStatsManager usageStatsManager = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
long currentTime = System.currentTimeMillis();
List<UsageStats> usageStatsList = usageStatsManager.queryUsageStats(UsageStatsManager.INTERVAL_BEST, currentTime - 2000, currentTime);
if (usageStatsList != null || usageStatsList.isEmpty()) {
SortedMap<Long, UsageStats> usageStatsSortedMap = new TreeMap<Long, UsageStats>();
for (UsageStats usageStats : usageStatsList) {
usageStatsSortedMap.put(usageStats.getLastTimeUsed(), usageStats);
}
if (!usageStatsSortedMap.isEmpty()) {
packname = usageStatsSortedMap.get(usageStatsSortedMap.lastKey()).getPackageName();
}
}
} else {
//5.0之前可直接使用getRunningTasks
//<uses-permission android:name="android.permission.GET_TASKS"/>
List<ActivityManager.RunningTaskInfo> runningTaskInfoList = activityManager.getRunningTasks(1);
packname = runningTaskInfoList.get(0).topActivity.getPackageName();
}
Log.i("latest",packname==null?"null":packname);
return packname;
}
/**
* 后台检测刷新任务
*/
class RefreshTask extends TimerTask {
@TargetApi(Build.VERSION_CODES.M)
@Override
public void run() {
if(AppUtil.getLatestPackage(context)==null){
//最近运行的app包名为null执行刷新小悬浮窗操作
handler.post(new Runnable() {
@Override
public void run() {
//已显示悬浮窗,更新小悬浮窗
FloatWindowManager.getInstance(context).updateFloatWindowSmall();
}
});
return;
}
if(AppUtil.isHome(context)){
//当前在桌面上则显示或更新悬浮窗
if (FloatWindowManager.getInstance(context).isFloatWindowShowing()) {
handler.post(new Runnable() {
@Override
public void run() {
//已显示悬浮窗,更新小悬浮窗
FloatWindowManager.getInstance(context).updateFloatWindowSmall();
}
});
} else if (!FloatWindowManager.getInstance(context).isFloatWindowShowing()) {
handler.post(new Runnable() {
@Override
public void run() {
//未显示悬浮窗,则显示小悬浮窗
FloatWindowManager.getInstance(context).showFloatWindowSmall(context);
}
});
}
}else{
//当前不在桌面则关闭所有悬浮窗
handler.post(new Runnable() {
@Override
public void run() {
FloatWindowManager.getInstance(context).closeAllFloatWindow();
}
});
}
}
}
关于如何开启查看应用使用情况的权限,我们也只能跳到设置界面让用户手动去开启
/**
* 授权查看应用使用情况权限的点击事件
* @param view
*/
public void authorizationUsage(View view){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
startActivity(new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS));
} else {
Toast.makeText(this, "只有5.0以上手机需要查看应用使用情况权限", Toast.LENGTH_SHORT).show();
}
}
坑3、6.0之后悬浮窗权限被默认关闭,需要手动开启了;
Google在Android6.0中收紧了悬浮窗口的权限,因为大量App使用悬浮窗口的目的和当初Google预想的大相径庭,Google当初只是想让悬浮窗口被老老实实拿来做用户通知,但这个特性成为了令人眼花缭乱甚至会影响使用的交互工具。因此,Google在Android6.0中默认禁止了悬浮窗口(FloatWindow)的权限,并让它不能被轻易打开。我们需要像开启查看应用使用情况的权限一样,跳转至设置界面让用户手动开启。
/**
* 授权悬浮窗的点击事件
* @param view
*/
public void authorizationFloatWindow(View view) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
startActivity(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION));
} else {
Toast.makeText(this, "只有6.0以上手机需要开启悬浮窗权限", Toast.LENGTH_SHORT).show();
}
}
悬浮窗使用
踩完坑之后就是常规使用了,参考了郭神的博客。
/**
* 小悬浮窗
* 简单的数字时钟,可点击打开大悬浮窗,可随意拖动位置
*
* @author SJL
* @date 2016/11/30 21:36
*/
public class FloatWindowSmall extends LinearLayout {
private Context context;
private WindowManager windowManager;
public WindowManager.LayoutParams layoutParams;
public FloatWindowSmall(Context context) {
super(context);
this.context = context;
init();
}
private void init() {
LayoutInflater.from(context).inflate(R.layout.float_window_small, this);
windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
View view = findViewById(R.id.floatWindowSmallParent);
layoutParams = new WindowManager.LayoutParams();
//设置宽高
layoutParams.width = view.getLayoutParams().width;
layoutParams.height = view.getLayoutParams().height;
//设置位置
layoutParams.x = AppUtil.getScreen(context).x - layoutParams.width;
layoutParams.y = AppUtil.getScreen(context).y / 2 - layoutParams.height / 2;
//设置交互
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
//设置图片格式
layoutParams.format = PixelFormat.RGBA_8888;
//设置对齐
layoutParams.gravity = Gravity.TOP | Gravity.LEFT;
//设置显示类型
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
update();
}
public void update() {
((TextView) findViewById(R.id.tvTime)).setText(new SimpleDateFormat("HH:mm:ss", Locale.US).format(new Date()));
}
private float startX = -1;
private float startY = -1;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = event.getX();
startY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
layoutParams.x = (int) (event.getRawX() - startX);
layoutParams.y = (int) (event.getRawY() - startY);
windowManager.updateViewLayout(this, layoutParams);
break;
case MotionEvent.ACTION_UP:
if (event.getX() == startX && event.getY() == startY) {
openFloatWindowBig();
}
break;
}
return super.onTouchEvent(event);
}
/**
* 打开大悬浮窗
*/
private void openFloatWindowBig() {
FloatWindowManager.getInstance(context).closeFloatWindowSmall();
FloatWindowManager.getInstance(context).showFloatWindowBig(context);
}
}