-
Notifications
You must be signed in to change notification settings - Fork 188
Architecture
FloatingX 3.0 splits "a floating window" into three orthogonal roles: Host, Engine and Feature. Knowing who does what tells you where a behaviour is configured, why state is not lost across page changes, and how to extend the library.
| Role | The question it answers | What you touch directly |
|---|---|---|
| Host | Where the window hangs |
appHost {} / systemHost {} / viewGroupHost() / fragmentHost() / fxScope {}
|
| Engine | What state the window is in, how commands run |
FxState, control.show() / hide() / moveTo()
|
| Feature | What behaviour the container has |
anchor / gesture / animation / modal config, plus addFeature(...)
|
The content view belongs to the engine, not to the host. That is why changing pages or swapping hosts never rebuilds the content — the foundation of "the window survives page changes" in 3.0.
A host creates the container, attaches it somewhere, reports the usable bounds, and tells the engine when its own host is gone.
| Host | Attaches to | Notes |
|---|---|---|
AppHost |
The DecorView of the current foreground Activity (default; can be switched to CONTENT) |
On a page change the same container is silently reparented, so engine state, features and animations do not restart; on a page rejected by the black/white list or a filter it is detached entirely. See App Host |
SystemHost |
A WindowManager window |
Three overlay-permission strategies; on denial requestSwap(fallback) downgrades to AppHost (2.x's SYSTEM_AUTO). See System Host
|
ViewGroupHost |
Any ViewGroup
|
The window is confined to that container, is not registered, and its lifetime belongs to the caller. See Scope Host |
FragmentHost |
The Fragment's root view | Fine to write in onCreate: it attaches once the view exists and cancels on destroy; also not registered. See Scope Host
|
Hosts are interchangeable: swapHost keeps the anchor, the listeners and the features, which is why
a system window downgrading to an app-level one keeps its config, listeners and current position.
INSTALLED ──attach──> ATTACHED ──show──> SHOWN
│ │ │
└────────── cancel ───┴────────────────┘
↓
CANCELLED (terminal)
-
INSTALLED: created, container not attached yet. -
ATTACHED: container attached, but not visible. -
SHOWN: visible. -
CANCELLED: terminal;cancel()is idempotent, and afterwardsshow/hide/moveTo/moveBythrowIllegalStateException.
Command queue: calling show() / hide() / moveTo() before the host is ready loses nothing —
the commands queue up and are replayed in order once it is. So install works from
Application.onCreate, from an Activity's onCreate, even from a Service.
When the host is lost: onHostLost keeps desiredVisible — if you asked for it to be visible,
it stays visible on the next host. Page changes, rotation, Activity recreate, being detached by the
blacklist and coming back: the window survives all of it.
The container itself only holds the content; every behaviour is an FxFeature:
public interface FxFeature {
public fun onAttach(scope: FxFeatureScope)
public fun onDetach()
public fun onCancel() {}
public fun onRemove() {}
public fun onConfigChanged(old: FxConfig, new: FxConfig) {}
public fun onContentSizeChanged(size: FxSize) {}
public fun onBoundsChanged() {}
public fun onShow() {}
public fun onHide() {}
}The built-in features (wired up from your config — normally you just write config):
| Feature | Responsible for | Config |
|---|---|---|
LocationFeature |
Anchor, margin, overflow, safeArea, adsorption, position persistence |
anchor / margin / overflow / safeArea / adsorb / persist
|
GestureFeature |
Drag, click, long press, drag region, child conflicts, touch pass-through | gesture {} |
AnimationFeature |
Show / hide animations | animation(...) |
ModalScrimFeature |
Intercepting touches outside the content, optionally hiding on outside touch | modal(...) |
Hosts and the compose module append their own: SystemWindowFeature (syncing
WindowManager.LayoutParams) and KeyboardFeature (the EditTexts registered via keyboard(...))
from floatingx-system, and ComposeOwnerFeature (driving the FxComposeOwner Lifecycle from the
container state) from floatingx-compose.
Features never reference each other; shared data always goes through FxFeatureScope (which
exposes control / config / container / host / logger and offers commitAnchor(...) /
dispatch { … } / requestRelayout()).
When you need behaviour no built-in config covers, write an FxFeature instead of patching the
container or wrapping it in another ViewGroup:
class LogFeature : FxFeature {
override fun onAttach(scope: FxFeatureScope) { /* scope.control / scope.container */ }
override fun onDetach() {}
override fun onBoundsChanged() { /* the usable area changed */ }
}
// wire it up at install time
FloatingX.install("tag") {
layout(R.layout.fx_card)
addFeature(LogFeature())
appHost(app)
}
// or add / remove at runtime
control.addFeature(LogFeature())
control.removeFeature(feature)
// or drop a kind of feature from the config
control.update { removeFeatures { it is LogFeature } }3.0 stores an FxAnchor(gravity, dx, dy) — "which edge, and how far inward from it" — and not the
absolute top-left coordinate. When the content resizes, the screen rotates or the usable area
changes, the position is recomputed from the anchor, so the anchored edge stays put and no 2.x-style
"force fix" switch is needed.
The geometry types (FxGeometry) are pure Kotlin and do not depend on android.graphics.*, which
makes FxLayoutResolver / FxAdsorbResolver pure functions verifiable without a device. This one
decision is the root cause of a long list of size / rotation issues being fixed in 3.0; see
Issue Coverage.
floatingx-core is a pure View implementation: it does not depend on android.view.WindowManager,
androidx.fragment, androidx.compose, androidx.lifecycle or androidx.appcompat. So "take what
you need" is literal — skip system windows and no WindowManager code comes along; skip Compose and
no Compose dependency comes along.
Start at Getting Started, see every config option in Configuration, or go back to Home.