开发常驻应用时遇到这样的需求:点击配网或者某一项跳转到了系统设置界面,这时候要求界面上出现一个悬浮按钮,点击悬浮按钮后要能够快速返回到自己的应用。这里面主要涉及到两个点:悬浮窗、应用前后台切换
首先说一下应用前后台监听,这里是结合application生命周期和APP是否在前台或后台运行做的处理:
private int mFinalCount;
private void initLifeCycle() {
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
@Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
LogMgr.d(TAG, "onActivityCreated");
}
@Override public void onActivityStarted(Activity activity) {
LogMgr.d(TAG, "onActivityStarted");
mFinalCount++;
//如果mFinalCount ==1,说明是从后台到前台
if (mFinalCount == 1 && !(TopActivityManager.getInstance()
.getCurrentActivity() instanceof FlashActivity)) {
//说明从后台回到了前台
FloatWindowManager.getInstance().dismissWindow();
}
}
@Override public void onActivityResumed(Activity activity) {
LogMgr.d(TAG, "onActivityResumed");
}
@Override public void onActivityPaused(Activity activity) {
LogMgr.d(TAG, "onActivityPaused");
}
@Override public void onActivityStopped(Activity activity) {
LogMgr.d(TAG, "onActivityStopped");
mFinalCount--;
//如果mFinalCount == 0,说明是前台到后台
if (mFinalCount == 0 && SystemHelper.isRunningBackground(ContextUtils.getContext())) {
FloatWindowManager.getInstance().applyOrShowFloatWindow(getApplicationContext());
}
}
@Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
LogMgr.d(TAG, "onActivitySaveInstanceState");
}
@Override public void onActivityDestroyed(Activity activity) {
LogMgr.d(TAG, "onActivityDestroyed");
}
});
}
这里通过一个变量记录当前打开的activity个数,配合当前显示的activity和是否在后端运行来判断出前后台切换,同时显示和隐藏掉悬浮窗
悬浮窗代码:
package com.unisound.smartphone.utils;
import android.app.ActivityManager;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import android.util.Log;
import android.view.Gravity;
import android.view.WindowManager;
import com.unisound.sdk.service.utils.ContextUtils;
import com.unisound.sdk.service.utils.LogMgr;
import com.unisound.smartphone.ui.AVCallFloatView;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
public class FloatWindowManager {
private static final String TAG = "FloatWindowManager";
private static volatile FloatWindowManager instance;
private boolean isWindowDismiss = true;
private WindowManager windowManager = null;
private WindowManager.LayoutParams mParams = null;
private AVCallFloatView floatView = null;
private Dialog dialog;
public static FloatWindowManager getInstance() {
if (instance == null) {
synchronized (FloatWindowManager.class) {
if (instance == null) {
instance = new FloatWindowManager();
}
}
}
return instance;
}
public void applyOrShowFloatWindow(Context context) {
if (isRunBackground(context)) {
if (checkPermission(context)) {
showWindow(context);
} else {
applyPermission(context);
}
}
}
private boolean checkPermission(Context context) {
return commonROMPermissionCheck(context);
}
private boolean commonROMPermissionCheck(Context context) {
Boolean result = true;
if (Build.VERSION.SDK_INT >= 23) {
try {
Class clazz = Settings.class;
Method canDrawOverlays = clazz.getDeclaredMethod("canDrawOverlays", Context.class);
result = (Boolean) canDrawOverlays.invoke(null, context);
} catch (Exception e) {
LogMgr.e(TAG, Log.getStackTraceString(e));
}
}
return result;
}
private void applyPermission(Context context) {
commonROMPermissionApply(context);
}
/**
* 通用 rom 权限申请
*/
private void commonROMPermissionApply(final Context context) {
if (Build.VERSION.SDK_INT >= 23) {
showConfirmDialog(context, new OnConfirmResult() {
@Override public void confirmResult(boolean confirm) {
if (confirm) {
try {
commonROMPermissionApplyInternal(context);
} catch (Exception e) {
LogMgr.e(TAG, Log.getStackTraceString(e));
}
} else {
LogMgr.d(TAG, "user manually refuse OVERLAY_PERMISSION");
//需要做统计效果
}
}
});
}
}
public static void commonROMPermissionApplyInternal(Context context)
throws NoSuchFieldException, IllegalAccessException {
Class clazz = Settings.class;
Field field = clazz.getDeclaredField("ACTION_MANAGE_OVERLAY_PERMISSION");
Intent intent = new Intent(field.get(null).toString());
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setData(Uri.parse("package:" + context.getPackageName()));
context.startActivity(intent);
}
private void showConfirmDialog(Context context, OnConfirmResult result) {
showConfirmDialog(context, "您的手机没有授予悬浮窗权限,请开启后再试", result);
}
private void showConfirmDialog(Context context, String message, final OnConfirmResult result) {
if (dialog != null && dialog.isShowing()) {
dialog.dismiss();
}
dialog = new AlertDialog.Builder(context).setCancelable(true)
.setTitle("")
.setMessage(message)
.setPositiveButton("现在去开启", new DialogInterface.OnClickListener() {
@Override public void onClick(DialogInterface dialog, int which) {
result.confirmResult(true);
dialog.dismiss();
}
})
.setNegativeButton("暂不开启", new DialogInterface.OnClickListener() {
@Override public void onClick(DialogInterface dialog, int which) {
result.confirmResult(false);
dialog.dismiss();
}
})
.create();
dialog.show();
}
private interface OnConfirmResult {
void confirmResult(boolean confirm);
}
private void showWindow(Context context) {
if (!isWindowDismiss) {
LogMgr.e(TAG, "view is already added here");
return;
}
isWindowDismiss = false;
if (windowManager == null) {
windowManager =
(WindowManager) context.getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
}
Point size = new Point();
windowManager.getDefaultDisplay().getSize(size);
int screenWidth = size.x;
int screenHeight = size.y;
mParams = new WindowManager.LayoutParams();
mParams.packageName = context.getPackageName();
mParams.width = dp2px(context, 50);
mParams.height = dp2px(context, 50);
mParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
int mType;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mType = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
mType = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
}
mParams.type = mType;
mParams.format = PixelFormat.RGBA_8888;
mParams.gravity = Gravity.LEFT | Gravity.TOP;
mParams.x = screenWidth - dp2px(context, 100);
mParams.y = screenHeight - dp2px(context, 171);
floatView = new AVCallFloatView(context);
floatView.setParams(mParams);
floatView.setIsShowing(true);
windowManager.addView(floatView, mParams);
}
public void dismissWindow() {
if (isWindowDismiss) {
LogMgr.e(TAG, "window can not be dismiss cause it has not been added");
return;
}
isWindowDismiss = true;
floatView.setIsShowing(false);
if (windowManager != null && floatView != null) {
windowManager.removeViewImmediate(floatView);
}
}
private int dp2px(Context context, float dp) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dp * scale + 0.5f);
}
/** 判断程序是否在后台运行 */
private boolean isRunBackground(Context context) {
ActivityManager activityManager =
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> appProcesses =
activityManager.getRunningAppProcesses();
for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
if (appProcess.processName.equals(context.getPackageName())) {
// 表明程序在后台运行
return appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_BACKGROUND;
}
}
return false;
}
/** 判断程序是否在前台运行(当前运行的程序) */
private boolean isRunForeground() {
ActivityManager activityManager = (ActivityManager) ContextUtils.getContext()
.getSystemService(Context.ACTIVITY_SERVICE);
String packageName = ContextUtils.getContext().getPackageName();
List<ActivityManager.RunningAppProcessInfo> appProcesses =
activityManager.getRunningAppProcesses();
if (appProcesses == null) return false;
for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
if (appProcess.processName.equals(packageName)
&& appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
// 程序运行在前台
return true;
}
}
return false;
}
}
package com.unisound.smartphone.ui;
import android.content.Context;
import android.graphics.Point;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.WindowManager;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import com.unisound.sdk.service.utils.ContextUtils;
import com.unisound.smartphone.R;
import com.unisound.smartphone.utils.SystemHelper;
public class AVCallFloatView extends FrameLayout {
private static final String TAG = "AVCallFloatView";
/**
* 记录手指按下时在小悬浮窗的View上的横坐标的值
*/
private float xInView;
/**
* 记录手指按下时在小悬浮窗的View上的纵坐标的值
*/
private float yInView;
/**
* 记录当前手指位置在屏幕上的横坐标值
*/
private float xInScreen;
/**
* 记录当前手指位置在屏幕上的纵坐标值
*/
private float yInScreen;
/**
* 记录手指按下时在屏幕上的横坐标的值
*/
private float xDownInScreen;
/**
* 记录手指按下时在屏幕上的纵坐标的值
*/
private float yDownInScreen;
private boolean isAnchoring = false;
private boolean isShowing = false;
private WindowManager windowManager = null;
private WindowManager.LayoutParams mParams = null;
public AVCallFloatView(Context context) {
super(context);
initView();
}
private void initView() {
windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
LayoutInflater inflater = LayoutInflater.from(getContext());
View floatView = inflater.inflate(R.layout.window_backtoapp, null);
addView(floatView);
}
public void setParams(WindowManager.LayoutParams params) {
mParams = params;
}
public void setIsShowing(boolean isShowing) {
this.isShowing = isShowing;
}
@Override public boolean onTouchEvent(MotionEvent event) {
if (isAnchoring) {
return true;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
xInView = event.getX();
yInView = event.getY();
xDownInScreen = event.getRawX();
yDownInScreen = event.getRawY();
xInScreen = event.getRawX();
yInScreen = event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
xInScreen = event.getRawX();
yInScreen = event.getRawY();
// 手指移动的时候更新小悬浮窗的位置
updateViewPosition();
break;
case MotionEvent.ACTION_UP:
if (Math.abs(xDownInScreen - xInScreen) <= ViewConfiguration.get(getContext())
.getScaledTouchSlop() && Math.abs(yDownInScreen - yInScreen) <= ViewConfiguration.get(
getContext()).getScaledTouchSlop()) {
// 点击效果
SystemHelper.setTopApp(ContextUtils.getContext());
} else {
//吸附效果
anchorToSide();
}
break;
default:
break;
}
return true;
}
private void anchorToSide() {
isAnchoring = true;
Point size = new Point();
windowManager.getDefaultDisplay().getSize(size);
int screenWidth = size.x;
int screenHeight = size.y;
int middleX = mParams.x + getWidth() / 2;
int animTime = 0;
int xDistance = 0;
int yDistance = 0;
int dp25 = dp2px(10);
if (middleX <= dp25 + getWidth() / 2) {
xDistance = dp25 - mParams.x;
} else if (middleX <= screenWidth / 2) {
xDistance = dp25 - mParams.x;
} else if (middleX >= screenWidth - getWidth() / 2 - dp25) {
xDistance = screenWidth - mParams.x - getWidth() - dp25;
} else {
xDistance = screenWidth - mParams.x - getWidth() - dp25;
}
if (mParams.y < dp25) {
yDistance = dp25 - mParams.y;
} else if (mParams.y + getHeight() + dp25 >= screenHeight) {
yDistance = screenHeight - dp25 - mParams.y - getHeight();
}
Log.e(TAG, "xDistance " + xDistance + " yDistance" + yDistance);
animTime =
Math.abs(xDistance) > Math.abs(yDistance) ? (int) (((float) xDistance / (float) screenWidth)
* 600f) : (int) (((float) yDistance / (float) screenHeight) * 900f);
this.post(new AnchorAnimRunnable(Math.abs(animTime), xDistance, yDistance,
System.currentTimeMillis()));
}
public int dp2px(float dp) {
final float scale = getContext().getResources().getDisplayMetrics().density;
return (int) (dp * scale + 0.5f);
}
private class AnchorAnimRunnable implements Runnable {
private int animTime;
private long currentStartTime;
private Interpolator interpolator;
private int xDistance;
private int yDistance;
private int startX;
private int startY;
public AnchorAnimRunnable(int animTime, int xDistance, int yDistance, long currentStartTime) {
this.animTime = animTime;
this.currentStartTime = currentStartTime;
interpolator = new AccelerateDecelerateInterpolator();
this.xDistance = xDistance;
this.yDistance = yDistance;
startX = mParams.x;
startY = mParams.y;
}
@Override public void run() {
if (System.currentTimeMillis() >= currentStartTime + animTime) {
if (mParams.x != (startX + xDistance) || mParams.y != (startY + yDistance)) {
mParams.x = startX + xDistance;
mParams.y = startY + yDistance;
windowManager.updateViewLayout(AVCallFloatView.this, mParams);
}
isAnchoring = false;
return;
}
float delta = interpolator.getInterpolation(
(System.currentTimeMillis() - currentStartTime) / (float) animTime);
int xMoveDistance = (int) (xDistance * delta);
int yMoveDistance = (int) (yDistance * delta);
Log.e(TAG, "delta: " + delta + " xMoveDistance " + xMoveDistance + " yMoveDistance "
+ yMoveDistance);
mParams.x = startX + xMoveDistance;
mParams.y = startY + yMoveDistance;
if (!isShowing) {
return;
}
windowManager.updateViewLayout(AVCallFloatView.this, mParams);
AVCallFloatView.this.postDelayed(this, 16);
}
}
private void updateViewPosition() {
//增加移动误差
mParams.x = (int) (xInScreen - xInView);
mParams.y = (int) (yInScreen - yInView);
Log.e(TAG, "x " + mParams.x + " y " + mParams.y);
windowManager.updateViewLayout(this, mParams);
}
}
<?xml version="1.0" encoding="utf-8"?>
<com.allen.library.CircleImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/ll_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@mipmap/ic_launcher"
>
</com.allen.library.CircleImageView>
还有几个用到的方法:判断后台前台运行和把应用提到前台:
/**
* 判断本地是否已经安装好了指定的应用程序包
*
* @param packageNameTarget :待判断的 App 包名,如 微博 com.sina.weibo
* @return 已安装时返回 true,不存在时返回 false
*/
public static boolean appIsExist(Context context, String packageNameTarget) {
if (!"".equals(packageNameTarget.trim())) {
PackageManager packageManager = context.getPackageManager();
List<PackageInfo> packageInfoList =
packageManager.getInstalledPackages(PackageManager.MATCH_UNINSTALLED_PACKAGES);
for (PackageInfo packageInfo : packageInfoList) {
String packageNameSource = packageInfo.packageName;
if (packageNameSource.equals(packageNameTarget)) {
return true;
}
}
}
return false;
}
/**
* 将本应用置顶到最前端
* 当本应用位于后台时,则将它切换到最前端
*/
public static void setTopApp(Context context) {
if (!isRunningForeground(context)) {
/**获取ActivityManager*/
ActivityManager activityManager =
(ActivityManager) context.getSystemService(ACTIVITY_SERVICE);
/**获得当前运行的task(任务)*/
List<ActivityManager.RunningTaskInfo> taskInfoList = activityManager.getRunningTasks(100);
for (ActivityManager.RunningTaskInfo taskInfo : taskInfoList) {
/**找到本应用的 task,并将它切换到前台*/
if (taskInfo.topActivity.getPackageName().equals(context.getPackageName())) {
activityManager.moveTaskToFront(taskInfo.id, 0);
break;
}
}
}
}
/**
* 判断本应用是否已经位于最前端
*
* @return 本应用已经位于最前端时,返回 true;否则返回 false
*/
public static boolean isRunningForeground(Context context) {
ActivityManager activityManager = (ActivityManager) context.getSystemService(ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> appProcessInfoList =
activityManager.getRunningAppProcesses();
/**枚举进程*/
for (ActivityManager.RunningAppProcessInfo appProcessInfo : appProcessInfoList) {
if (appProcessInfo.importance
== ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
if (appProcessInfo.processName.equals(context.getApplicationInfo().processName)) {
return true;
}
}
}
return false;
}
public static boolean isRunningBackground(Context context) {
ActivityManager activityManager = (ActivityManager) context.getSystemService(ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> runningAppProcesses =
activityManager.getRunningAppProcesses();
for (ActivityManager.RunningAppProcessInfo processInfo : runningAppProcesses) {
if (processInfo.processName.equals(context.getPackageName())) {
return processInfo.importance
== ActivityManager.RunningAppProcessInfo.IMPORTANCE_BACKGROUND;
}
}
return false;
}
悬浮窗是直接用的一个开源框架:
https://github.com/zhaozepeng/FloatWindowPermission