欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android应用内悬浮窗的实现方案示例

程序员文章站 2023-08-13 14:58:23
1、悬浮窗的基本介绍 悬浮窗,大家应该也不陌生,凌驾于应用之上的一个小弹窗,实现上很简单,就是添加一个系统级别的窗口,android中通过windowmanagerser...

1、悬浮窗的基本介绍

悬浮窗,大家应该也不陌生,凌驾于应用之上的一个小弹窗,实现上很简单,就是添加一个系统级别的窗口,android中通过windowmanagerservice( wms)来管理所有的窗口,对于wms来说,管你是activity、toast、dialog,都不过是通过windowmanagerglobal.addview()添加的一个个view。

android中的窗口分为三个级别:

1.1 应用窗口,比如activity的窗口;

1.2 子窗口,依赖于父窗口,比如popupwindow;

1.3 系统窗口,比如状态栏、toast,目标悬浮窗就是系统窗口.

2、根据产品需求进行设计

先了解一下大概的产品需求:

1、悬浮窗需要跨越整个应用
2、需要与悬浮窗进行交互
3、悬浮窗得移动
4、点击跳转特定的页面
5、消息提示的拖拽小红点

需求很简单,但是如果估算没错,不下一周产品经理会添加新的需求,所以为了更好的后续扩展,需要进行合理的设计,主要分为以下几点:

1、悬浮窗自定义一个framelayout布局floatlayout,里面进行拖动及点击响应处理;
2、floatmonkservice,是一个服务,开启服务的时候创建悬浮窗;
3、floatcallback,交互接口,在floatmonkservice里面实现接口,用于交互;
4、floatwindowmanager,悬浮窗的管理,因为后续悬浮窗布局可能有好几个,可以在这里面进行切换;
5、homewatcherreceiver,广播接收者,因为在应用内展示,需要监听用户在点击home键和切换键的时候隐藏悬浮窗,需要floatmonkservice里头动态注册;
6、floatactioncontroller,其实就是代理,其它模块需要通过它来和悬浮窗进行交互,真正干活的是实现floatcallback接口的floatmonkservice;
7、floatpermissionmanager,需要适配各个傻逼机型的权限,庆幸网上已有大佬分享,只需要单独对7.0系统进行一些适配就行,;
8、拖拽控件draggableflagview,直接拿来在悬浮窗上出现很奇怪的问题,所以需要改造一下下才能达到图中效果。

3、具体实现

float_littlemonk_layout.xml

<framelayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:dfv="http://schemas.android.com/apk/res-auto"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:gravity="center"
  android:orientation="vertical">

  <relativelayout
    android:id="@+id/monk_relative_root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <imageview
      android:id="@+id/float_id"
      android:layout_width="70dp"
      android:layout_height="80dp"
      android:layout_gravity="center_vertical|end"
      android:scaletype="center"
      android:src="@drawable/little_monk" />

  </relativelayout>

  <framelayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <floatwindow.xishuang.float_lib.view.draggableflagview
      android:id="@+id/main_dfv"
      android:layout_width="17dp"
      android:layout_height="17dp"
      android:layout_gravity="end"
      dfv:color1="#ff3b30" />
  </framelayout>
</framelayout>

简单的布局,就是一张图片+右上角放一个自定义的小红点。

floatlayout.java

@override
  public boolean ontouchevent(motionevent event) {
    // 获取相对屏幕的坐标,即以屏幕左上角为原点
    int x = (int) event.getrawx();
    int y = (int) event.getrawy();
    //下面的这些事件,跟图标的移动无关,为了区分开拖动和点击事件
    int action = event.getaction();
    switch (action) {
      case motionevent.action_down:
        starttime = system.currenttimemillis();
        mtouchstartx = event.getx();
        mtouchstarty = event.gety();
        break;
      case motionevent.action_move:
        //图标移动的逻辑在这里
        float mmovestartx = event.getx();
        float mmovestarty = event.gety();
        // 如果移动量大于3才移动
        if (math.abs(mtouchstartx - mmovestartx) > 3
            && math.abs(mtouchstarty - mmovestarty) > 3) {
          // 更新浮动窗口位置参数
          mwmparams.x = (int) (x - mtouchstartx);
          mwmparams.y = (int) (y - mtouchstarty);
          mwindowmanager.updateviewlayout(this, mwmparams);
          return false;
        }
        break;
      case motionevent.action_up:
        endtime = system.currenttimemillis();
        //当从点击到弹起小于半秒的时候,则判断为点击,如果超过则不响应点击事件
        if ((endtime - starttime) > 0.1 * 1000l) {
          isclick = false;
        } else {
          isclick = true;
        }
        break;
    }
    //响应点击事件
    if (isclick) {
      toast.maketext(mcontext, "我是大傻叼", toast.length_short).show();
    }
    return true;
  }

为了把悬浮窗的view操作抽离出来,自定义了这个布局,主要进行两部分功能,悬浮窗的移动和点击处理,重点是通过mwindowmanager.updateviewlayout(this, mwmparams)来进行悬浮窗的位置移动,我这个demo里面只是简单的通过时间来判断点击事件,有必要的话点击事件需要添加特定view范围判断来响应点击。

// 如果移动量大于3才移动
if (math.abs(mtouchstartx - mmovestartx) > 3 && math.abs(mtouchstarty - mmovestarty) > 3) 

这个判断是为了避免点击悬浮窗不在重心位置会出现移动的现象。

floatmonkservice.java

/**
 * 悬浮窗在服务中创建,通过暴露接口floatcallback与activity进行交互
 */
public class floatmonkservice extends service implements floatcallback {
  /**
   * home键监听
   */
  private homewatcherreceiver mhomekeyreceiver;

  @override
  public void oncreate() {
    super.oncreate();
    floatactioncontroller.getinstance().registercalllittlemonk(this);
    //注册广播接收者
    mhomekeyreceiver = new homewatcherreceiver();
    final intentfilter homefilter = new intentfilter(intent.action_close_system_dialogs);
    registerreceiver(mhomekeyreceiver, homefilter);
    //初始化悬浮窗ui
    initwindowdata();
  }

  @override
  public ibinder onbind(intent intent) {
    return null;
  }

  /**
   * 初始化windowmanager
   */
  private void initwindowdata() {
    floatwindowmanager.createfloatwindow(this);
  }

  @override
  public void ondestroy() {
    super.ondestroy();
    //移除悬浮窗
    floatwindowmanager.removefloatwindowmanager();
    //注销广播接收者
    if (null != mhomekeyreceiver) {
      unregisterreceiver(mhomekeyreceiver);
    }
  }

  /////////////////////////////////////////////////////////实现接口////////////////////////////////////////////////////
  @override
  public void guideuser(int type) {
    floatwindowmanager.updataredanddialog(this);
  }


  /**
   * 悬浮窗的隐藏
   */
  @override
  public void hide() {
    floatwindowmanager.hide();
  }

  /**
   * 悬浮窗的显示
   */
  @override
  public void show() {
    floatwindowmanager.show();
  }

  /**
   * 添加可领取的数量
   */
  @override
  public void addobtainnumer() {
    floatwindowmanager.addobtainnumer(this);
    guideuser(4);
  }

  /**
   * 减少可领取的数量
   */
  @override
  public void setobtainnumber(int number) {
    floatwindowmanager.setobtainnumber(this, number);
  }
}

服务开启的时候通过floatwindowmanager.createfloatwindow(this)来创建悬浮窗,实现floatcallback 实现需要交互的接口。下面看一下创建悬浮窗的真正操作是怎样的。

floatwindowmanager.java

/**
   * 创建一个小悬浮窗。初始位置为屏幕的右下角位置。
   */
  public static void createfloatwindow(context context) {
    wmparams = new windowmanager.layoutparams();
    windowmanager windowmanager = getwindowmanager(context);
    mfloatlayout = new floatlayout(context);
    if (build.version.sdk_int >= 24) { /*android7.0不能用type_toast*/
      wmparams.type = windowmanager.layoutparams.type_phone;
    } else { /*以下代码块使得android6.0之后的用户不必再去手动开启悬浮窗权限*/
      string packname = context.getpackagename();
      packagemanager pm = context.getpackagemanager();
      boolean permission = (packagemanager.permission_granted == pm.checkpermission("android.permission.system_alert_window", packname));
      if (permission) {
        wmparams.type = windowmanager.layoutparams.type_phone;
      } else {
        wmparams.type = windowmanager.layoutparams.type_toast;
      }
    }

    //设置图片格式,效果为背景透明
    wmparams.format = pixelformat.rgba_8888;
    //设置浮动窗口不可聚焦(实现操作除浮动窗口外的其他可见窗口的操作)
    wmparams.flags = windowmanager.layoutparams.flag_not_focusable;
    //调整悬浮窗显示的停靠位置为左侧置顶
    wmparams.gravity = gravity.start | gravity.top;

    displaymetrics dm = new displaymetrics();
    //取得窗口属性
    mwindowmanager.getdefaultdisplay().getmetrics(dm);
    //窗口的宽度
    int screenwidth = dm.widthpixels;
    //窗口高度
    int screenheight = dm.heightpixels;
    //以屏幕左上角为原点,设置x、y初始值,相对于gravity
    wmparams.x = screenwidth;
    wmparams.y = screenheight;

    //设置悬浮窗口长宽数据
    wmparams.width = windowmanager.layoutparams.wrap_content;
    wmparams.height = windowmanager.layoutparams.wrap_content;
    mfloatlayout.setparams(wmparams);
    windowmanager.addview(mfloatlayout, wmparams);
    mhasshown = true;
    //是否展示小红点展示
    checkreddot(context);
  }

/**
   * 返回当前已创建的windowmanager。
   */
  private static windowmanager getwindowmanager(context context) {
    if (mwindowmanager == null) {
      mwindowmanager = (windowmanager) context.getsystemservice(context.window_service);
    }
    return mwindowmanager;
  }

核心代码其实就是mwindowmanager = (windowmanager) context.getsystemservice(context.window_service),其中的context不能是activity的,一开始就说了,activity会返回它专享的windowmanager,而activity的窗口级别是属于应用层的。进行一些初始化操作之后 windowmanager.addview(mfloatlayout, wmparams)把布局添加进去就ok了。

 if (build.version.sdk_int >= 24) { /*android7.0不能用type_toast*/
      wmparams.type = windowmanager.layoutparams.type_phone;
    } else { /*以下代码块使得android6.0之后的用户不必再去手动开启悬浮窗权限*/
      string packname = context.getpackagename();
      packagemanager pm = context.getpackagemanager();
      boolean permission = (packagemanager.permission_granted == pm.checkpermission("android.permission.system_alert_window", packname));
      if (permission) {
        wmparams.type = windowmanager.layoutparams.type_phone;
      } else {
        wmparams.type = windowmanager.layoutparams.type_toast;
      }
    }

说一下这段代码的意义,当windowmanager.layoutparams.type设置为windowmanager.layoutparams.type_toast的时候,是可以跳过权限申请的,但是为毛又单独适配各个机型呢,因为我们有小米android系统,魅族android系统,还有华为等等android系统,特别是产品经理的魅族,一些特殊机型上是没有效果的,所以为了更保险,得再加一份权限申请,还有一点得提一下,那就是7.0上windowmanager.layoutparams.type_toast,悬浮窗只能持续一秒的时间,所以7.0不设这个type,谷歌爸爸最叼,7.0以上老老实实申请权限。

floatactioncontroller.java

/**
 * author:xishuang
 * date:2017.08.01
 * des:与悬浮窗交互的控制类,真正的实现逻辑不在这
 */
public class floatactioncontroller {

  private floatactioncontroller() {
  }

  public static floatactioncontroller getinstance() {
    return littlemonkproviderholder.sinstance;
  }

  // 静态内部类
  private static class littlemonkproviderholder {
    private static final floatactioncontroller sinstance = new floatactioncontroller();
  }

  private floatcallback mcalllittlemonk;

  /**
   * 开启服务悬浮窗
   */
  public void startmonkserver(context context) {
    intent intent = new intent(context, floatmonkservice.class);
    context.startservice(intent);
  }

  /**
   * 关闭悬浮窗
   */
  public void stopmonkserver(context context) {
    intent intent = new intent(context, floatmonkservice.class);
    context.stopservice(intent);
  }

  /**
   * 注册监听
   */
  public void registercalllittlemonk(floatcallback calllittlemonk) {
    mcalllittlemonk = calllittlemonk;
  }

  /**
   * 悬浮窗的显示
   */
  public void show() {
    if (mcalllittlemonk == null) return;
    mcalllittlemonk.show();
  }

  /**
   * 悬浮窗的隐藏
   */
  public void hide() {
    if (mcalllittlemonk == null) return;
    mcalllittlemonk.hide();
  }
}

这就是暴露出来的接口,按需添加,效果大概是这样的。

大概效果如下:

Android应用内悬浮窗的实现方案示例

demo:代码地址感兴趣可以看看完整的。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。