diff --git a/BLOCK_OUTSIDE_CLICKS.md b/BLOCK_OUTSIDE_CLICKS.md
new file mode 100644
index 00000000..6d36b089
--- /dev/null
+++ b/BLOCK_OUTSIDE_CLICKS.md
@@ -0,0 +1,74 @@
+# Block Outside Clicks Feature
+
+This document describes the new "Block Outside Clicks" feature that makes app-level floating windows behave like Dialog, preventing interactions with content behind the floating window.
+
+## Usage
+
+### Builder Pattern
+```kotlin
+// Create a floating window with block outside clicks enabled
+private val modalFx by createFx {
+ setLayout(R.layout.item_floating)
+ setBlockOutsideClicks(true) // Enable blocking outside clicks
+ setGravity(FxGravity.CENTER)
+ build().toControl(this@YourActivity)
+}
+
+// Show the modal floating window
+modalFx.show()
+```
+
+### Runtime Configuration
+```kotlin
+// Enable blocking outside clicks at runtime
+floatingWindowControl.configControl.setBlockOutsideClicks(true)
+
+// Disable blocking outside clicks at runtime
+floatingWindowControl.configControl.setBlockOutsideClicks(false)
+```
+
+### Global FloatingX Installation
+```kotlin
+FloatingX.install {
+ setContext(context)
+ setLayout(R.layout.your_floating_layout)
+ setScopeType(FxScopeType.APP) // Only works with APP scope
+ setBlockOutsideClicks(true)
+}.show()
+```
+
+## Features
+
+- **Dialog-like behavior**: When enabled, the floating window prevents touches from reaching the content behind it
+- **Touch area detection**: Only touches outside the floating window bounds are blocked
+- **Runtime toggle**: Can be enabled/disabled at runtime using the config control
+- **APP-level only**: This feature only works with app-level floating windows (`FxScopeType.APP`), not system-level windows
+- **Efficient implementation**: Uses touch interception at the DecorView level for optimal performance
+
+## Important Notes
+
+1. **APP-level only**: This feature only works with app-level floating windows. System floating windows cannot block touches to other applications.
+
+2. **Proper cleanup**: The touch interception is automatically cleaned up when the floating window is hidden or destroyed.
+
+3. **Performance**: The implementation is lightweight and doesn't create additional overlay views.
+
+## Demo
+
+Check out the `BlockOutsideClicksTestActivity` in the demo app to see this feature in action. The demo shows:
+
+- A background with clickable buttons
+- A modal floating window that blocks clicks to the background
+- A normal floating window for comparison
+- Toggle functionality to enable/disable the blocking behavior
+
+## Example Output
+
+When the feature is enabled:
+- Clicking on the floating window works normally
+- Clicking anywhere else on the screen is blocked and doesn't reach the Activity's views
+- The floating window behaves like a modal dialog
+
+When the feature is disabled:
+- Normal floating window behavior
+- Background content remains interactive
\ No newline at end of file
diff --git a/README.md b/README.md
index 85e88f6e..6dc1c4ba 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,7 @@
- 支持 **JetPack Compose**
- 支持 **浮窗半隐藏模式**
+- 支持 **阻挡外部点击**,类似Dialog的模态行为;
- 支持 **自定义隐藏显示动画**;
- 支持 **多指触摸**,精准决策触摸手势;
- 支持 自定义是否保存历史位置及还原;
diff --git a/README_EN.md b/README_EN.md
index 7b938618..1b120f13 100644
--- a/README_EN.md
+++ b/README_EN.md
@@ -19,6 +19,7 @@
- Supports **JetPack Compose**
- Supports **semi-hidden floating window mode**
+- Supports **blocking outside clicks**, similar to Dialog modal behavior;
- Supports **custom hide/show animations**;
- Supports **multi-touch**, precise touch gesture recognition;
- Supports custom history position saving and restoration;
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 89a0422d..2b29a527 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -34,6 +34,9 @@
android:configChanges="keyboard|orientation|keyboardHidden|screenSize|smallestScreenSize|screenLayout|locale|navigation|fontScale|mcc|mnc|uiMode" />
+
(R.id.cardItemFx).setCardBackgroundColor(Color.RED)
+ }
+ }
+ addItemView("关闭阻挡外部点击") {
+ activityFx.configControl.setBlockOutsideClicks(false)
+ }
addItemView("进入测试页面") {
TestActivity::class.java.start(this@MainActivity)
}
+ addItemView("进入阻挡外部点击测试页面") {
+ com.petterp.floatingx.app.test.BlockOutsideClicksTestActivity::class.java.start(this@MainActivity)
+ }
addItemView("进入system浮窗测试页面") {
SystemActivity::class.java.start(this@MainActivity)
}
diff --git a/app/src/main/java/com/petterp/floatingx/app/test/BlockOutsideClicksTestActivity.kt b/app/src/main/java/com/petterp/floatingx/app/test/BlockOutsideClicksTestActivity.kt
new file mode 100644
index 00000000..c8b5c74b
--- /dev/null
+++ b/app/src/main/java/com/petterp/floatingx/app/test/BlockOutsideClicksTestActivity.kt
@@ -0,0 +1,97 @@
+package com.petterp.floatingx.app.test
+
+import android.graphics.Color
+import android.os.Bundle
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import com.petterp.floatingx.app.R
+import com.petterp.floatingx.app.addItemView
+import com.petterp.floatingx.app.addLinearLayout
+import com.petterp.floatingx.app.addNestedScrollView
+import com.petterp.floatingx.app.createLinearLayoutToParent
+import com.petterp.floatingx.app.simple.FxAnimationImpl
+import com.petterp.floatingx.assist.FxGravity
+import com.petterp.floatingx.util.createFx
+
+/**
+ * Test activity for block outside clicks functionality
+ * @author petterp
+ */
+class BlockOutsideClicksTestActivity : AppCompatActivity() {
+
+ private var isModalBlocking = true // Track the current state
+
+ // Create a floating window with block outside clicks enabled by default
+ private val modalFx by createFx {
+ setLayout(R.layout.item_floating)
+ setBlockOutsideClicks(true)
+ setGravity(FxGravity.CENTER)
+ setAnimationImpl(FxAnimationImpl())
+ setEnableAnimation(true)
+ setEnableLog(true, "modal_fx")
+ build().toControl(this@BlockOutsideClicksTestActivity)
+ }
+
+ // Create a normal floating window for comparison
+ private val normalFx by createFx {
+ setLayout(R.layout.item_floating)
+ setGravity(FxGravity.TOP_OR_LEFT)
+ setOffsetXY(50f, 100f)
+ setEnableLog(true, "normal_fx")
+ build().toControl(this@BlockOutsideClicksTestActivity)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ createLinearLayoutToParent {
+ setBackgroundColor(Color.LTGRAY)
+ addNestedScrollView {
+ addLinearLayout {
+ addItemView("这是一个可点击的按钮") {
+ Toast.makeText(
+ this@BlockOutsideClicksTestActivity,
+ "背景按钮被点击了!",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ addItemView("显示模态浮窗(阻挡外部点击)") {
+ modalFx.show()
+ modalFx.updateViewContent { holder ->
+ holder.setText(R.id.tvItemFx, "模态")
+ }
+ }
+ addItemView("显示普通浮窗") {
+ normalFx.show()
+ normalFx.updateViewContent { holder ->
+ holder.setText(R.id.tvItemFx, "普通")
+ }
+ }
+ addItemView("隐藏模态浮窗") {
+ modalFx.hide()
+ }
+ addItemView("隐藏普通浮窗") {
+ normalFx.hide()
+ }
+ addItemView("切换模态浮窗的阻挡模式") {
+ // Toggle the block outside clicks setting
+ isModalBlocking = !isModalBlocking
+ modalFx.configControl.setBlockOutsideClicks(isModalBlocking)
+ val message = if (isModalBlocking) {
+ "模态浮窗现在阻挡外部点击"
+ } else {
+ "模态浮窗现在允许外部点击"
+ }
+ Toast.makeText(this@BlockOutsideClicksTestActivity, message, Toast.LENGTH_SHORT).show()
+ }
+ addItemView("另一个背景可点击按钮") {
+ Toast.makeText(
+ this@BlockOutsideClicksTestActivity,
+ "第二个背景按钮被点击了!",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/petterp/floatingx/app/test/ScopeActivity.kt b/app/src/main/java/com/petterp/floatingx/app/test/ScopeActivity.kt
index e18c01cb..d85b906c 100644
--- a/app/src/main/java/com/petterp/floatingx/app/test/ScopeActivity.kt
+++ b/app/src/main/java/com/petterp/floatingx/app/test/ScopeActivity.kt
@@ -166,6 +166,14 @@ class ScopeActivity : AppCompatActivity() {
}
}
}
+ addItemView("启用阻挡外部点击") {
+ scopeFx.configControl.setBlockOutsideClicks(true)
+ Toast.makeText(this@ScopeActivity, "已启用阻挡外部点击", Toast.LENGTH_SHORT).show()
+ }
+ addItemView("禁用阻挡外部点击") {
+ scopeFx.configControl.setBlockOutsideClicks(false)
+ Toast.makeText(this@ScopeActivity, "已禁用阻挡外部点击", Toast.LENGTH_SHORT).show()
+ }
}
}
}
diff --git a/build.gradle b/build.gradle
index c9f56094..d04e6d63 100644
--- a/build.gradle
+++ b/build.gradle
@@ -5,7 +5,7 @@ buildscript {
plugins {
alias(libs.plugins.vanniketch.maven.publish) apply false
alias(libs.plugins.compose.compiler) apply false
- alias(libs.plugins.android.lirary) apply false
+ alias(libs.plugins.android.library) apply false
alias(libs.plugins.android.application) apply false
alias(libs.plugins.jetbrains.kotlin.android) apply false
}
\ No newline at end of file
diff --git a/floatingx/src/main/java/com/petterp/floatingx/assist/helper/FxBasisHelper.kt b/floatingx/src/main/java/com/petterp/floatingx/assist/helper/FxBasisHelper.kt
index 952396d0..44e428d4 100644
--- a/floatingx/src/main/java/com/petterp/floatingx/assist/helper/FxBasisHelper.kt
+++ b/floatingx/src/main/java/com/petterp/floatingx/assist/helper/FxBasisHelper.kt
@@ -98,6 +98,9 @@ abstract class FxBasisHelper {
@JvmField
internal var enableAssistLocation: Boolean = false
+ @JvmField
+ internal var enableBlockOutsideClicks: Boolean = false
+
@JvmField
internal var iFxTouchListener: IFxTouchListener? = null
@@ -222,6 +225,7 @@ abstract class FxBasisHelper {
enableSaveDirection = this@Builder.enableSaveDirection
enableClickListener = this@Builder.enableClickListener
enableAssistLocation = assistLocation != null
+ enableBlockOutsideClicks = this@Builder.enableBlockOutsideClicks
enableDebugLog = this@Builder.enableDebugLog
fxLogTag = this@Builder.fxLogTag
@@ -292,6 +296,17 @@ abstract class FxBasisHelper {
return this as T
}
+ /**
+ * 设置是否阻止点击浮窗外部区域,类似Dialog的效果
+ * 当启用后,浮窗外部区域将无法响应点击事件,只有浮窗本身可以交互
+ * 注意:此功能仅对APP级别的浮窗有效,系统级浮窗不支持此功能
+ * @param isEnable 默认false
+ */
+ fun setBlockOutsideClicks(isEnable: Boolean): T {
+ this.enableBlockOutsideClicks = isEnable
+ return this as T
+ }
+
/** 设置边缘吸附方向,默认 [FxAdsorbDirection.LEFT_OR_RIGHT] */
fun setEdgeAdsorbDirection(direction: FxAdsorbDirection): T {
this.edgeAdsorbDirection = direction
diff --git a/floatingx/src/main/java/com/petterp/floatingx/imp/FxBasicConfigProvider.kt b/floatingx/src/main/java/com/petterp/floatingx/imp/FxBasicConfigProvider.kt
index 8c458a8a..301cdcd4 100644
--- a/floatingx/src/main/java/com/petterp/floatingx/imp/FxBasicConfigProvider.kt
+++ b/floatingx/src/main/java/com/petterp/floatingx/imp/FxBasicConfigProvider.kt
@@ -55,6 +55,14 @@ open class FxBasicConfigProvider>(
internalView?.moveToEdge()
}
+ override fun setBlockOutsideClicks(isEnable: Boolean) {
+ helper.enableBlockOutsideClicks = isEnable
+ // Notify the platform provider to update the outside click blocking
+ if (p is com.petterp.floatingx.imp.app.FxAppPlatformProvider) {
+ p.updateBlockOutsideClicks()
+ }
+ }
+
override fun setTouchListener(listener: IFxTouchListener) {
helper.iFxTouchListener = listener
}
diff --git a/floatingx/src/main/java/com/petterp/floatingx/imp/app/FxAppPlatformProvider.kt b/floatingx/src/main/java/com/petterp/floatingx/imp/app/FxAppPlatformProvider.kt
index 4fe2f57c..810cc25e 100644
--- a/floatingx/src/main/java/com/petterp/floatingx/imp/app/FxAppPlatformProvider.kt
+++ b/floatingx/src/main/java/com/petterp/floatingx/imp/app/FxAppPlatformProvider.kt
@@ -2,6 +2,7 @@ package com.petterp.floatingx.imp.app
import android.app.Activity
import android.content.Context
+import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.core.view.OnApplyWindowInsetsListener
@@ -75,12 +76,84 @@ class FxAppPlatformProvider(
} else if (fxView.visibility != View.VISIBLE) {
fxView.visibility = View.VISIBLE
}
+ // Update overlay when showing
+ updateBlockOutsideClicks()
}
override fun hide() {
+ // Remove touch interception when hiding
+ removeTouchInterception()
detach()
}
+ /**
+ * Update the outside click blocking state
+ */
+ fun updateBlockOutsideClicks() {
+ if (helper.enableBlockOutsideClicks) {
+ setupTouchInterception()
+ } else {
+ removeTouchInterception()
+ }
+ }
+
+ private var originalTouchInterceptor: View.OnTouchListener? = null
+
+ private fun setupTouchInterception() {
+ val containerView = containerGroupView ?: return
+
+ // Save original touch listener if any
+ if (originalTouchInterceptor == null) {
+ // Set our touch interceptor
+ val touchInterceptorTag = "fx_touch_interceptor".hashCode()
+ originalTouchInterceptor = containerView.getTag(touchInterceptorTag) as? View.OnTouchListener
+
+ val touchInterceptor = View.OnTouchListener { _, event ->
+ // Check if touch is inside floating window
+ val fxView = _internalView
+ if (fxView != null && ViewCompat.isAttachedToWindow(fxView) && fxView.visibility == View.VISIBLE) {
+ val location = IntArray(2)
+ fxView.getLocationOnScreen(location)
+ val fxLeft = location[0]
+ val fxTop = location[1]
+ val fxRight = fxLeft + fxView.width
+ val fxBottom = fxTop + fxView.height
+
+ val x = event.rawX
+ val y = event.rawY
+
+ // If touch is inside floating window, let it pass through
+ if (x >= fxLeft && x <= fxRight && y >= fxTop && y <= fxBottom) {
+ return@OnTouchListener false // Don't consume, let floating window handle
+ }
+
+ // Touch is outside floating window, consume the event
+ return@OnTouchListener true
+ }
+
+ // No floating window visible, don't consume
+ false
+ }
+
+ containerView.setTag(touchInterceptorTag, touchInterceptor)
+ containerView.setOnTouchListener(touchInterceptor)
+ }
+
+ helper.fxLog.v("Touch interception enabled")
+ }
+
+ private fun removeTouchInterception() {
+ val containerView = containerGroupView ?: return
+
+ // Restore original touch listener
+ val touchInterceptorTag = "fx_touch_interceptor".hashCode()
+ containerView.setOnTouchListener(originalTouchInterceptor)
+ containerView.setTag(touchInterceptorTag, null)
+ originalTouchInterceptor = null
+
+ helper.fxLog.v("Touch interception disabled")
+ }
+
private fun checkOrReInitGroupView(): ViewGroup? {
val curGroup = containerGroupView
if (curGroup == null || curGroup !== topActivity?.decorView) {
@@ -127,6 +200,7 @@ class FxAppPlatformProvider(
override fun reset() {
hide()
clearWindowsInsetsListener()
+ removeTouchInterception()
_internalView = null
_containerGroup?.clear()
_containerGroup = null
@@ -137,6 +211,7 @@ class FxAppPlatformProvider(
private fun detach() {
_internalView?.visibility = View.GONE
containerGroupView?.safeRemoveView(_internalView)
+ removeTouchInterception()
_containerGroup?.clear()
_containerGroup = null
}
diff --git a/floatingx/src/main/java/com/petterp/floatingx/listener/control/IFxConfigControl.kt b/floatingx/src/main/java/com/petterp/floatingx/listener/control/IFxConfigControl.kt
index b0a23d4c..bf6715a1 100644
--- a/floatingx/src/main/java/com/petterp/floatingx/listener/control/IFxConfigControl.kt
+++ b/floatingx/src/main/java/com/petterp/floatingx/listener/control/IFxConfigControl.kt
@@ -64,6 +64,14 @@ interface IFxConfigControl {
* */
fun setEnableEdgeAdsorption(isEnable: Boolean)
+ /**
+ * 设置是否阻止点击浮窗外部区域
+ * 当启用后,浮窗外部区域将无法响应点击事件,只有浮窗本身可以交互
+ * 注意:此功能仅对APP级别的浮窗有效,系统级浮窗不支持此功能
+ * @param isEnable 默认false
+ */
+ fun setBlockOutsideClicks(isEnable: Boolean)
+
/** 设置滑动监听 */
fun setTouchListener(listener: IFxTouchListener)
diff --git a/floatingx/src/test/java/com/petterp/floatingx/test/BlockOutsideClicksTest.kt b/floatingx/src/test/java/com/petterp/floatingx/test/BlockOutsideClicksTest.kt
new file mode 100644
index 00000000..a14fba9c
--- /dev/null
+++ b/floatingx/src/test/java/com/petterp/floatingx/test/BlockOutsideClicksTest.kt
@@ -0,0 +1,65 @@
+package com.petterp.floatingx.test
+
+import org.junit.Test
+import org.junit.Assert.*
+
+/**
+ * Test for block outside clicks coordinate calculations
+ * This test validates that the touch coordinate logic is correct
+ */
+class BlockOutsideClicksTest {
+
+ @Test
+ fun testTouchInsideFloatingWindow() {
+ // Simulated floating window bounds
+ val fxLeft = 100
+ val fxTop = 200
+ val fxRight = 300 // width = 200
+ val fxBottom = 400 // height = 200
+
+ // Touch coordinates inside the floating window
+ val touchX = 150f
+ val touchY = 250f
+
+ // Test the touch detection logic
+ val isInsideWindow = touchX >= fxLeft && touchX <= fxRight && touchY >= fxTop && touchY <= fxBottom
+
+ assertTrue("Touch should be inside floating window", isInsideWindow)
+ }
+
+ @Test
+ fun testTouchOutsideFloatingWindow() {
+ // Simulated floating window bounds
+ val fxLeft = 100
+ val fxTop = 200
+ val fxRight = 300
+ val fxBottom = 400
+
+ // Touch coordinates outside the floating window
+ val touchX = 50f // Left of window
+ val touchY = 250f
+
+ // Test the touch detection logic
+ val isInsideWindow = touchX >= fxLeft && touchX <= fxRight && touchY >= fxTop && touchY <= fxBottom
+
+ assertFalse("Touch should be outside floating window", isInsideWindow)
+ }
+
+ @Test
+ fun testTouchOnBoundary() {
+ // Simulated floating window bounds
+ val fxLeft = 100
+ val fxTop = 200
+ val fxRight = 300
+ val fxBottom = 400
+
+ // Touch coordinates on the boundary (should be considered inside)
+ val touchX = 100f // Exactly on left edge
+ val touchY = 200f // Exactly on top edge
+
+ // Test the touch detection logic
+ val isInsideWindow = touchX >= fxLeft && touchX <= fxRight && touchY >= fxTop && touchY <= fxBottom
+
+ assertTrue("Touch on boundary should be considered inside", isInsideWindow)
+ }
+}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 419520e4..0f7cef36 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -45,7 +45,7 @@ compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview"
[plugins]
vanniketch-maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "vanniketch-maven-publish" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
-android-lirary = { id = "com.android.library", version.ref = "androidGradlePlugin" }
+android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" }
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }