这里有新鲜出炉的精品教程,程序狗速度看过来!
Android是一种基于Linux的自由及开放源代码的操作系统,主要使用于移动设备,如智能手机和平板电脑,由Google公司和开放手机联盟领导及开发。尚未有统一中文名称,中国大陆地区较多人使用“安卓”或“安致”。
本篇文章主要介绍了Android应用内悬浮窗的实现方案示例,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
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();
- }
- }
这就是暴露出来的接口,按需添加,效果大概是这样的。
大概效果如下:
Demo:代码地址感兴趣可以看看完整的。
来源: http://www.phperz.com/article/17/0914/345901.html