博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android应用内悬浮窗的实现方案
阅读量:6712 次
发布时间:2019-06-25

本文共 12614 字,大约阅读时间需要 42 分钟。

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、拖拽控件,直接拿来在悬浮窗上出现很奇怪的问题,所以需要改造一下下才能达到图中效果。

3、具体实现

float_littlemonk_layout.xml

复制代码

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

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();    }}复制代码

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

HomeWatcherReceiver.java

/** * Author:xishuang * Date:2017.08.01 * Des:一些Home建与切换键的广播监听,需要动态注册 */public class HomeWatcherReceiver extends BroadcastReceiver {    private static final String TAG = "HomeWatcherReceiver";    private static final String SYSTEM_DIALOG_FROM_KEY = "reason";    private static final String SYSTEM_DIALOG_FROM_RECENT_APPS = "recentapps";    private static final String SYSTEM_DIALOG_FROM_HOME_KEY = "homekey";    private static final String SYSTEM_DIALOG_FROM_LOCK = "lock";    @Override    public void onReceive(Context context, Intent intent) {        String action = intent.getAction();        Log.i(TAG, "onReceive: action: " + action);        //根据不同的信息进行一些个性操作        if (action.equals(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) {            String from = intent.getStringExtra(SYSTEM_DIALOG_FROM_KEY);            Log.i(TAG, "from: " + from);            if (SYSTEM_DIALOG_FROM_HOME_KEY.equals(from)) { //短按Home键                Log.i(TAG, "Home Key");                //按home键会直接关闭悬浮窗                FloatActionController.getInstance().stopMonkServer(context);            } else if (SYSTEM_DIALOG_FROM_RECENT_APPS.equals(from)) { //长按Home键或是Activity切换键                Log.i(TAG, "long press home key or activity switch");            } else if (SYSTEM_DIALOG_FROM_LOCK.equals(from)) { //锁屏操作                Log.i(TAG, "lock");            }        }    }}复制代码

这个就是一个广播接收者,需要监听系统的一些操作,然后根据不同的操作实现自己想要的逻辑,Demo中我只是针对Home键进行了简单的处理,点击Home退到主页会直接销毁服务,看具体要求进行扩展。

接下来看一下具体的使用,先看下Activity的布局 activity_main.xml

复制代码

就是两个按钮,一个用来开启悬浮窗,一个用来进行简单的交互,展示小红点。

Mainactivity.java

public class MainActivity extends AppCompatActivity implements View.OnClickListener {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        Button btOpenFloat = (Button) findViewById(R.id.open_float);        Button btRedDot = (Button) findViewById(R.id.red_dot);        assert btOpenFloat != null;        btOpenFloat.setOnClickListener(this);        assert btRedDot != null;        btRedDot.setOnClickListener(this);    }    @Override    public void onClick(View v) {        if (v.getId() == R.id.open_float) {            boolean isPermission = FloatPermissionManager.getInstance().applyFloatWindow(this);            //有对应权限或者系统版本小于7.0            if (isPermission || Build.VERSION.SDK_INT < 24) {                //开启悬浮窗                FloatActionController.getInstance().startMonkServer(this);            }        } else if (v.getId() == R.id.red_dot) {            //开启小红点            FloatActionController.getInstance().setObtainNumber(1);        }    }    @Override    protected void onDestroy() {        super.onDestroy();        //关闭悬浮窗        FloatActionController.getInstance().stopMonkServer(this);    }}复制代码

具体使用看起来也还简单,因为逻辑都已经尽量封装和解耦了,就是在开启悬浮窗的时候,7.0版本以上必须先申请权限才能开启,7.0以下可以直接开启,因为前面已经设置WindowManager.LayoutParams.TYPE_TOAST,虽然有些特殊机型也必须申请权限,但起码先保证我的悬浮窗在大多数手机上可以先展示出来。

boolean isPermission = FloatPermissionManager.getInstance().applyFloatWindow(this);复制代码

这段代码说明,无论在哪种情况,我会先进行权限检查,双重保险。

大概效果如下:

感兴趣可以看看完整的演示代码。

转载地址:http://vmalo.baihongyu.com/

你可能感兴趣的文章
HDFS High Availability Using the Quorum Journal Manager
查看>>
Sql日期时间格式转换
查看>>
mesos+marathon+zookeeper的docker管理集群亲手搭建实例(环境Centos6.8)
查看>>
你应了解的4种JS设计模式
查看>>
垃圾收集器Serial 、Parallel、CMS、G1
查看>>
mongodb基本概念解析
查看>>
前端学HTTP之网关、隧道和中继
查看>>
OpenCV【2】---读取png图片显示到QT label上的问题
查看>>
Fedora 25-64位操作系统中安装配置Hyperledger Fabric过程
查看>>
Azure China (12) 域名备案问题
查看>>
ActiveX组件与JavaScript交互
查看>>
2013第52周六当前用到的一些软件及网站
查看>>
DrawDibDraw函数的使用方法
查看>>
两种将字符串转换成浮点数的方法
查看>>
Xcode 调试技巧-b
查看>>
几种常见SQL分页方式效率比较
查看>>
socket中的SO_REUSEADDR
查看>>
Android中Bitmap、Drawable、byte[]转换
查看>>
[杂记]是否有必要精通unix的shell语法?
查看>>
Oracle 数据类型及存储方式(二)
查看>>