Skip to content

Architecture

petterp edited this page Aug 30, 2026 · 2 revisions

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.

The three roles

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.

Host: where the window hangs

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.

Engine: state machine + command queue

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 afterwards show/hide/moveTo/moveBy throw IllegalStateException.

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.

Feature: container behaviour plugins

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()).

Extending

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 } }

Why positioning is this stable

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.

Module dependency boundary

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.

Clone this wiki locally