赞
踩
现在很多应用都有小悬浮窗的功能,比如看直播的时候,通过Home键返回桌面,直播的小窗口仍可以在屏幕上显示。下面将介绍下悬浮窗的的一种简单实现方式。
Window我们应该很熟悉,它是一个接口类,具体的实现类为PhoneWindow,它可以对View进行管理。WindowManager是一个接口类,继承自ViewManager,从名称就知道它是用来管理Window的,它的实现类是WindowManagerImpl。如果我们想要对Window(View)进行添加、更新和删除操作就可以使用WindowManager,WindowManager会将具体的工作交由WindowManagerService处理。这里我们只需要知道WindowManager能用来管理Window就好。
WindowManager是一个接口类,继承自ViewManager,ViewManager中定义了3个方法,分布用来添加、更新和删除View,如下所示:
- public interface ViewManager {
- public void addView(View view, ViewGroup.LayoutParams params);
- public void updateViewLayout(View view, ViewGroup.LayoutParams params);
- public void removeView(View view);
- }
WindowManager也继承了这些方法,而这些方法传入的参数都是View类型,说明了Window是以View的形式存在的。
悬浮窗的简易布局如下的可参考下面的layout_floating_window.xml文件。顶层深色部分的FrameLayout布局是用来实现悬浮窗的拖拽功能的,点击右上角ImageView可以实现关闭悬浮窗,剩下区域显示内容,这里只是简单地显示文本内容,不做复杂的东西,故只设置TextView。
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical">
-
- <FrameLayout
- android:id="@+id/layout_drag"
- android:layout_width="match_parent"
- android:layout_height="15dp"
- android:background="#dddddd">
- <androidx.appcompat.widget.AppCompatImageView
- android:id="@+id/iv_close"
- android:layout_width="15dp"
- android:layout_height="15dp"
- android:layout_gravity="end"
- android:src="@drawable/img_delete"/>
- </FrameLayout>
-
- <androidx.appcompat.widget.AppCompatTextView
- android:id="@+id/tv_content"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_gravity="center_horizontal"
- android:background="#eeeeee"
- android:scrollbars="vertical"/>
- </LinearLayout>
1. 使用服务Service
Service 是一种可在后台执行长时间运行操作而不提供界面的应用组件,可由其他应用组件启动,而且即使用户切换到其他应用,仍将在后台继续运行。要保证应用在后台时,悬浮窗仍然可以正常显示,所以这里可以使用Service。
2. 获取WindowManager并设置LayoutParams
- private lateinit var windowManager: WindowManager
- private lateinit var layoutParams: WindowManager.LayoutParams
- override fun onCreate() {
- // 获取WindowManager
- windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
- layoutParams = WindowManager.LayoutParams().apply {
- // 实现在其他应用和窗口上方显示浮窗
- type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
- } else {
- WindowManager.LayoutParams.TYPE_PHONE
- }
- format = PixelFormat.RGBA_8888
- // 设置浮窗的大小和位置
- gravity = Gravity.START or Gravity.TOP
- flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
- width = 600
- height = 600
- x = 300
- y = 300
- }
- }
3. 创建View并添加到WindowManager
- private lateinit var floatingView: View
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- if (Settings.canDrawOverlays(this)) {
- floatingView = LayoutInflater.from(this).inflate(R.layout.layout_floating_window.xml, null)
- windowManager.addView(floatingView, layoutParams)
- }
- return super.onStartCommand(intent, flags, startId)
- }
4. 实现悬浮窗的拖拽和关闭功能
- // 浮窗的坐标
- private var x = 0
- private var y = 0
-
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- if (Settings.canDrawOverlays(this)) {
- floatingView = LayoutInflater.from(this).inflate(R.layout.layout_floating_window.xml, null)
- windowManager.addView(floatingView, layoutParams)
-
- // 点击浮窗的右上角关闭按钮可以关闭浮窗
- floatingView.findViewById<AppCompatImageView>(R.id.iv_close).setOnClickListener {
- windowManager.removeView(floatingView)
- }
- // 实现浮窗的拖动功能, 通过改变layoutParams来实现
- floatingView.findViewById<AppCompatImageView>(R.id.layout_drag).setOnTouchListener { v, event ->
- when (event.action) {
- MotionEvent.ACTION_DOWN -> {
- x = event.rawX.toInt()
- y = event.rawY.toInt()
- }
- MotionEvent.ACTION_MOVE -> {
- val currentX = event.rawX.toInt()
- val currentY = event.rawY.toInt()
- val offsetX = currentX - x
- val offsetY = currentY - y
- x = currentX
- y = currentY
- layoutParams.x = layoutParams.x + offsetX
- layoutParams.y = layoutParams.y + offsetY
- // 更新floatingView
- windowManager.updateViewLayout(floatingView, layoutParams)
- }
- }
- true
- }
- return super.onStartCommand(intent, flags, startId)
- }
5. 利用广播进行通信
- private var receiver: MyReceiver? = null
- override fun onCreate() {
- // 注册广播
- receiver = MyReceiver()
- val filter = IntentFilter()
- filter.addAction("android.intent.action.MyReceiver")
- registerReceiver(receiver, filter)
- }
-
- inner class MyReceiver : BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent) {
- val content = intent.getStringExtra("content") ?: ""
-
- // 通过Handler更新UI
- val message = Message.obtain()
- message.what = 0
- message.obj = content
- handler.sendMessage(message)
- }
- }
-
- val handler = Handler(this.mainLooper) { msg ->
- tvContent.text = msg.obj as String
- false
- }
可以在Activity中通过广播给Service发送信息
- fun sendMessage(view: View?) {
- Intent("android.intent.action.MyReceiver").apply {
- putExtra("content", "Hello, World!")
- sendBroadcast(this)
- }
- }
6. 设置权限
悬浮窗的显示需要权限,在AndroidManefest.xml中添加:
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
此外,还要通过Settings.ACTION_MANAGE_OVERLAY_PERMISSION来让动态设置权限,在Activity中设置。
- // MainActivity.kt
- fun startWindow(view: View?) {
- if (!Settings.canDrawOverlays(this)) {
- startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName")), 0)
- } else {
- startService(Intent(this@MainActivity, FloatingWindowService::class.java))
- }
- }
-
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- super.onActivityResult(requestCode, resultCode, data)
- if (requestCode == 0) {
- if (Settings.canDrawOverlays(this)) {
- Toast.makeText(this, "悬浮窗权限授权成功", Toast.LENGTH_SHORT).show()
- startService(Intent(this@MainActivity, FloatingWindowService::class.java))
- }
- }
- }
- class FloatingWindowService : Service() {
- private lateinit var windowManager: WindowManager
- private lateinit var layoutParams: WindowManager.LayoutParams
- private lateinit var tvContent: AppCompatTextView
- private lateinit var handler: Handler
-
- private var receiver: MyReceiver? = null
- private var floatingView: View? = null
- private val stringBuilder = StringBuilder()
-
- private var x = 0
- private var y = 0
-
- // 用来判断floatingView是否attached 到 window manager,防止二次removeView导致崩溃
- private var attached = false
-
- override fun onCreate() {
- super.onCreate()
- // 注册广播
- receiver = MyReceiver()
- val filter = IntentFilter()
- filter.addAction("android.intent.action.MyReceiver")
- registerReceiver(receiver, filter);
-
- // 获取windowManager并设置layoutParams
- windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
- layoutParams = WindowManager.LayoutParams().apply {
- type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
- } else {
- WindowManager.LayoutParams.TYPE_PHONE
- }
- format = PixelFormat.RGBA_8888
- // format = PixelFormat.TRANSPARENT
- gravity = Gravity.START or Gravity.TOP
- flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
- width = 600
- height = 600
- x = 300
- y = 300
- }
- handler = Handler(this.mainLooper) { msg ->
- tvContent.text = msg.obj as String
- // 当文本超出屏幕自动滚动,保证文本处于最底部
- val offset = tvContent.lineCount * tvContent.lineHeight
- floatingView?.apply {
- if (offset > height) {
- tvContent.scrollTo(0, offset - height)
- }
- }
- false
- }
- }
-
- override fun onBind(intent: Intent?): IBinder? {
- return null
- }
-
- @SuppressLint("ClickableViewAccessibility")
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- if (Settings.canDrawOverlays(this)) {
- floatingView = LayoutInflater.from(this).inflate(R.layout.layout_show_log, null)
- tvContent = floatingView!!.findViewById(R.id.tv_log)
- floatingView!!.findViewById<AppCompatImageView>(R.id.iv_close).setOnClickListener {
- stringBuilder.clear()
- windowManager.removeView(floatingView)
- attached = false
- }
- // 设置TextView滚动
- tvContent.movementMethod = ScrollingMovementMethod.getInstance()
-
- floatingView!!.findViewById<FrameLayout>(R.id.layout_drag).setOnTouchListener { v, event ->
- when (event.action) {
- MotionEvent.ACTION_DOWN -> {
- x = event.rawX.toInt()
- y = event.rawY.toInt()
- }
- MotionEvent.ACTION_MOVE -> {
- val currentX = event.rawX.toInt()
- val currentY = event.rawY.toInt()
- val offsetX = currentX - x
- val offsetY = currentY - y
- x = currentX
- y = currentY
- layoutParams.x = layoutParams.x + offsetX
- layoutParams.y = layoutParams.y + offsetY
- windowManager.updateViewLayout(floatingView, layoutParams)
- }
- }
- true
- }
-
- windowManager.addView(floatingView, layoutParams)
- attached = true
- }
- return super.onStartCommand(intent, flags, startId)
- }
-
- override fun onDestroy() {
- // 注销广播并删除浮窗
- unregisterReceiver(receiver)
- receiver = null
- if (attached) {
- windowManager.removeView(floatingView)
- }
- }
-
- inner class MyReceiver : BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent) {
- val content = intent.getStringExtra("content") ?: ""
- stringBuilder.append(content).append("\n")
- val message = Message.obtain()
- message.what = 0
- message.obj = stringBuilder.toString()
- handler.sendMessage(message)
- }
- }
- }
以上就是Android悬浮窗的一个简单实现方式。如果需要实现其他复杂一点的功能,比如播放视频,也可以在此基础上完成。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。