Android悬浮快捷按钮

在开发常驻应用时,通过监听应用的前后台状态和利用悬浮窗,实现从系统设置界面快速返回应用的功能。文章介绍了如何结合application生命周期以及判断应用是否在前台运行,来控制悬浮窗的显示与隐藏,并提供了悬浮窗使用的开源框架链接。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

开发常驻应用时遇到这样的需求:点击配网或者某一项跳转到了系统设置界面,这时候要求界面上出现一个悬浮按钮,点击悬浮按钮后要能够快速返回到自己的应用。这里面主要涉及到两个点:悬浮窗、应用前后台切换

首先说一下应用前后台监听,这里是结合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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值