摘要:近日看公司直播项目,其中有一个功能就是退出某房间之后,直播界面会以悬浮窗的形式出现,并且可以拖动悬浮窗到界面中任意位置,点击悬浮框之后,又可以回到房间中继续观看直播。现在这个功能在主流的直播或者视频类软件中都可以看到,比如:某鱼、某猫、某珠、某牙、某tube。当然了,某tobe当中的悬浮窗效果更佳炫酷,可以炫酷地从悬浮框中将视频主界面慢慢拖动出来,具体效果下载某tube就能看到。这篇文章就记录一下传统悬浮窗播放视频的原理,以及悬浮框涉及到的 Window 和 WindowManager 的相关知识。

  • Window 和 WindowManager 概述
  • 创建一个 Window
    • 实现要点
    • 后续代码前提
    • 清单文件中的权限
    • 检查权限并启动Service
    • FloatWindowService
    • FloatWindow
  • 效果
  • 相关知识点学习资料

Window 和 WindowManager 概述

Window 表示一个窗口的概念,在日常开发中直接接触到 Window 的机会并不多,但是在某些特殊的时候,我们需要在桌面上显示一个类似悬浮框的东西(360的小火箭、360手机助手最新版当中桌面上显示的枫叶),那么这种效果就需要用 Window 来实现。Window 是一个抽象类,它的具体实现类是 PhoneWindow,创建一个 Window 跟简单,只需通过 WindowManager 即可完成。
WindowManager 是完结访问 Window 的入口,Window 的具体实现位于 WindowManagerServiceWindowManager 和 Window 打交道是一个 IPC 过程。
Android 中的所有视图都是通过 Window 来呈现的,不管是 ActivityDialog 还是 Toast,他们的实际视图都是附加在 Window 上的,因此,Window 实际是 View 的直接管理者。比如说,在事件分发的过程中,点击事件首先是由 Window 传递给 DecorView,然后再由 DecorView 往子 View 分发,最终分发到能够消耗这个点击事件的 View 当中;并且 Activity 生命周期方法 onCreate 中经常调用的 setContentView 方法底层也是通过 Window 来完成的。

创建一个 Window

上面概述中提到,要想创建一个 Window ,只需通过 WindowManager 即可实现。

public void addWindow(){Button button = new Button(getApplicationContext());button.setText("动态添加");WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, 0, 0, PixelFormat.TRANSPARENT);layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;layoutParams.gravity = Gravity.LEFT | Gravity.TOP;layoutParams.x= 600;layoutParams.y= 600;getWindowManager().addView(button, layoutParams);}

上面的代码就将一个 Button 添加到屏幕上 (600,600) 的位置,对 WindowMnager.Layoutparams 常用参数做说明:

public LayoutParams(int w, int h, int xpos, int ypos, int _type,int _flags, int _format) {
  • w ,h 表示 Window 的宽高,可以通过构造方法传入,也可以在创建好 WindowManager.Layoutparams 之后,直接给其 widthheight 成员变量赋值。

  • xpos,ypos 表示 Window 在手机屏幕上的绝对位置,与 w,h 一样,这两个值也可以在实例化 WindowManager.Layoutparams 之后给 x,y 成员变量属性赋值,要向更改悬浮窗的位置,就是改变的这两个参数

  • _type 表示的是 Window 的类型,Window 有三种类型:

    1. 应用 Window ,这个 Window 对应着一个 Activity,层级范围(1~99)
    2. Window , 不能单独存在,它需要附属在一个特定的父 Window 中,比如说 Dialog ,层级范围(1000~1999)
    3. 系统 Window ,这是需要申明权限才能够创建的 Window, 比如说常用的 Toast ,层级范围(2000~2999)。

TYPE_SYSTEM_OVERLAY(2006),TYPE_TOAST(2005),TYPE_PHONE(2002)

  • Flags 参数表示 Window 的属性,它可以有很多选项,通过这些选项可以控制 Window 的显示特性,比较常用的有:

    1. FLAG_NOT_FOCUSABLE
    表示 Window 不需要获取焦点,也不需要接收各种输入事件,此标记会同时启用 FLAG_NOT_TOUCH_MODE,事件会直接传递给下层的具有焦点的 Window
    2. FLAG_NOT_TOUCH_MODE
    此模式下,系统会将当前 Window 区域以外的点击事件传递给底层的 Window ,当前 Window 区域以内的会自己处理,一般来说这个标记都需要开启,不然其他的 Window 接收不到单击事件。
    3. FLAG_SHOW_WHEN_LOCKED
    Window 显示在锁屏界面上。

WindowManager 常用的方法就三个:添加 View,删除 View,更新 View 。这三个方法定义在 ViewManager 中,WindowManager 继承了 ViewManager。想做悬浮窗播放视频,就需要用到这三个方法,其中悬浮框随手指拖拽而移动就是在 onTouchEvent 回调中调用 updateView 的方法。

public interface ViewManager{public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params);public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params);public void removeView(View view);}

实现要点

  1. WindowManagerWindow 相关,用于展示悬浮框。
  2. 要实现悬浮框,那么就会涉及到权限问题,从 Andtoid 6.0 开始,需要在运行时去获取悬浮窗的权限。
  3. 启动悬浮窗的组件(Activity 或者 Fragment or else)在启动了悬浮窗之后,自己本身肯定是要关闭的,所以这里悬浮框就很适合在 Service 中管理。
  4. 悬浮窗一般是可以与用户交互的,那么这里就会涉及到触摸反馈。

后续代码前提

  • 播放器播放需要一个 m3u8 url,公司自研播放器代码不贴出。
  • 当前 WatchVideoActivity 正在全屏播放,此时点击了“悬浮窗播放”按钮。
  • 这里的悬浮窗播放指的是点播,非直播情况

清单文件中的权限

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

检查权限并启动Service

//悬浮窗播放按钮final Button button_litter_player = (Button) findViewById(R.id.button_litter_player);button_litter_player.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {//此处为检查用户是否已经授权我们的应用悬浮窗权限boolean check = ConstInfo.hasPermissionFloatWin(getApplicationContext());if (!check) {Toast.makeText(getApplication(), "悬浮窗权限未打开,请去打开应用悬浮窗权限", Toast.LENGTH_SHORT).show();} else {//FloatWindowService 就是用于管理悬浮窗的 ServiceIntent intent = new Intent(WatchVideoActivity.this, FloatWindowService.class);Bundle bundle = new Bundle();//当前播放视频的m3u8地址bundle.putString("m3u8Url", getCurrentUrl());                              //主要是记录当前播放的位置,这样在悬浮窗出现后,可以接着之前全屏播放的点继续播放bundle.putInt(EXTRA_VIDEO_CURRENT_POSITION, mVideoView.getCurrentPosition());finish();}}});

其中检查权限的方法是发射调用:

/*** 判断是否开启浮窗权限,api未公开,使用反射调用* @return*/public static boolean hasPermissionFloatWin(Context context) {Log.d(ConstInfo.TAG, "hasAuthorFloatWin android.os.Build.VERSION.SDK_INT="+android.os.Build.VERSION.SDK_INT);if (android.os.Build.VERSION.SDK_INT < 19) {return true;}try {AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);Class c = appOps.getClass();Class[] cArg = new Class[3];cArg[0] = int.class;cArg[1] = int.class;cArg[2] = String.class;Method lMethod = c.getDeclaredMethod("checkOp", cArg);//24是浮窗权限的标记return (AppOpsManager.MODE_ALLOWED == (Integer) lMethod.invoke(appOps, 24, Binder.getCallingUid(), context.getPackageName()));}catch(Exception e){return false;}}

FloatWindowService

Service 详情戳 Android Developer # Service Guide

  1. ServiceonCreate 方法只会在 Service 首次创建的时候调用一次,所以在这个方法中创建悬浮框的实例比较适合,因为只支持一个悬浮窗
  2. onStartCommond 方法在每次调用 startService 方法时都会调用,所以在这个方法中适合检查悬浮窗的状态,比如:是否需要退出悬浮窗,还是直接开始在悬浮窗中继续播放等等.
  3. onDestroy 方法中就直接销毁悬浮窗实例即可.
public class FloatWindowService extends Service {private static final String TAG = "==FloatWindowService==";public static final String ACTION_PLAY = "com.xxxx.testxxxsdk.FloatWindowService.ACTION_PLAY";public static final String ACTION_EXIT = "com.xxxx.testxxxsdk.FloatWindowService.ACTION_EXIT";public static final String PLAY_TYPE = "com.xxxx.testxxxsdk.FloatWindowService.PLAY_TYPE";public static final String EXTRA_VIDEO_LIST = "list";//用于标记当前悬浮窗时候已经显示public static boolean mIsFloatWindowShown = false;//悬浮窗实例private FloatWindow mFloatWindow;@Overridepublic void onCreate() {super.onCreate();LogUtil.d(TAG,"[onCreate] " + "FloatWindowService onCreate");//这里将Service本身传入悬浮窗,是为了实现点击悬浮窗重新进入WatchVideoActivity 全屏播放,且提供 Context,mFloatWindow = new FloatWindow(this);mFloatWindow.createFloatView();mIsFloatWindowShown = true;}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {LogUtil.d(TAG, "[onStartCommand] " + "FloatWindowService onStart");//此处为特殊逻辑处理,和项目需求相关,不做解释if (intent.hasExtra(ACTION_EXIT)) {stopSelf();} else {//在这里就拿到之前点击悬浮窗按钮时传递过来的数据,包括播放m3u8地址和当前播放位置等Bundle bundle = intent.getBundleExtra(ACTION_PLAY);if (bundle != null && mFloatWindow != null) {LogUtil.d(TAG,"[onStartCommand] " + "FloatWindowService onStart play bundle");//将bundle数据交给悬浮窗控件本身去处理mFloatWindow.play(bundle);}}return START_STICKY;}@Overridepublic void onDestroy() {super.onDestroy();LogUtil.d(TAG,"[onDestroy] " +"FloatWindowService onDestroy" );if (mFloatWindow != null) {mFloatWindow.destroy();}mIsFloatWindowShown = false;}@Nullable@Overridepublic IBinder onBind(Intent intent) {return null;}}

上述代码可以看到,Service 在这里就是管理了悬浮窗的生命周期,以及传递数据的作用.

FloatWindow

这是悬浮窗的实现类,之前的代码在”悬浮播放”这一功能来说,都是铺垫. 参照文章前面对 WindowManager 的描述,这里肯定也会涉及到悬浮窗参数和悬浮窗布局,以及悬浮窗的交互.

首先是布局,这列悬浮窗比较简单 top_window_player:

<?xml version="1.0" encoding="utf-8"?><RelativeLayout android:id="@+id/root_view"xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@drawable/top_window_player_bg"><ImageView
            android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_centerInParent="true"android:padding="10dp"android:src="@drawable/logo"/><ProgressBar
            android:id="@+id/progressbar_loading"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_centerInParent="true"android:indeterminateDrawable="@anim/loading_anim"android:visibility="gone"/><com.xxxxxx.xxxsdk.XXXVideoView
            android:id="@+id/live_player_videoview"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_centerInParent="true"android:visibility="gone"/><ImageButton
            android:id="@+id/lsq_closeButton"android:layout_width="20dp"android:layout_height="20dp"android:layout_alignParentRight="true"android:background="@drawable/close_small"android:paddingRight="5dp"android:paddingTop="5dp"/></RelativeLayout>

ImageView 是用于展示默认状态图,ImageButton 为右上角叉叉,XXXVideoView 为自研的播放器,这里就不贴出代码了.

构造方法

  • 这里将 Service 本身传入悬浮窗,是为了实现点击悬浮窗重新进入 WatchVideoActivity 全屏播放,
  • 提供 Context
  • 绑定两者生命周期,即悬浮窗销毁时,服务就要停止
public FloatWindow(Service hostService){mHostService = hostService;mAppContext = mHostService.getApplication();}

createFloatView() 真正创建 Window 的方法

这个方法中做 3 件事 :

  • 使用 WindowManager 创建 Window
  • 布局控件初始化
  • 触摸反馈
public void createFloatView() {wmParams = new WindowManager.LayoutParams();mWindowManager = (WindowManager) mAppContext.getSystemService(mAppContext.WINDOW_SERVICE);//即使应用退出,悬浮窗也可以可以再桌面当中显示wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;wmParams.format = PixelFormat.RGBA_8888;//悬浮窗需要自己处理点击事件wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;//初始位置在屏幕左边的中间wmParams.gravity = Gravity.LEFT | Gravity.CENTER_VERTICAL;// 悬浮窗的宽为手机屏幕宽度的三分之一, 4:3 高宽比wmParams.width = TestApplication.SCREEN_WIDTH / 3;wmParams.height = (wmParams.width / 3) * 4;    //Service中的ContextLayoutInflater inflater = LayoutInflater.from(mAppContext);mFloatLayout = (RelativeLayout) inflater.inflate(R.layout.top_window_player, null);mWindowManager.addView(mFloatLayout, wmParams);progressbar_loading = (ProgressBar) mFloatLayout.findViewById(R.id.progressbar_loading);ImageButton closebutton = (ImageButton) mFloatLayout.findViewById(R.id.lsq_closeButton);closebutton.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {mHostService.stopSelf();}});// 设置悬浮窗的Touch监听mFloatLayout.setOnTouchListener(new View.OnTouchListener() {int lastX, lastY;int paramX, paramY;public boolean onTouch(View v, MotionEvent event) {switch (event.getAction()) {case MotionEvent.ACTION_DOWN://手指按下的位置lastX = (int) event.getRawX();lastY = (int) event.getRawY();//记录手指按下时,悬浮窗的位置paramX = wmParams.x;paramY = wmParams.y;break;case MotionEvent.ACTION_MOVE:int dx = (int) event.getRawX() - lastX;int dy = (int) event.getRawY() - lastY;wmParams.x = paramX + dx;wmParams.y = paramY + dy;// 更新悬浮窗位置mWindowManager.updateViewLayout(mFloatLayout, wmParams);break;case MotionEvent.ACTION_UP://当手指按下的位置和手指抬起来的位置距离小于5像素时,将此次触摸归结为点击事件,if (Math.abs(event.getRawX() - lastX) < 5 && Math.abs(event.getRawY() - lastY) < 5)mFloatLayout.callOnClick();break;}return true;}});//设置悬浮窗的点击监听mFloatLayout.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {                //点击悬浮窗重新跳转回 WatchVideoActivity 全屏播放Intent intent = new Intent(mHostService.getApplicationContext(), WatchVideoActivity.class);//同理播放器在WatchVideoActivity 中全屏播放也是需要播放地址和 悬浮窗已经播放到的无照顾intent.putExtra("m3u8Url", mUrl);                 intent.putExtra(EXTRA_VIDEO_CURRENT_POSITION, mVideoView.getCurrentPosition());//Service 中启动 Activityintent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);mHostService.startActivity(intent);//销毁服务mHostService.stopSelf();}});mVideoView = (XXXVideoView) mFloatLayout.findViewById(R.id.live_player_videoview);//初始化播放器mVideoView.initialize();//监听播放器 播放器相关不贴出代码mVideoView.setListener(mVideoListener);}

开始播放的方法

public void play(Bundle param) {mBundleParam = param;if (mBundleParam == null)return;//拿到从 WatchVideoActivity 中传递过来的 播放地址mUrl = mBundleParam.getString("m3u8Url");//拿到从 WatchVideoActivity 中传递过来的当前播放位置,以便继续播放mCurrPositionFromWatchVod = mBundleParam.getInt(WatchVideoActivity.EXTRA_VIDEO_CURRENT_POSITION, -1);//播放器相关,省略部分代码stop_play();start_play();}

效果

相关知识点学习资料

  1. Android 悬浮窗参数权限的小结,这篇文章写得时间较早,其中有点内容在我测试机 红米Note 4X 当中并没有办法验证,索性,还是需要向用户申请悬浮窗权限.
  2. Service 官方文档
  3. 运行时权限官方文档 , 鸿洋大神—— Android 6.0 运行时权限处理完全解析
  4. Android 带你彻底理解 Window 和 WindowManager , 《Android 开发艺术探索》 08-理解Window和WindowManager
    抄书系列

原文发布于:Android中的Window、WindowManager以及悬浮框视频播放的实现

Android中的Window、WindowManager以及悬浮框视频播放的实现相关推荐

  1. WindowManager 简单悬浮框的实现

    参考: permission denied for window type 2003 WindowManager(窗口管理服务) 10.7 WindowManager(窗口管理服务) 权限: < ...

  2. android获取当前windows,Android 中的 Window

    一台 Android 手机屏幕上显示的内容就是由一个个 Window 组合而成的.顶部的状态栏是一个 Window,底部的导航栏也是一个 Window,中间自己的应用显示区域也是一块大 Window, ...

  3. android综合资讯App、自定义悬浮框、屏幕助手、空灵音乐源码等

    Android精选源码 综合资讯类APP android自定义悬浮窗 android实现支付宝信用界面动画 android一个类似点融投资的app源码 Android手机屏幕助手 android视频播 ...

  4. android类之间的关系,Android 中Activity,Window和View之间的关系

    Activity是Android应用程序的载体,允许用户在其上创建一个用户界面,并提供用户处理事件的API,如 onKeyEvent, onTouchEvent等. 并维护应用程序的生命周期.Acti ...

  5. android 仿美团悬浮,类似美团悬浮框的效果

    具体代码: ublic class MainActivity extends Activity implements OnScrollListener{ /** * 自定义的MyScrollView ...

  6. 用WindowManager实现Android悬浮框以及拖动事件

    下面的App程序代码实现通过主Activity的启动按钮,启动一个Service,然后在Service中创建添加悬浮窗口:(话不多说,直接上代码) 在这里我们先看一下需要创建的类和布局和需要添加的权限 ...

  7. android 浮动文字提示,怎么在Android中实现一个自由拖动并显示文字的悬浮框

    怎么在Android中实现一个自由拖动并显示文字的悬浮框 发布时间:2021-01-27 15:34:05 来源:亿速云 阅读:107 作者:Leah 今天就跟大家聊聊有关怎么在Android中实现一 ...

  8. WindowManager解析(二)Android悬浮框无法弹出输入法的原因和无需权限显示悬浮窗

    Android悬浮框无法弹出输入法 最近要研究悬浮窗方面的东西,遇到一个问题,我的悬浮窗里面有一个输入框,但是不弹出输入法,后来找到一个方法: 在WindowManager的实例获取方式不对,之前是这 ...

  9. Android 悬浮框按钮

    示例: 悬浮框的xml代码:(layout_float.xml) <?xml version="1.0" encoding="utf-8"?> &l ...

最新文章

  1. Eclipse创建struts.xml
  2. python类和对象详解_Python公开课 - 详解面向对象
  3. Redis基础知识之—— hset 和hsetnx 的区别
  4. SAP Spartacus page slot里的Component,对应的DOM节点是如何插入到DOM tree里的
  5. LINUX framebuffer
  6. css设置 ul的内外边距,9月3日学习CSS选择器,背景设置,及内外边距知识总结
  7. 探索Windows Azure 监控和自动伸缩系列1 - 连接中国区Azure
  8. 阿里再度开源重磅技术!95% 程序员都需要了解
  9. SpringCache @Cacheable 在同一个类中调用方法,导致缓存不生效的问题及解决办法...
  10. 接不住了,能撒手吗?
  11. MATLAB——zeros
  12. 【渝粤教育】电大中专跨境电子商务理论与实务答案作业 题库
  13. 编程及C/C++初学者FAQ
  14. adxl345取出值怎么算角度_ADXL345测量倾斜角度数据跳动
  15. Prometheus+Grafana搭建Jmeter性能监控平台
  16. linux-网卡名字说明_基本网络配置_修改MTU值
  17. 网贷查询接口开发 网贷黑名单查询 个人网贷黑名单查询
  18. 六大原则之依赖倒转(倒置)原则
  19. 数学问题1 - 两个圆圈,小圆贴着大圆外部转过一圈,问小圆转几圈
  20. 电子表格软件能解决什么问题?

热门文章

  1. Python爬虫系列之爬取某优选微信小程序全国店铺商品数据
  2. [附源码]计算机毕业设计JAVA哈金院食堂美食评价系统
  3. nodejs+vue+elementui校友会校友录社交网站系统-vscode
  4. 网站建设-通过链接策略建立排名:
  5. pbft共识机制 java实现_区块链开发:共识机制PBFT #C09
  6. 研究发现蝴蝶的翅膀颜色跟飞行速度有关
  7. 值得收藏——超详细 Spring Boot 知识清单
  8. 详解自动编码器(AE)
  9. unity3D防止破解插件:Anti-Cheat Toolkit的使用
  10. python字典值求平均值_如何用Python打印字典键值的平均值?