Android实现悬浮窗的简单方法实例
作者:Android开发中文站
1. 前言
现在很多应用都有小悬浮窗的功能,比如看直播的时候,通过Home键返回桌面,直播的小窗口仍可以在屏幕上显示。下面将介绍下悬浮窗的的一种简单实现方式。
2.原理
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的形式存在的。
3.具体实现
3.1浮窗布局
悬浮窗的简易布局如下的可参考下面的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>
3.2 悬浮窗的实现
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)) } } }
3.3 完整代码
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) } } }
4. 总结
以上就是Android悬浮窗的一个简单实现方式。如果需要实现其他复杂一点的功能,比如播放视频,也可以在此基础上完成。
到此这篇关于Android实现悬浮窗的简单方法的文章就介绍到这了,更多相关Android实现悬浮窗内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!