Components and priorities
Source: the 7bdac078 revision of the android/snippets repository. Across 583 Kotlin files, for every androidx.compose.* symbol we counted how many files import it (the numbers in parentheses). How often an API appears in the official samples is a good proxy for how much real apps need it.
Status: โ done ยท ๐ก partly / in another form ยท โฌ missing
Runtime
| API | Status | Note |
|---|---|---|
@Composable (276) | โ | composable(site, args..., [=]{...}): restartable and skippable. ComposableScope: inline (like Column) |
remember (135), mutableStateOf (93) | โ | |
rememberSaveable, rememberSaveableStateHolder / SaveableStateProvider | โ | inside a SaveableStateHolder the value is kept while its content is not shown (nav entries, saved tabs, items scrolled out of LazyColumn/LazyRow, by their key); rememberScrollState, rememberLazyListState, rememberPagerState, rememberNavController, rememberTextFieldState are saveable. Saveable types (Saver<T>: numbers, bool, enum, strings, vectors/optionals of these, MutableState, scroll/list/pager states, TextFieldState, NavController, SaveableStateHolder) are written to bytes by ComposeHost::saveState(): on Android they go into the Activity's Bundle when the app moves to the background, and when the process is killed and brought back they are restored before the first frame (including the navigation stack and the values of entries and list items). A value outside a provider is gone once its composable leaves (as in Compose). ViewModels die with the process; the values in their SavedStateHandles survive. Inline savers (Compose's Saver(save, restore), listSaver, mapSaver): CustomSaver<T>{save, restore}, listSaver<T>(โฆ), mapSaver<T>(โฆ) and rememberSaveable(saver, init) โ for a type without a Saver<T>, or a value that should be written some other way; if init creates a MutableState<T>, the state is stored with that saver (Compose's stateSaver); on an unreadable record init is used. Material states are saved as in Compose too: rememberTopAppBarState (collapse and scroll; initial values via parameters), rememberDrawerState, rememberFloatingToolbarState/rememberBottomAppBarState, rememberModalBottomSheetState (an open sheet does not slide open again when it comes back; unless it was left halfway, it opens fully), rememberSearchBarState, rememberSwipeToDismissBoxState (a swiped-away row comes back still off the edge), rememberTimePickerState, rememberWideNavigationRailState, rememberDatePickerState, rememberDateRangePickerState, rememberPaneExpansionState (with Saver<...> specializations; hand-made states can also be saved with rememberSaveable) |
getValue/setValue (113/95) | ๐ก | Kotlin by delegation; in C++ .get() / .set() |
rememberCoroutineScope (33), LaunchedEffect (26), value-returning suspend functions, withContext(Dispatchers.IO/Default) | โ | C++20 coroutines: -> Task, co_await delay(...), cancellation = destroying the frame; TaskOf<T>: a coroutine that returns a value (Kotlin's value-returning suspend fun) โ co_return value;, and from another coroutine T x = co_await f(); (runs as part of it and is cancelled with it; an exception it throws is caught in the awaiting coroutine; can be nested); co_await withBackground([] { return work(); }) (Compose's withContext(Dispatchers.IO/Default)): the work runs on one of several worker threads, and the coroutine resumes on the UI thread with the returned value (or the thrown exception); if it is cancelled in between, the work still finishes and its result is discarded; on the web build, which has no threads, it runs right there immediately; the test rule's waitForIdle waits for background work |
mutableIntStateOf/FloatStateOf/LongStateOf (20/8/2), mutableDoubleStateOf | โ | aliases of mutableStateOf (the value is already stored unboxed) |
CompositionLocalProvider (15), compositionLocalOf (2), staticCompositionLocalOf, providesDefault, CompositionLocalContext | โ | A single type, CompositionLocal<T> (identified by its address, never copied; define it globally): CompositionLocal<T>{defaultValue} or compositionLocalOf<T>([] { โฆ }) / staticCompositionLocalOf(โฆ) โ the default is computed once, the first time it is read without a provider, and may throw (a local that must be provided: []() -> User { throw โฆ }); both recompose only the scopes that read it (the static one too). providesDefault(value): provides the value only if nothing above provides one (a library default the app can override). currentCompositionLocalContext() captures all locals provided at that point (the reading scope subscribes to all of them), and CompositionLocalProvider(context, content) provides them all again elsewhere. Missing: compositionLocalWithComputedDefaultOf, providesComputed |
ProvideTextStyle | โ | ProvideTextStyle({.fontSize = 20, .fontWeight = FontWeight::Bold, .color = โฆ}, content): the text style of its content is merged with the current one (font, size, weight, style, family into LocalTextStyle; color into LocalContentColor) |
LocalLayoutDirection (LayoutDirection.Rtl), Modifier.absoluteOffset, absolutePadding | โ | CompositionLocalProvider({LocalLayoutDirection.provides(LayoutDirection::Rtl)}, โฆ): text with no direction of its own (only digits, punctuation) is laid out in the layout direction (TextDirection::Content, as in Compose; Text, TextField, rememberTextMeasurer), Row starts from the right, Start/End (alignment, Arrangement, padding, offset) are mirrored, horizontal scroll/LazyRow/Pager start from the right and follow the finger in the opposite direction (including fling, page snapping, bringing focus into view); the drawer slides in from the right, and SwipeToDismissBox directions are mirrored. Not mirrored: drawing (drawBehind, Canvas), absoluteOffset/absolutePadding, window insets, anchoredDraggable (in screen direction, as in Compose). Slider/RangeSlider (the track starts from the right, the start thumb is on the right) and Switch (thumb on the left when on) are mirrored; Icons::AutoMirrored::Filled::ArrowBack/ArrowForward/KeyboardArrowLeft/KeyboardArrowRight/List/Send (Compose's Icons.AutoMirrored) are drawn mirrored in RTL (ImageVector::autoMirrored()/mirrored(); the mirror is generated once), and so are the DatePicker's month arrows. Dropdown menus (DropdownMenu, ExposedDropdownMenuBox) align to the anchor's right edge in RTL. The app's direction comes from the system language: ComposeHost::setLayoutDirection provides LocalLayoutDirection at the root; the SDL backend (desktop, Android, web) turns on RTL if the first preferred language is written right to left (isRtlLanguage: ar, he/iw, fa, ur, ps, ckb, sd, ug, yi, dv, ks) and updates when the language changes; it can be forced with KOMPOZ_LAYOUT_DIRECTION=rtl|ltr |
DisposableEffect (10), SideEffect (5), key (1) | โ | |
mutableStateListOf (9) | โ | SnapshotStateList<T> |
mutableStateMapOf (SnapshotStateMap) | โ | mutableStateMapOf<K, V>({{k, v}, โฆ}): reading subscribes, changing recomposes the readers; entries in key order (std::map); get(k) (std::optional), getOrDefault, containsKey, put (returns the previous value; putting the same value recomposes no one), putAll, remove (a missing key recomposes no one), clear, update |
derivedStateOf (8), rememberUpdatedState (5), produceState (2) | โ | derivedStateOf readers recompose only when the value changes |
snapshotFlow (5) | โ | co_await snapshotFlow([&] { ... }).collect(...): notifies when the states it reads change and the result differs (without requesting a frame) |
viewModel(), ViewModel (onCleared, viewModelScope) | โ | the ViewModel of the nearest owner: a navigation entry (cleared once it is fully popped off the stack and gone from the screen; lives on in a tab saved with saveState) or the app; by type and an optional key. viewModelScope is cancelled on clear. viewModel<T>(*entry.parent()): the screens of a nested graph share the graph's ViewModel. SavedStateHandle: passed to a ViewModel whose constructor takes one; the saveable values in it (get/set/remove/keys, getStateFlow observable) are written with the app's saved state (a navigation entry's with the NavController), and after the process dies and comes back they are handed back to the ViewModel with the same owner and key; when the entry is popped, its values go too. Notes: navigation arguments are not put into the handle |
Lifecycle, LifecycleOwner, LocalLifecycleOwner, LifecycleEventEffect, LifecycleStartEffect, LifecycleResumeEffect, collectAsStateWithLifecycle | โ | states (InitializedโCreatedโStartedโResumed, and down to Destroyed) are passed through step by step, each step an event (OnCreateโฆOnDestroy); addObserver brings a late observer up to the current state with events, and the observer is removed when the returned handle goes away; currentStateAsState() is observable, currentStateFlow(). The app's owner (ComposeHost::lifecycleOwner()) follows the window: on Android, going to the background gives Started (state saved at that moment goes into the Bundle) then Created, and Resumed on return; on desktop/web, Started on focus loss, Created when minimized or hidden; Destroyed when the host is destroyed. A navigation entry is also a LifecycleOwner (its screen's LocalLifecycleOwner): the shown screen is Resumed (Started while a transition runs), the exiting one Started, those waiting on the stack and in saved tab stacks Created, and one popped off the stack and gone from the screen Destroyed (then its ViewModels are cleared); none of them go above the NavHost's own lifecycle (in a nested NavHost, the parent entry's) and they go down with it immediately (without waiting for a frame); when the NavHost leaves the composition, its screens go down to Created. Effects are bound directly to events (they run even when no frames are drawn in the background); collectAsStateWithLifecycle(flow, minActiveState = Started) stops collecting when the state drops below it (and does not count as a subscriber: WhileSubscribed stops the upstream), and picks up the latest value on return. Notes: entries in saved tab stacks are not destroyed as in Jetpack; they wait in Created; on Android, window focus (the notification shade) does not change the lifecycle |
collectAsState (6), StateFlow/MutableStateFlow | โ | flow.collectAsState() returns the value and recomposes the caller when it changes; setValue/update (atomic)/compareAndSet from any thread, collectors on the UI thread; conflated |
Cold Flow (flow { emit }, flowOf, emptyFlow) and its operators | โ
the producer is a coroutine; co_await emit(v) resumes once the collector has taken the value (backpressure); map, filter, onEach, transform, take (stops the producer without an exception), distinctUntilChanged, debounce, flatMapLatest (a new value cancels the previous one), combine (two flows or StateFlows), stateIn(scope, initialValue) (Kotlin's SharingStarted.Eagerly), launchIn, collectAsState(initial); on StateFlow, asFlow() and the same operators. When the collecting coroutine is cancelled, the producer's and the operators' own coroutines go too. MutableSharedFlow<T>(replay) (tryEmit from any thread, every value to every collector, the last replay values to a new collector, subscriptionCount), shareIn, stateIn(scope, SharingStarted::Eagerly / Lazily / WhileSubscribed(5000ms), initialValue): WhileSubscribed collects the upstream only while someone is listening; when the last listener leaves, it waits for the timeout and then stops, and the value is kept. Missing: SharedFlow overflow policies (collector queues are unbounded), combine with more than two flows | |
Modifier.blur(radius) / blur(radiusX, radiusY, edgeTreatment), BlurredEdgeTreatment | โ
as in Compose: a graphicsLayer whose renderEffect is a BlurEffect, blurred only when both radii are; BlurredEdgeTreatment holds a shape: Rectangle (the default) or BlurredEdgeTreatment{shape} cuts it to the bounds or the shape, the pixels past the bounds taken as the edges' (TileMode::Clamp), Unbounded lets it spread out past them, fading (Decal). On the GPU renderer a Gaussian of Android's sigma for the radius (0.57735 r + 0.5 px), wide ones scaled down as Skia does (within 3 / 255 of the exact Gaussian); in SDL the texture is halved as many times as the radius's power-of-two exponent and scaled back up step by step (close to a Gaussian, not exact; no blur with the software renderer). blur(BlurRadiusSpec, edgeTreatment, alpha) and blur([](BlurScope&) { โฆ }) (read while drawing, with the node's size) are Compose 1.13's progressive blurs: BlurRadiusSpec::uniform (a BlurEffect), verticalGradient, horizontalGradient, linearGradient(start, end, โฆ) and radialGradient(โฆ, center, fallOffRadius), each from one radius to another or through BlurStops, and shader(maxRadius, [](Density, Size) { โฆ }) (each pixel by the widest radius times the alpha of the shader the block makes for the layer's size and the node's density, in its dp from the layer's top left; equal only to itself) (BlurRadiusSpec::createRenderEffect(size, edgeTreatment, density) gives the effect); on the GPU as Compose draws them (a shader's alpha drawn first into a texture of half floats) (its own Gaussian of sigma r / 2 px out to r px, no more than 150 px, across then down, each pixel by its radius); in SDL the sharp pixels and those blurred by the widest radius, mixed by how much each pixel blurs (no blur with the software renderer). Invalid values are kept in range rather than rejected (radii below 0 as 0, fractions to 0..1). Every form takes a BlurType (Kompoz's): Family (the default, Compose's: the node and its children), Morph (frosted glass: what is drawn behind the node blurred within its shape, the node and its children sharp over it; GraphicsContext::drawBackdrop, which reads what the same target holds so far โ in an offscreen layer, that layer's) or Self (only what the node draws itself โ background, border, image, drawBehind โ its children sharp over it, placed by the elements in between); the alpha goes with what is blurred. A change behind a Morph node redraws all it reads. | |
movableContentOf (MovableContent) | โ
remember([] { return movableContentOf([] { โฆ }); }), then card(): when it is called somewhere else in the same composition (a Column instead of a Row, the other pane), its remembered values, nodes and running effects (LaunchedEffect keeps running, DisposableEffect is not disposed) move there; the new place may be composed before or after the old one. If it is called in two places in the same composition, the second one is a new instance; if it is not called at all, it is forgotten once at the end of composition. Parameterized forms: movableContentOf<P1, P2โฆ>([](const P1&, const P2&โฆ) {...}) composes with the parameters given at each call, carrying its state along; movableContentWithReceiverOf<R, Pโฆ>([](R&, const P&โฆ) {...}) takes the receiver as the first parameter (Compose's R.(P) -> Unit) | |
Channel (channel.hpp), BufferOverflow, produce, consumeEach, receiveAsFlow/consumeAsFlow | โ
Kotlin's Channel: each element is received once. Capacity Rendezvous (default, the sender waits for a receiver), a number, Buffered (64), Conflated (only the latest), Unlimited; when full, BufferOverflow::Suspend (waits) / DropOldest / DropLatest. co_await send(v) โ true if received or buffered, false if closed (or cancelled while waiting); trySend does not wait and works from any thread. co_await receive() โ the next element, or nullopt once closed and drained (Kotlin's receiveCatching; nothing throws); tryReceive. close() ends sending, remaining elements can still be received; cancel() discards the rest too. isClosedForSend/Receive, isEmpty. receiveAsFlow() distributes elements among collectors; consumeAsFlow()/consumeEach() cancel the channel when done. produce<T>(scope, block, capacity): a channel that closes when the block finishes and is cancelled if the block is cancelled. Coroutines resume on the UI thread. Note: an element handed to a receiver that is cancelled after the handoff but before its coroutine resumes is lost (as in Kotlin without onUndeliveredElement) | |
withFrameNanos (1) | โ | co_await awaitFrame() |
Layout (foundation.layout)
| API | Status |
|---|---|
padding (120), fillMaxSize/Width/Height (117/67/6), size (51), width/height (18/39) | โ |
Column (108), Box (92), Row (50), Arrangement (66), Spacer (36) | โ
laid out in whole pixels as Compose's (roundToPx sizes, padding sides and spacing; alignment a half up; weights' leftover pixels to the first) |
PaddingValues (16), offset (16), heightIn/widthIn (4/3), safeDrawingPadding (3) | โ |
aspectRatio (14), wrapContentSize/Width/Height (13), requiredSize/Width/Height (5), requiredSizeIn/WidthIn/HeightIn | โ
required*In are ranges that ignore the incoming constraints: an unset bound (max kInfinity) keeps the incoming one as far as the set bound allows; content that overflows the incoming constraints is centered |
sizeIn, defaultMinSize, offset { } | โ
offset { } with a lambda (read during layout); defaultMinSize applies only when the incoming minimum is 0 |
WindowInsets (7), windowInsetsPadding (4), imePadding (3), systemBars, navigationBarsPadding, consumeWindowInsets | โ
types (status/navigation bar, ime, cutout, system gestures, captionBar โ a freeform window's title bar, included in systemBars โ, mandatorySystemGestures, tappableElement, waterfall, safeGestures = the gesture edges of all of them, safeDrawing/safeContent; captionBarPadding, waterfallPadding, systemGesturesPadding, mandatorySystemGesturesPadding, safeGesturesPadding; Android 11+), preferredFrameRate(hz) / preferredFrameRate(FrameRateCategory::Normal|High|NoPreference): while placed, requests a display refresh rate (the fastest request wins; on Android 11+ Surface.setFrameRate on the surface it draws to, High = the display's fastest mode, Normal = 60 Hz; does nothing elsewhere). It is a request, as in Compose: the system's policy has the final say โ e.g. HyperOS on the 14 Ultra caps apps it doesn't recognize to 60 Hz (dumpsys display: render: (0.0 60.0)), so the display stays at 60 even though the request reaches SurfaceFlinger; imeNestedScroll(): when the scrollable inside reaches its end and keeps being pulled, the keyboard follows the finger in, pushing down moves the keyboard out first, and on release it settles open or closed based on the fling or on whether it is past halfway (Android 11+, WindowInsetsAnimationController; while a text field is focused; does nothing elsewhere); onConsumedWindowInsetsChanged { consumedWindowInsets -> } is called during layout when the insets consumed before it (in the same chain and its ancestors) change, i.e. consumption in nested paddings; windowInsetsTopHeight/BottomHeight/StartWidth/EndWidth (space under/over the bar; does not account for what was already consumed around it and does not consume anything itself; in RTL, start is the right side); components use the Compose defaults; system bar icons via SystemBarsAppearance. On Android 11+, WindowInsets::ime() tracks the keyboard frame by frame as it slides (WindowInsetsAnimation: imePadding rises together with the keyboard instead of jumping at once); WindowInsets::imeAnimationSource()/imeAnimationTarget() are where the slide starts and where it goes (ime() when at rest), and WindowInsets::isImeVisible() tells whether the keyboard is open or opening |
BoxWithConstraints (6) | โ the layout constraints are written to state, and the content recomposes in the same frame |
IntrinsicSize (4), FlowRow/FlowColumn (1) | โ
width(IntrinsicSize::Min): texts are as wide as their widest word (breaking at every space, never splitting a word, styled spans at their own sizes; maxLines is not taken into account), a Row is the sum of these, a Column the widest โ like Compose's min intrinsic; Max: the single-line width; for height Min = Max (the natural height at the given width) |
AlignmentLine (FirstBaseline, LastBaseline), Modifier.alignByBaseline/alignBy, paddingFrom/paddingFromBaseline | โ
texts and text fields report their baselines (on a single line, the text's baseline; on multiple lines, those of the first and last visible lines), and parent layouts inherit their children's (the topmost for the first, the bottommost for the last; padding included); inside a Row, alignByBaseline() puts texts of different sizes and button labels on the same line (the row grows if needed); paddingFromBaseline(top, bottom) puts the first baseline that far from the top and the last one that far from the bottom; in your own Layout, placeable[FirstBaseline] / node->alignmentLine(...) |
Foundation
| API | Status | |
|---|---|---|
background (57), clickable (36), border (14), verticalScroll (15), rememberScrollState (17), BorderStroke, shadow(โฆ, clip, ambientColor, spotColor) | โ
border(BorderStroke(width, color or brush), shape): a brush (gradient) border, inside the shape; shadow(elevation, shape, clip, ambientColor, spotColor): the shadow is a blend of the two colors, faint or strong in proportion to their alphas, and clip clips the content to the shape (as in Compose; unlike Compose, the two-parameter shadow does not clip). ScrollState: value() and maxValue() are observable (as in Compose: a composable that reads them recomposes as it scrolls; with derivedStateOf only what is needed), canScrollForward/Backward(), isScrollInProgress() (observable: while the user drags and during the fling that follows, while animateScrollTo/animateScrollBy run; also in LazyListState, LazyGridState and โ without the animations โ LazyStaggeredGridState; it ends when a wheel or a touch stops the fling). verticalScroll(state, enabled, reverseScrolling) / horizontalScroll(โฆ): with enabled = false, and with userScrollEnabled = false in lazy lists, grids and staggered grids, it scrolls only from code (scrollTo, scrollToItem, animations) and to bring something into view; no dragging, flinging, wheel, nested-scroll leftover from the content inside (the parent gets it), or accessibility scrolling; reverseScrolling: starts from the end (bottom, right) and counts its value back from there | |
| Overscroll (Android 12+ stretch) | โ
when verticalScroll/horizontalScroll, lazy lists and pagers are pulled past their end with a finger, the content stretches from the pulled edge (with increasing resistance); when the finger comes back, the stretch is undone first, and on release it springs back; a fling that hits the end does a short stretch. Mouse and wheel do not stretch; a bare scrollable() and the part a parent takes in nested scrolling (e.g. PullToRefresh) do not stretch. LocalOverscrollFactory (Compose 1.8): scroll containers (verticalScroll/horizontalScroll, lazy lists and grids, pagers) created inside CompositionLocalProvider({LocalOverscrollFactory.provides(std::nullopt)}, โฆ) do not stretch. OverscrollEffect (Compose 1.8): your own effect โ applyToScroll(delta, source, performScroll) (takes a part first, passes the rest to scrolling, and shows the leftover), applyToFling(velocity, performFling) (a coroutine; co_await performFling(remaining) returns the velocity consumed, and the rest is what remains on hitting the end), isInProgress(), draw(ContentDrawScope&) and onRemeasured(size); rememberOverscrollEffect() (stretch; null when the factory is off), withoutVisualEffect/withoutEventHandling; verticalScroll/horizontalScroll(state, overscrollEffect, โฆ), LazyListOptions/LazyGridOptions/LazyStaggeredGridOptions/PagerOptions::overscrollEffect (empty: stretch, nullptr: none), scrollable(state, orientation, overscrollEffect, โฆ) (events only) + Modifier.overscroll(overscrollEffect) (where it is drawn). Only touch drags and their flings go to the effect | |
isSystemInDarkTheme (15) | โ | |
Image (34) + ContentScale (28), ImageBitmap, Painter, Modifier.paint | โ
imageResource: JPEG, PNG, WebP, BMP (synchronously only PNG/BMP on the web), pixels from code. Painter (as in Compose): BitmapPainter, painterResource("images/x.png"), VectorPainter/rememberVectorPainter (as Compose's VectorPainter: the vector drawn into a bitmap of the size it is drawn at in pixels โ kept by the vector for that size, its last eight โ and that drawn 1:1, through the painter's color filter, else the vector's tintColor/tintBlendMode, else, all of it in one color, as an Alpha8 mask in that color; mirrored right to left if it autoMirrors; rememberVectorPainter(defaultWidth, defaultHeight, [viewportWidth, viewportHeight, [name, tintColor, tintBlendMode, autoMirror]], [&](float viewportWidth, float viewportHeight) { vector::Group({โฆ}, [&] { vector::Path({โฆ}); }); }) makes the vector of composable content, as Compose's (kompoz::vector: Group, Path, and RenderVectorGroup(group, configs) drawing a tree with a VectorConfig of each name overriding its VectorPropertys, as Compose's animated vectors do): a state/animation it reads makes it again (the composable that remembers it recomposes), the same result stays the same vector; without a viewport, its default size in pixels; its name VectorRootGroup unless given), ColorPainter, BrushPainter, Painter::custom([](DrawScope&) {...}, intrinsicSize); built-in painters are equal if they draw the same thing (composition is skipped). Image(painter, contentDescription, modifier, alignment, contentScale, alpha, colorFilter) is sized like Compose's PainterNode (a fixed-width image stays at its intrinsic height with Fit, and FillWidth scales it up) and clipped; Icon(painter) at its own size (24 dp if it has none), tinted with LocalContentColor; Modifier.paint(painter, sizeToIntrinsics, alignment, contentScale, alpha, colorFilter) draws behind the content (in RTL, Start is on the right). A painter's color filter is in effect around what it draws (GraphicsContext::pushColorFilter), a custom one's drawing included. Note: a custom painter's alpha is applied via a layer | |
ColorFilter (tint, colorMatrix, lighting), ColorMatrix (setToSaturation, setToScale, timesAssign), BlendMode | โ
Image(โฆ, colorFilter), AsyncImageOptions::colorFilter, DrawScope::drawImage(โฆ, colorFilter), painters: the filter is in effect while they draw (GraphicsContext::pushColorFilter, inner filters first); Kompoz's GPU renderer applies it per pixel, SDL_Renderer to each color and brush and to a copy of each image made once on the CPU (kept while the image lives, the last 32). A vector's paths keep their own paints through it (a gray matrix stays gray). tint takes all 29 blend modes (default SrcIn: the shape in that color); blend(source, destination, mode) works one out; Icon already takes a tint | |
AsyncImage (Coil), rememberAsyncImagePainter | โ
AsyncImage(model, contentDescription, modifier, {placeholder, error, contentScale, crossfadeMillis}): an asset/file path or an http(s) URL, loaded and decoded in the background (on Linux desktop if libjpeg-turbo, libwebp, libcurl are available; on Android BitmapFactory and HttpURLConnection; on the web the browser and fetch; on Windows WIC and WinINet), one load per model, a 64 MB memory cache (least recently used goes first), crossfade, onState; the image is decoded at the size layout first gives it (like Coil: a large image in a small box is downscaled by powers of 2, never below the box size; libjpeg-turbo/libwebp/WIC/BitmapFactory downscale while decoding), and downloads stay in a disk cache (DiskCache, the app's cache directory, Application::imageDiskCacheBytes = 64 MB, KOMPOZ_IMAGE_CACHE, KOMPOZ_IMAGE_LOG); rememberAsyncImage(model, size) gives the state; rememberAsyncImagePainter(model) gives the image's Painter once loaded (empty and sizeless before that), for Image(painter) or Modifier.paint. Notes: on Android, network access needs the INTERNET permission and plain http:// needs usesCleartextTraffic; on Windows, WebP goes through the system's WebP codec; on the web, the disk cache is the browser's; HTTP cache headers are ignored | |
LazyColumn (45), items (21), LazyRow (5), rememberLazyListState (7) | โ
key, contentPadding, spacing, fling; rememberLazyListState(index) opens at an item; reverseLayout (first item at the bottom, like chats); animateScrollToItem (for a distant item it first jumps close, then slides with a spring; corrects the target as items get measured), animateScrollBy, stopScroll; ScrollState::animateScrollTo/By (stopped by a drag or a new animation); LazyListState::layoutInfo() (observable, after each layout but only when it changes): visibleItemsInfo (index, key hash, offset from the start of the content โ after padding โ, size, contentType; the pinned header first), viewportStartOffset (โleading padding) / viewportEndOffset, totalItemsCount, viewportSize, orientation, reverseLayout, paddings, spacing โ with derivedStateOf, "is it near the end?" (load more); contentType (items(n, key, contentType, โฆ), item(key, contentType, โฆ), the list form) is reported back in layoutInfo (it has no other effect, since items are not reused); string keys (item("header", โฆ)) are hashed by their text. The grids' info comes through LazyGridState / LazyStaggeredGridState (layoutInfo()); LazyListOptions::arrangement (Compose's verticalArrangement/horizontalArrangement): gives the spacing instead of spacing, and, when all items fit, their placement on screen (Arrangement::Bottom, Center, SpaceBetween, spacedBy(8, Kind::Center)โฆ; in a reversed list Top is the far end); when they don't fit, it scrolls from the start as usual | |
RoundedCornerShape (32), CircleShape (17), clip (26) | โ
clips through the drawing context, no layer (GraphicsContext::pushClipShape); RoundedCornerShapePercent(50) (Compose's RoundedCornerShape(percent): a percentage of the short side), lerp(shape, shape, t) blends the corners (morphing corners; corner by corner for shapes with per-corner sizes); shadow(elevation, shape). Shapes with per-corner sizes: RoundedCornerShape(topStart, topEnd, bottomEnd, bottomStart) (dp) or with CornerSize ({8} dp, CornerPercent(50) fraction) โ in RTL, start is on the right; as in Compose, if the two corners of a side don't fit on the short side, both shrink together. Background, border, clipping, shadow, graphicsLayer clipping and the ripple follow these corners; the backends draw them with CornerRadii (a single radius becomes four equal corners) | |
Canvas (10), drawBehind (11), drawWithContent (5) | โ
DrawScope with Compose's signatures and parameters: drawLine, drawRect, drawImage (at its size in pixels, or srcOffset/srcSize pixels into dstOffset/dstSize, sampled as FilterQuality says: None the nearest pixel, Low (the default) bilinear, Medium bilinear from mipmaps, High Mitchell's cubic โ on the GPU renderer, from premultiplied texels, so that a transparent pixel's color doesn't bleed; SDL_Renderer samples Medium and High as Low), drawRoundRect (CornerRadius(x, y): quarter ellipses), drawCircle, drawOval, drawArc(โฆ, useCenter, โฆ) (the path Skia draws: a pie slice, a chord, an open arc; a turn and more), drawPath, drawPoints (a hairline's dots a pixel) โ each in a color or a brush, with alpha, style (Fill, or Stroke(width, miter, cap, join, pathEffect): width 0 a hairline, one pixel wide whatever the transform), colorFilter and blendMode; density(), layoutDirection(); text and vectors (Kompoz's drawText(text, topLeft, font, color), drawVector); translate (a drawContent() inside it moves too)/inset/clipRect/rotate/scale; PathEffect (Stroke(โฆ, pathEffect), a Paint's), as Skia's (Android's) and applied as Skia applies it, when the path is drawn, at the pixels it is drawn at (curves measured finer at a finer scale): dashPathEffect(intervals, phase) (each contour starts the pattern again; a closed one joins its last dash to its first; curves dashed as pieces of themselves; a line with butt ends dashed as rectangles; a zero-length dash a zero-length line, for its caps; a fill not dashed; an odd last interval left out as Android does), cornerPathEffect(r) (each corner between two lines cut back by r, half a line at most, through a quadratic; curves as they are, fills too), stampedPathEffect(shape, advance, phase, StampedPathEffectStyle::Translate / Rotate / Morph) (the shape stamped every advance along each contour, moved there, turned the way it goes, or bent along it, each of its sides a quadratic through its middle; filled, not stroked; p puts the first stamp at advance โ p), chainPathEffect(outer, inner) (inner's effect, then outer's on what it made); what Skia refuses (no intervals, a negative one, nothing on; no radius; no advance or an empty shape) is no effect; filterPath shows what an effect makes. PathMeasure as Android's (Skia's SkPathMeasure): contours of no length skipped, curves cut in halves until each piece is within half a unit of a line (resScale for a path drawn finer), getPosition/getTangent on the curves themselves (NaN when there is no contour: Compose's Offset.Unspecified), getSegment giving pieces of the curves as curves (a quadratic's a quadratic, a conic's a conic), isClosed, nextContour โ on rectangle, circle, arc, path and drawLine(โฆ, cap, pathEffect) strokes; drawPoints (PointMode.Points/Lines/Polygon); blendMode in every draw call (with a color or a brush): all of Compose's 29 modes (the Porter-Duff ones, Modulate, Plus, the separable and non-separable ones) on Kompoz's GPU renderer, as W3C's compositing spec and Skia define them; SDL_Renderer draws SrcOver, DstIn, DstOut, Clear, Src, Dst and Plus (the others as SrcOver) โ graphicsLayer { compositingStrategy = CompositingStrategy::Offscreen } always opens the layer offscreen, and blend modes inside it affect only that layer (a list with fading edges: drawContent() then drawRect(gradient, blendMode = DstIn)); BlendMode has all 29 (all of them in ColorFilter::tint too); Path.op(a, b, PathOperation::Union / Intersect / Difference / ReverseDifference / Xor) and Path::combine, a + b, a - b, a | b, a & b, a ^ b (according to the paths' fillType; the result is closed contours with their inside on the left made of the pieces of the operands' segments that bound it โ a curve's piece the same curve (a conic's a conic), its ends where the curves meet to a hundredth of a dp; holes, self-intersecting paths, shared edges and pieces touching at a corner become separate contours), Path::contains(point); PathMeasure: length, getPosition, getTangent, getSegment, nextContour (curves are measured as fine polylines, segments come back as lines) | |
Brush (solid, linearGradient, horizontalGradient, verticalGradient, radialGradient, sweepGradient), TileMode | โ
color stops or evenly spaced colors; the ends default to the area's edges (kInfinity); TileMode::Clamp (end color), Repeated (repeats from the start), Mirror (back and forth), Decal (transparent outside) โ in linear and radial gradients; in SDL a tiled gradient is drawn from a 256-texel texture using the mesh's UVs (repeats are sharp), and on clipped paths with vertex colors | |
Shader, LinearGradientShader, RadialGradientShader, SweepGradientShader, ImageShader, CompositeShader, ShaderBrush (Brush::shader), Brush.composite, ShaderBrush.transform, Brush.intrinsicSize, lerp(Brush) | โ
as in Compose: a shader paints pixel by pixel in the dp of what it paints (an image's pixel a dp): a gradient at its stops (or evenly) as its tile mode says, an image bilinearly from its premultiplied pixels and tiled across and down as its two tile modes say, two shaders composited by a blend mode; transformed(Matrix) (what it painted at p it paints at the matrix's p; a second after the first). Brush::shader(createShader, intrinsicSize) makes its shader for the size it paints, again only for another size, and is equal only to itself and its copies; Brush::shader(shader) is the same shader at any size; Brush::composite(dst, src, blendMode) composites two brushes' shaders (a color as a gradient of one color) and is equal to one of the same three; withTransform(Matrix) moves a gradient's or a shader's paint (createShader(size) gives the shader with it); a paint's alpha fades a shader brush (shaderAlpha); intrinsicSize() (a linear gradient's between its given ends, a radial one's diameter, a shader brush's own, NaN where there is none); lerp(a, b, t) mixes two colors, two gradients of a kind (colors index by index, given stops' offsets, ends, centers and radii; the tile mode and an unset center switch halfway) and a color with a gradient (the gradient in that color); anything else switches halfway. The GPU renderer works shaders out per pixel through their transform (perspective too), images sampled from their texture with its tile modes, a composite's two the same and blended where the pixel is (a composite of composites drawn into a texture of its own first); in SDL a shader brush is worked out on the CPU (Shader::colorAt) into a texture of the area, multiplied by a path's coverage. Not yet: RuntimeShader (AGSL) | |
Color (64 bits), ColorSpace, ColorSpaces, Rgb, Connector, WhitePoint, Illuminant, TransferParameters, Adaptation, ColorModel, RenderIntent, lerp(Color), compositeOver, luminance, toArgb, Color.hsv/hsl | โ
as Compose's: a Color is 64 bits packed as Compose packs them: in sRGB its ARGB in the upper 32, 8 bits a channel (Color{1, 0.5f, 0}, Color::hex(0xFF8000), Color::argb(0xFFFF8000), Color(value)), in another built-in color space (Color(r, g, b, alpha, ColorSpaces::DisplayP3); Compose's 20, its ids) its components as half floats within the space's range, a 10-bit alpha and the space's id. red(), green(), blue(), alpha() (an L*a*b* or Oklab color's L, a, b), colorSpace(), convert(space) (through the spaces' connector, perceptually), copy(alpha) and copy(alpha, red, green, blue), compositeOver (in the background's space), luminance, toArgb, isSpecified, colors::Unspecified (0x10). lerp(start, stop, t) mixes in Oklab, t within 0..1, in the stop's space; an animated color goes as Compose's Color.VectorConverter (in Oklab, past its target as a spring or an easing overshoots, each component kept within range), from animateColorAsState, transitions and infinite ones, and animated vectors. The colorspace package as Compose's: RGB spaces from primaries and a white point or a matrix with transfer functions, ICC parametric curves (PQ and HLG too) or a gamma, isWideGamut/isSrgb, CIE XYZ, L*a*b* and Oklab, connectors between any two (the built-in ones' kept), adapt to another white point (Bradford, von Kries, CIECAT02), ColorSpaces::match. Gradients mix their colors in sRGB, as Skia's. Drawn in sRGB: a color of another space is turned into (extended) sRGB as it is drawn, not rounded to 8 bits on the way (an F16 bitmap keeps what is finer); the material color utilities take a color's sRGB. Not yet: wide-gamut output (a P3 display's), with the backends' phases | |
ImageBitmap(width, height, config, hasAlpha, colorSpace), ImageBitmapConfig, Canvas(ImageBitmap), readPixels, toPixelMap, PixelMap | โ
as in Compose, which is Android's Bitmap: a bitmap to draw into, its pixels clear (black and opaque where it has no alpha: Rgb565, or hasAlpha = false); copies are the same bitmap. graphics::Canvas(bitmap) draws into it a unit a pixel, as the canvas goes, by the platform's renderer on the UI thread (waited for from another; kept until there is one: tests without a platform). Each config keeps what is drawn as Android does: Argb8888 and Gpu 8 bits a channel, F16 half floats (finer than 8 bits, and past 0..1), Alpha8 the alpha alone (drawn as a mask: in the paint's color or shader from a canvas, in black from a DrawScope, as Compose's), Rgb565 5, 6 and 5 bits over black, opaque; one without alpha what is drawn over its black. On the GPU renderer an Argb8888, Gpu or F16 bitmap is kept as its texture (drawn into a new texture over the old one, which draws recorded before still read) and read back when its pixels are asked for (readPixels, pixels()), the others read back as they are drawn into; SDL_Renderer draws into a target and reads it back (an F16 one at 8 bits). readPixels(buffer, startX, startY, width, height, bufferOffset, stride) gives ARGB, toPixelMap() a PixelMap (map(x, y) a Color); version() changes each time it is drawn into, and what shows it (a texture, a display list, a color filter's copy, an ImageShader) follows. colorSpace() is the space it was made in (sRGB unless given); its pixels are kept in sRGB, as they are drawn and shown | |
drawIntoCanvas, Canvas (ui.graphics: graphics::Canvas), Paint, PaintingStyle, CanvasDrawScope.draw | โ
as in Compose, which is Android's Canvas: save, restore (nothing saved: nothing), saveLayer(bounds, paint) (a layer composited by the paint's alpha, color filter and blend mode), withSave, withSaveLayer; translate, scale (and about a pivot), rotate (clockwise; about a pivot), rotateRad, skew(sx, sy) (factors, as Android's Canvas and Compose's take them: x' = x + sxยทy, y' = syยทx + y; Compose documents degrees), skewRad (hands degrees(radians) on the same way), concat(Matrix) (its z row and column left out, as Android does) โ each before what is there; clipRect (sorted, ClipOp), clipPath; drawLine (stroked whatever the style, butt ends as butt), drawRect, drawRoundRect(โฆ, radiusX, radiusY), drawOval, drawCircle, drawArc/drawArcRad (sorted, as Skia sorts them), drawPath, drawImage and drawImageRect (faded by the paint's alpha; its color and shader left out), drawPoints, drawRawPoints, drawVertices(Vertices, blendMode, paint) (a triangle mesh as Skia draws one: Vertices(VertexMode::Triangles / TriangleStrip / TriangleFan, positions, textureCoordinates, colors, indices), or its arrays' form; each pixel whose middle a triangle holds, not anti-aliased; the paint's shader where the texture coordinate is, or its color opaque, through the vertex colors โ worked out between them, premultiplied, in perspective too โ by blendMode, the paint the source; faded by the paint's alpha; texture coordinates or colors that don't match the positions left out, and a triangle with an index out of range, where Compose throws; Compose on Android leaves blendMode out and modulates, as Android's Canvas takes none, its other platforms use it, as here); enableZ/disableZ do nothing (Kompoz draws layers in order; z order is the layout's zIndex). Paint (Compose's, as a struct): color (its alpha is alpha()/setAlpha), shader (in the canvas's coordinates, faded by the alpha), style (Fill/Stroke), strokeWidth (0: a hairline), strokeCap, strokeJoin, strokeMiterLimit, pathEffect (on fills too, but for dashes: Skia dashes only strokes), colorFilter, blendMode, filterQuality, isAntiAlias (false: each pixel all in or all out, as Skia decides it โ its middle inside the shape: a rect square to the pixels with its left and top edges out and its right and bottom ones in, a path by its winding at the middle as Skia's scan converter rounds an edge's ends, a hairline a pixel each step along its longer axis; GraphicsContext::pushAntiAlias; on SDL_Renderer the edges unfaded and paths by FreeType's monochrome rasterizer). A DrawScope's canvas()/drawIntoCanvas has its origin at the scope's top left; what it leaves saved, transformed or clipped holds for the scope's later draws until a block it is in ends (translate, clipRect, withTransformโฆ) or the scope does, as Compose restores it. graphics::Canvas(context, origin) draws into any GraphicsContext; DrawScope(canvas, size) draws a scope into a canvas (Compose's CanvasDrawScope.draw). Every scope over a node (drawBehind, drawWithContent, Canvas, drawWithCache's CacheDrawScope, which is built again when the size, density or layout direction changes, a recording, a custom Painter) has the screen's density and the node's layout direction. A DrawScope's rects, round rects, ovals and arcs of negative size are sorted, as Skia sorts them. | |
rememberGraphicsLayer, GraphicsLayer (record, drawLayer, toImageBitmap) | โ
as in Compose 1.7: d.record(layer, [&](DrawScope&) { d.drawContent(); }) records the content (or everything drawn) into the layer instead of drawing it here (from the scope's top-left, at its own size or the scope's); d.drawLayer(layer) draws the recording as many times as needed, at its position in properties(), as a graphicsLayer draws: scale, translation, rotation in 2D or 3D and cameraDistance about pivotOffset (unset: its middle), alpha, clip to its outline (setRectOutline, setRoundRectOutline, setPathOutline), blendMode, colorFilter, compositingStrategy โ a reflection, a shadowed copy; toImageBitmap() turns the recording into pixels at the screen density (the platform draws offscreen: in SDL a target texture + readback; premultiplied colors are corrected); this function, which suspends in Compose, returns immediately here; give a size to the Image, which counts image pixels as dp | |
drawWithCache (5), Path (5), arcTo/addArc, PathIterator, PathHitTester, DrawScope.drawOval/clipPath/withTransform | โ
Path as Compose's, which is Android's (Skia's SkPath): lines, quadratics, conics (conicTo: a quadratic whose control point is weighed; ovals, round rects and arcs are made of them, quarter circles exactly) and cubics; addRect, addOval, addRoundRect(RoundRect, Direction) go round as Path::Direction says (counterclockwise by default, as Compose's) from Skia's starting points (a rect's top left, an oval's right, a round rect's left side; a corner with a radius of 0 either way square); arcTo/arcToRad(oval, start, sweep, forceMoveTo) (0 on the right, clockwise; conics; a turn at most; a lone point for no sweep from 0; joined to the pen by a line where it isn't there), addArc/addArcRad (its own contour; a turn or more from a quarter's angle the whole oval from there); a move right after a move takes its place, drawing after close() starts from the contour's start; transform(Matrix) (in perspective quadratics and conics become conics, exactly, cubics are split, what passes behind the eye is cut off), isConvex(), rewind(), relativeQuadraticTo/relativeConicTo/relativeCubicTo, addPath(path, offset), translate; contains(point) exact on curves (Skia's: on the outline is inside); iterator(ConicEvaluation::AsConic or AsQuadratics, tolerance) gives PathSegments (their points from where each starts; conics as conics, or as Skia's quadratics within the tolerance); PathHitTester(path, tolerance) (Compose's: open contours not closed, an edge not inside). Filled with FreeType (NonZero/EvenOdd) and stroked with FT_Stroker in SDL_Renderer (conics as quadratics within a tenth of a pixel), mask cache. DrawScope: drawOval(color or brush, topLeft, size, style); clipPath(path, block, ClipOp::Intersect/Difference) and clipRect(topLeft, size, block, op): any path by its fill type, its edge anti-aliased, through the drawing context's clip stack (GraphicsContext::pushClipPath, pushClipRoundRect), no layer (SDL_Renderer draws such a clip's content in a layer it then masks with the shape, on the CPU with the software renderer); withTransform([](DrawTransform& t) { t.translate(โฆ); t.rotate(โฆ); t.scale(โฆ); t.transform(matrix); t.inset(โฆ); t.clipRect(โฆ); t.clipPath(โฆ); }, block): applied in order, each inside the previous one. Matrix (Compose's 4x4, operation for operation) and rotateRad; transforms go through GraphicsContext::pushTransform, not a layer: nothing is clipped to the drawing's area, as in Compose (SDL_Renderer still draws a turned or scaled drawing through a layer over what can show) | |
FocusRequester (12), onFocusChanged (3), LocalFocusManager (4) (clearFocus, moveFocus), Modifier.focusProperties, Modifier.focusGroup | โ
navigation with Tab/Shift+Tab and IME Next/Previous; focusProperties({.canFocus = false}) (does not take focus via Tab, touch or requestFocus), {.next = r, .down = r2, .right = FocusRequester::cancel()} (movement in that direction goes to that requester's node โ or the first focusable inside it โ, and stays put with cancel(); if the target cannot take focus, the usual path applies); as in Compose, these also come from nodes above a focus target (a field's modifier). focusGroup(): arrow keys first look for the nearest candidate in the innermost group that contains focus, then outward group by group; seen from outside, a group (by its bounds) is a single target, and when it is chosen, focus enters the nearest focusable inside it in that direction (or the first one); with nested groups the outer one counts (Compose's two-dimensional focus search); Tab order does not change; no onEnter/onExit. The node that receives focus (however it gets it) is brought into view, like Compose's focusables. focusRestorer(fallback): when focus comes back in from outside (via keys or requestFocus on the group), it returns to where it left; the first time (or if that node is gone), to the fallback requester's node; focusTarget() (the low-level name of focusable()), onFocusEvent (called on every focus event โ even when the state stays the same while focus moves among children โ, whereas onFocusChanged only when it changes) | |
BringIntoViewRequester, Modifier.bringIntoViewRequester | โ
co_await requester.bringIntoView() (or a part of the node with bringIntoView(Rect{...})): the surrounding scrollables, from inner to outer, reveal it with the least scrolling; animated with the scrollables' spring (medium-low stiffness); outer ones see the target where the inner one will move it (if the inner box suffices, the page does not move) | |
BringIntoViewSpec, LocalBringIntoViewSpec | โ
how a scroll container brings its content into view (Compose's): calculateScrollDistance(offset, size, containerSize) returns how far to scroll (the focused item, the field above the keyboard, BringIntoViewRequester); verticalScroll/horizontalScroll, scrollable, lazy lists and grids created inside CompositionLocalProvider({LocalBringIntoViewSpec.provides(...)}, โฆ) pick it up (e.g. keeping the focused card at one third, like a TV row). In RTL the distance is measured from the right. Default: just enough to be visible; if it is larger than the viewport, its start | |
Custom indication (IndicationNodeFactory, IndicationNode) | โ
your own indication, as in Compose: the factory creates one node per component; the node receives press (with position), release, cancel, hover and focus interactions through onInteraction, draws around the content with draw(ContentDrawScope&), and animates inside coroutineScope() (its jobs end when the component goes away). Indication(factory): clickable(โฆ, {.indication = โฆ}), LocalIndication (Material buttons too) or Modifier.indication(interactionSource, โฆ); on a clickable without a given source, the node listens to its own source. The same factory keeps the same node across recompositions | |
TextToolbar, LocalTextToolbar | โ
the text selection toolbar is replaced by your own (Compose's): when LocalTextToolbar is provided, Kompoz does not draw its own popup toolbar but calls showMenu(rect, onCopyRequested, onPasteRequested, onCutRequested, onSelectAllRequested) (unavailable actions are empty: no paste/cut in a read-only field, only copy and select all in SelectionContainer), and calls it again when the position or the actions change; hide() when the selection ends. Show your own toolbar in a popup so that pressing it does not end the selection | |
LocalUriHandler, LocalSoftwareKeyboardController, LocalHapticFeedback, LocalClipboardManager | โ
uriHandler().openUri(uri) (the app's own URLs โ deep links of a NavHost in the composition, e.g. kompoz://item/7 โ open inside the app; others via SDL_OpenURL: the browser, an intent on Android, xdg-open, ShellExecute), softwareKeyboardController().hide()/show() (the field stays focused, and the keyboard comes back on tap; with a hardware keyboard there is nothing to hide), hapticFeedback().performHapticFeedback(HapticFeedbackType::โฆ) (on Android the view's performHapticFeedback, with the closest constant on older versions; navigator.vibrate on the web; none on desktop), clipboardManager() (setText/getText/hasText); a long click and the text-selecting long press vibrate with LongPress, the handle with TextHandleMove at every character; Compose 1.8's LocalClipboard: Clipboard clip = clipboard(); โ co_await clip.getClipEntry() (std::optional<ClipEntry>, empty if there is nothing), co_await clip.setClipEntry(ClipEntry::withPlainText(text)) (std::nullopt clears it); awaited in a coroutine, it completes immediately here; plain text only | |
focusable (10) (non-text focus) | โ clickables can also be focused with the keyboard; Tab/Shift+Tab, directional focus with the arrow keys, click with Enter/Space, a focus ring, scrolling the focused node into view | |
Ripple: ripple(), Indication, LocalIndication, clickable(indication = โฆ) | โ
a Material ripple on every clickable: it spreads from the pressed point, starting at 0.3 of the larger side and reaching the farthest corner, with its center drifting to the middle, over 225 ms (FastOutSlowIn); it fades in over 75 ms and fades out over 150 ms after release (once it has finished spreading); clipped to the shape; requests no frames while held. ripple(bounded, radius, color): an unbounded one starts from the center, at the given radius, unclipped (the hover/focus layer and the focus ring are then also a circle of that radius); Indication::Kind::StateLayer is a flat layer, false none (indication = null); LocalIndication sets the one for the subtree. On a keyboard click the ripple starts from the center. Hover/focus layers (0.08/0.1) only with mouse/keyboard. Modifier.indication(source, ripple()): shows a source's presses (ripple) and hover/focus on another node โ clickable(โฆ, {.interactionSource = s, .indication = false}) on a row, with its ripple only on the icon inside. Notes: ripple alpha 0.12; the indication() ripple starts the press at the first frame it draws | |
MutableInteractionSource, Interaction (PressInteraction, HoverInteraction, FocusInteraction, DragInteraction), collectIsPressedAsState/collectIsHoveredAsState/collectIsFocusedAsState/collectIsDraggedAsState, Modifier.hoverable, clickable(interactionSource, indication = null) | โ
clickable(onClick, {.interactionSource, .indication = false, .onLongClick, .onDoubleClick}); the style of the Button/IconButton family and TextFieldOptions take an interactionSource; focusable(enabled, source), hoverable(source), draggable(โฆ, source), scrollable(โฆ, source); ScrollState/LazyListState/PagerState::interactionSource() report the user's drags. Press: Release on release (before the click), Cancel when the finger slides away or scrolling starts; a keyboard click is Press+Release; hover only with the mouse (touch ends the mouse's hover), on all nested hoverables; if the node goes away while pressed/focused, the closing event is sent. interactions() is a SharedFlow (a read-only field + opening a picker on Release); collectIsโฆAsState() is observable and gives the source's current state; your own events: Interaction::press(position), press.release(), tryEmit. Slider/RangeSlider: SliderOptions::interactionSource (Press from touch to release, DragStart when it slides sideways, hover while the mouse is over it; both are cancelled if it goes away); in RangeSlider the start thumb uses this source and the end thumb endInteractionSource; when the two thumbs overlap, the press is reported once the drag direction picks a thumb. A press (its interaction, its ripple) inside something the finger can scroll is shown after the tap timeout (100 ms), as in Compose: if scrolling starts first it is never shown, if it is released first it is pressed and immediately released; with the mouse and outside scrollables it is shown immediately | |
pointerInput (10), detectTapGestures/detectDragGestures (4/4), combinedClickable (3), PointerEventPass, detectDragGesturesAfterLongPress | โ
coroutine-based awaitPointerEvent, consume rules, horizontal/vertical drag. Each event travels in three passes (as in Compose): Initial from outer to inner (the parent sees or consumes it before its children), Main from inner to outer (the default), Final again from outer to inner (after everyone: has someone else consumed it?); awaitPointerEvent(PointerEventPass::Initial) waits for that pass, and the same event can reach a handler several times, pass by pass; a press consumed in Initial is not received by clickables either. Note: the clickables' and scrollables' own handling runs during Main; detectDragGesturesAfterLongPress(scope, {onDragStart, onDrag, onDragEnd, onDragCancel}): the drag starts once the finger has stayed still for the long-press timeout (400 ms) (onDragStart at the finger's position), then follows the whole path (the touch slop is not subtracted) and consumes the events; movement that exceeds the slop earlier abandons the gesture (the surrounding scroll takes it); Compose's building blocks (with TaskOf): co_await awaitFirstDown(scope, requireUnconsumed = true) (the next press; a consumed one does not count unless asked), co_await waitForUpOrCancellation(scope) (the change of the last finger when it lifts; for a consumed movement or one that leaves the element, a consumed release, or a cancellation: nothing), awaitEachGesture(scope, [](PointerInputScope& s) -> Task {โฆ}) (when a gesture ends, a new one does not start until all fingers are up); scope.anyPointerDown() | |
Multi-touch: detectTransformGestures (2), Modifier.transformable, calculateZoom/Rotation/Pan/Centroid | โ
an ID per finger, PointerEvent::changes; one-finger panning can be left to the page with canPan; if a second finger lands while the page has only just started scrolling with the first (250 ms / 48 dp), the gesture switches to zooming; transformable also zooms with Ctrl+wheel on a mouse and with touchpad pinch (SDL pinch on Wayland/macOS; it arrives as Ctrl+wheel on Windows and the web) (ComposeHost::zoom); touchscreens on desktop | |
horizontalScroll (2), LazyVerticalGrid (5) (GridCells.Fixed/Adaptive, full-row span), HorizontalPager (3) / VerticalPager (PagerState, page turning by fling, animateScrollToPage), PageSize, PagerSnapDistance, PagerDefaults.flingBehavior | โ
Pager: pageSize (PageSize::Fill โ the whole area without padding, PageSize::Fixed(dp), or PageSize{[](float availableSpace, float pageSpacing) {โฆ}}), snapPosition (SnapPosition::Start/Center/End: where the current page settles; pages do not scroll past their ends โ the first page's start rests at the start padding, the last page's end at the end padding; use contentPadding to bring the first and last pages to the center), all visible pages (plus one more and beyondViewportPageCount more) are in the composition; pages take as much space as they want (along the pager up to the page size, at the start of their slot), and are aligned on the cross axis with verticalAlignment (HorizontalPager, centered by default) / horizontalAlignment (VerticalPager); flingBehavior = PagerDefaults::flingBehavior(state, PagerSnapDistance::atMost(n), snapPositionalThreshold): turns as many pages as the fling velocity carries (at least one in that direction, at most n from the page it was released on), a slow drag turns the page once it passes snapPositionalThreshold of the page (in both directions), and it settles with snapAnimationSpec. PagerState: settledPage() (the page it last settled on; does not change while scrolling), targetPage() (the page the fling or animateScrollToPage is heading to; currentPage again once a finger intervenes), getOffsetDistanceInPages(page) (how many pages away that page is from its settled position โ read it in a graphicsLayer block for pages that shrink as they move away), canScrollForward/Backward(), scrollToPage(page, pageOffsetFraction), animateScrollToPage(page, pageOffsetFraction, spec). reverseLayout: pages are laid out from the end (the first page on the right โ at the bottom when vertical โ, the following ones to its left; paddings stay in place), and the finger works in the opposite direction (dragging, flinging, cooperation with a scrollable inside a page; in an RTL layout the two cancel each other out). key = [](int page) { return pageKey; }: keeps page state by key; when a page is inserted or removed before the current page, the current page remains the same page (its index shifts). Missing: layoutInfo | |
LazyGridState (rememberLazyGridState, firstVisibleItemIndex, scrollToItem/animateScrollToItem, layoutInfo) | โ
LazyGridOptions::state is now a LazyGridState: the position is tracked by item (the first item of the first visible row), scrolling to an item goes to its row (once the grid is laid out, if it isn't yet), layoutInfo().visibleItemsInfo gives each item's row/column, position and its own size (in a horizontal grid, rows are columns), maxSpan; it is saved. The underlying row list is lines() | |
LazyStaggeredGridState (rememberLazyStaggeredGridState, firstVisibleItemIndex, scrollToItem/animateScrollToItem, layoutInfo) | โ
LazyStaggeredGridOptions::state is now a LazyStaggeredGridState: the first visible item is the smallest index among the visible ones (observable), scrollToItem puts the item's top at the start of the content and keeps it there until the heights of its neighbors are measured (positions of unmeasured items are estimated from the average; another scroll releases it), layoutInfo().visibleItemsInfo gives the lane (lane, fullLine if it spans the full line), the scrolled position and the size, plus lanes; dp scrolling via scrollState() | |
HorizontalMultiBrowseCarousel, HorizontalUncontainedCarousel, rememberCarouselState (M3 Carousel) | โ
multi-browse: large items that fit the window (close to the preferred width), one medium, one small; as items scroll, they grow and shrink between keylines (always filling the window), the content stays at the large size and is masked from the center (itemShape, 28 dp corners by default), it snaps to an item on release, and at the end of the list the arrangement flips (so the last item also shows large); uncontained: fixed width, free scrolling; itemSpacing, contentPadding, currentItem, scrollToItem/animateScrollToItem, saveable. HorizontalCenteredHeroCarousel(state, maxItemWidth, โฆ): one large item in the middle, small ones on both sides (56 dp); at the start of the list the first item is large at the start, and at the end the last item is large at the end (with smooth transitions in between); on release an item settles as the large one; maxItemWidth caps the large item, and the small ones take the remaining space. The content can also take a CarouselItemScope: index and carouselItemInfo (size() is the mask's current width, minSize()/maxSize() its smallest and largest state); it changes on every scroll step without recomposing and is read while drawing (e.g. a label that fades in as the item grows). CarouselItemScope::maskClip(shape) / maskBorder(border, shape): the content is clipped to the part the item currently shows (the centered, item-wide mask), or a border is drawn around it (the item's own mask is itemShape; with RectangleShape, just its bounds) | |
nestedScroll (4) (NestedScrollConnection, NestedScrollDispatcher, NestedScrollSource) | โ pre/post scroll and fling chain; nested lists, a row inside a pager (the remaining velocity of a row flung past its end turns the page and does not go to the pager's parents; in Compose 1.11's source, the page connection swallows this velocity before the pager), a list in a bottom sheet; a scrollable element acts before the outer drag handlers | |
Modifier.anchoredDraggable, AnchoredDraggableState, DraggableAnchors | โ
values (any == type, usually an enum) and dp positions: DraggableAnchors<T>{{Start, 0}, {End, 300}} / .at(v, p), positionOf, closestAnchor(p[, searchUpwards]), min/maxPosition; state: currentValue/targetValue/settledValue (observable; the target triggers recomposition only when it changes), offset() (NaN until the anchors arrive; requireOffset() is then 0), progress(from, to), isAnimationRunning, lastVelocity, updateAnchors(newAnchors[, newTarget]) (from onSizeChanged; at rest it keeps its value and moves to that value's new position, a running animation retargets to the new position, and a drag continues within the bounds), snapTo, animateTo, settle(velocity), dispatchRawDelta. On release, Compose's rule: a fling faster than velocityThreshold (125 dp/s) goes to the anchor in that direction; otherwise, if positionalThreshold (halfway by default) was passed, to the next one; otherwise back; if confirmValueChange rejects, it goes back (animateTo/snapTo from code do not ask). The drag belongs to the user: it stops the animation, takes the touch slop and velocity from the host, does not spill over into surrounding scrollables, and does not respond to the mouse wheel; reverseDirection, interactionSource. rememberAnchoredDraggableState (the value is saved if T is saveable). SwipeToDismissBox and the sheet of BottomSheetScaffold use their own anchor logic; both settle with a spring that carries the release velocity (without overshooting the ends); SwipeToDismissBox, as in Compose, is also dismissed by a 125 dp/s fling (flung back quickly, it returns to place), or on a slow release once past 40% of the width | |
animateItem (5), SwipeToDismissBox (4), draggable (1), draggable2D, scrollable2D | โ
Modifier.scrollable2D(rememberScrollable2DState([](Offset d) { ...; return consumed; })) (Compose 1.9): scrolling in any direction after the touch slop; on release, a fling on each axis separately (with the scrollers' deceleration) โ an axis stops when it reaches its end โ, a press stops the fling, mouse wheel (the horizontal wheel too; at the end it hands off to the surrounding page); isScrollInProgress, dispatchRawDelta, stopScroll; no overscroll. Modifier.draggable2D(rememberDraggable2DState([](Offset d) {...}), {.interactionSource, .startDragImmediately, .onDragStarted, .onDragStopped, .reverseDirection}) (Compose 1.7): dragging in any direction is passed to the state after the touch slop (or as soon as it is pressed), the drag is consumed (the surrounding scroll does not get it), and on release the velocity (dp/s, both axes) is reported; dispatchRawDelta drags from code; callbacks are refreshed on every recomposition without restarting the gesture. animateItem: placement changes (spring), fade-in when inserted, and when removed it fades out in its old place in its last state (without recomposition; its data may already be gone), not receiving touches while fading; animateItem(fadeInSpec, placementSpec, fadeOutSpec) | |
PullToRefreshBox (PullToRefreshState, PullToRefreshDefaults.Indicator) | โ
a Box with Modifier.pullToRefresh; releasing past the threshold calls onRefresh, with a spinning indicator while refreshing. Options: state, threshold, enabled, contentAlignment, indicator. The box no longer clips its content (the indicator clips itself, as in Compose) | |
Modifier.pullToRefresh, PullToRefreshDefaults.IndicatorBox | โ
as in Compose 1.4.0: pullToRefresh(isRefreshing, state, onRefresh, enabled, threshold) on any box; it moves half the finger's distance and slows down more and more past the threshold (at most twice the threshold); pulls are ignored while the state (distanceFraction: 0 hidden, 1 at the threshold) is animating, and while refreshing, scrolls pass to the list; on the first draw the state is set to match isRefreshing (without animation). PullToRefreshState.snapTo. IndicatorBox(state, isRefreshing, modifier, content, maxDistance, shape, containerColor, elevation): a circle that emerges from below the top edge (no part of it is visible above that edge), with a shadow once it is out; for putting in your own indicator. Indicator(state, isRefreshing, modifier, containerColor, color, maxDistance): transitions between Compose's arrowed arc (empty for the first 40%, faint until the threshold, full color at the threshold, rotating beyond it) and a 16 dp spinning indicator; colors indicatorContainerColor (surfaceContainerHigh) and indicatorColor (onSurfaceVariant). Note: the old Indicator(state, isRefreshing, threshold, modifier) signature has changed | |
stickyHeader (LazyColumn/LazyRow) | โ the section header stays at the top, and the next header pushes it out; a distant header is composed too | |
LazyHorizontalGrid | โ
rows split the height, columns are lazy like LazyRow; FullLine is a whole column | |
LazyVerticalStaggeredGrid, LazyHorizontalStaggeredGrid, StaggeredGridCells | โ
items at their own height go into the shortest column (horizontally: at their own width, after the shortest row; horizontalItemSpacing, verticalSpacing; the state's positions, scroll and layoutInfo are along x); the visible area plus a half-screen margin is composed; FullLine header; state: LazyStaggeredGridState (its scrolling through scrollState(), in dp) | |
scrollable (3), rememberScrollableState, scrollableArea | โ
drag/fling deltas in the finger's direction; the unused part goes to the outer scrollable; reverseDirection. scrollableArea(state, orientation[, overscrollEffect], enabled, reverseScrolling) (Compose 1.9's): for layouts that place their content from the state themselves: scrollable + clipping along the axis (like the scroll containers, with a 30 dp margin across) + overscroll drawn on top (stretch or the given effect; nullptr: none); with enabled false it scrolls only from code, and the clipping stays | |
KeyboardOptions/KeyboardActions (3/1) | โ
on Android, the keyboard type (email, number, password...) and the action key; showKeyboardOnFocus = false: when focused from code (FocusRequester, Tab), typed input goes to the field but the software keyboard does not open; it opens on tap (or long press) | |
rememberTextFieldState (5) (TextFieldState), multi-line field (singleLine, minLines, maxLines, TextFieldLineLimits) | โ
multi-line by default, as in Compose; wrapping at word boundaries, Enter for a new line, arrows/Home/End across lines, tapping to put the cursor on a line, internal scrolling past maxLines; a multi-line keyboard on Android. TextFieldState also holds the selection (selection(), observable; UTF-8 byte offsets), edit([](TextFieldBuffer& b) { b.append(โฆ); b.placeCursorAtEnd(); }) (replace/insert/remove/selectAll/setSelection; a replacement shifts the selection along with the text after it), setTextAndSelectAll; while the field is focused, the keyboard picks up changes made from code together with their text and selection, and on focus it starts from that selection. TextFieldOptions::inputTransformation: InputTransformation::maxLength(n) (in code points; an edit that exceeds it is rejected), allCaps("tr") (uppercases only what is typed; in Turkish i โ ฤฐ, ฤฑ โ I), byValue([](const std::string& current, const std::string& proposed) { โฆ }), a.then(b); a modified or rejected edit is reported back to the keyboard, and changes made from code are not transformed. TextFieldOptions::outputTransformation: OutputTransformation{[](TextFieldBuffer& b) { b.insert(3, " "); }} changes only the display (phone/card grouping, prefix/suffix); the value, the keyboard, copying and accessibility get the text itself; the cursor and taps skip over the inserted parts (the cursor stops before something inserted at a position, and after something inserted at the very start: "$ | 250 USD"). state.undoState(): canUndo()/canRedo() (observable), undo(), redo(), clearHistory(); Ctrl+Z, Ctrl+Shift+Z / Ctrl+Y in the field. Typing is undone word by word (a space after a word starts a new step, a line break is a separate step), consecutive deletions are one step; an edit made from code is a step of its own; a new edit discards whatever could be redone; at most 100 steps lineLimits = TextFieldLineLimits::SingleLine / MultiLine(min, max) (as in Compose's state-based fields) replaces singleLine/minLines/maxLines |
BasicTextField (1) | โ undecorated editable text (placeholder, single/multi-line) | |
| Touch selection handles | โ a long press in a text field selects a word; draggable teardrop handles, a Cut/Copy/Paste/Select all toolbar; it ends on typing, on a focus change or on a tap elsewhere While a handle or the cursor is being dragged with a finger, Android's text magnifier (as in Compose): a 100ร48 dp lens with rounded corners, 42 dp above the finger, showing that part of the line magnified 1.25 times (it follows the finger horizontally and stays centered on the line), with a shadow; it disappears on release (in plain-text selection too) | |
Modifier.magnifier | โ
magnifier([] { return sourceCenter; }, {.magnifierCenter, .zoom = 1.25f, .size = {100, 48}, .cornerRadius = 24, .elevation = 4}) (as in Compose): drawn above everything, the area around the source point (relative to the modifier's position) magnified (the scene is redrawn in that layer), placed 42 dp above the source by default; the source is read while drawing, and NaN hides it. Note: popups do not appear in the magnifier | |
| Cursor handle (on a finger tap), word selection on a mouse double-click | โ dragging the handle moves the cursor, tapping it shows Paste/Select all; the host tells finger and mouse apart (a mouse long press does not select) | |
Selection in plain text (SelectionContainer, DisableSelection) | โ
a single selection spans all the Texts inside (in reading order): a long press with a finger selects a word, the handles drag from one text to the next, Copy/Select all; selection by dragging with the mouse, a word on double-click, Ctrl+C / Ctrl+A / Escape; the contents of DisableSelection are not selected. Colors come from LocalTextSelectionColors (TextSelectionColors{handleColor, backgroundColor}; MaterialTheme provides the scheme's primary and 40% of it), in fields too |
Material 3
| API | Status |
|---|---|
Text (160), MaterialTheme (75), Button (59), Surface (18), Card (17), TextButton (12) | โ |
MotionScheme, MaterialExpressiveTheme, MaterialTheme.motionScheme | โ
MotionScheme: default/fast/slow springs for things that move (spatial: position, size, shape) and things whose appearance changes (effects: color, opacity); MotionScheme::standard() (spatial damping 0.9 โ stiffness 700/1400/300; effects 1.0 โ 1600/3800/800) and MotionScheme::expressive() (spatial 0.8/380, 0.6/800, 0.8/200 โ bouncy; effects the same); motionScheme() returns the ambient one (LocalMotionScheme, standard at the top). MaterialTheme(colorScheme, motionScheme, content); the two-parameter MaterialTheme keeps the ambient motion; MaterialExpressiveTheme(colorScheme or std::nullopt, [motionScheme = expressive()], content). Components take their motion from the scheme: shape-morphing buttons and list items, the growth of the pressed button in a ButtonGroup, the rotation of the SplitButton arrow, ToggleFloatingActionButton (fastSpatial), the Snackbar's entrance (fastSpatial + fastEffects), the ShortNavigationBar indicator (defaultSpatial) |
BasicText, BasicSecureTextField, BasicTextField decorationBox / decorator | โ
BasicText(text, modifier, style): takes nothing from its surroundings (Compose's TextStyle.Default: 14 sp, black; not LocalTextStyle and LocalContentColor), with a string or an AnnotatedString; BasicSecureTextField(state, modifier, options): an undecorated password field โ just the dots (no container, line or label), cannot be copied; BasicTextField and BasicSecureTextField take a decorator (Compose's decorationBox/decorator): [&](const Content& innerTextField) {โฆ} draws the surroundings of the text itself and places the text with innerTextField() (which takes the remaining space in a Row); a touch on the decoration โ one not taken by clickables inside it โ goes to the field (focus, with the cursor at the nearest end of the text) |
lightColorScheme/darkColorScheme (11/10), HorizontalDivider (8), Checkbox (4), TriStateCheckbox (ToggleableState On/Off/Indeterminate), Switch (3) | โ
as in Compose 1.4: a 20 dp box inside a 2 dp border; the check mark is drawn along its path (default spatial spring), in the indeterminate state it bends into a line in the middle, and when unchecked it disappears all at once after 100 ms; colors animate with the effects springs, disabled is onSurface 38% (instant). Sizes as in Compose (see minimumInteractiveComponentSize) |
Switch(checked, onCheckedChange, modifier, SwitchOptions{enabled, interactionSource, thumbContent}) | โ
Compose 1.4's colors: when on, a primary track and an onPrimary thumb; when off, a surfaceContainerHighest track, an outline thumb and border; when disabled, faded over the surface (onSurface 12% track, surface thumb; 38% when off). The thumb slides with the fast spatial spring (0.9 / 1400) and becomes 28 dp instantly on press. thumbContent: a 16 dp icon in the thumb (onPrimaryContainer when on, surfaceContainerHighest when off), which keeps the thumb at its large size and slides with it (mirrored in RTL); the thumb is 16 dp off, 24 dp on, 28 dp pressed (the transitions are animated) |
TextField (8), OutlinedTextField (6) | โ
floating label, icons, supporting text, error, password; tap to place the cursor; arrows/selection/clipboard on desktop; textStyle (font, color, alignment); readOnly; prefix/suffix ({.label = "Tutar", .prefix = "โบ", .suffix = ",00"}: once the label moves up โ always, if there is no label โ they sit on both sides of the text in a muted color; the text makes room for them, and touch and the cursor follow suit); colors (TextFieldColors, Compose's TextFieldDefaults.colors()): focused/unfocused/error colors for the text, container, cursor, selection, indicator line, icons, label, placeholder, supporting text, prefix/suffix (error takes precedence over focus); unset ones are M3's, and the textStyle color overrides the text color; SecureTextField takes them too; enabled = false (not focusable, editable or selectable; disabled colors: onSurface 38%, outlined border 12%; disabling it while focused releases focus); shape (the corners of the container and the border; by default 4 dp on the top corners only for the filled field โ Compose's TextFieldDefaults.shape โ, and 4 dp on all four corners for the outlined one; the border follows the shape's corners, and the label gap is preserved); a labeled OutlinedTextField reserves 8 dp at the top (64 dp; the raised label stays inside the field, as in Compose); labelPosition: TextFieldLabelPosition::Attached(true) keeps the label raised at all times (the placeholder and prefix/suffix are also visible when empty), Above(alignment) puts the label above the field, outside it (bodySmall, 4 dp side and bottom spacing) |
SecureTextField, OutlinedSecureTextField, TextObfuscationMode | โ
single line, password keyboard (no suggestions), cannot be copied/cut; RevealLastTyped (default: a single typed character โ in the middle too โ is visible for 1.5 s; deleting, pasting or losing focus hides it immediately), Hidden, Visible (for an eye button); textObfuscationCharacter |
Icon (43) + Material icons (icons.filled 92) | โ
35 Filled icons made as Compose's materialIcon makes them (named Filled.Addโฆ, 24 ร 24, one path filled black); Icons::AutoMirrored::Filled (ArrowBack, ArrowForward, KeyboardArrowLeft/Right, List, Send) vectors of their own that mirror right to left. Icon(imageVector) is Icon(rememberVectorPainter(imageVector)), as in Compose: its default size (24 dp a Material icon's), tinted with LocalContentColor by a SrcIn color filter (a one-color icon an Alpha8 mask in the tint) |
ImageVector, ImageVector.Builder, VectorPath, VectorGroup, PathNode, PathParser, PathBuilder, addPathNodes | โ
as Compose's: an ImageVector is its name, default size (dp), viewport, a tree (root(): VectorGroups of VectorNodes, each a VectorPath or a VectorGroup), tintColor/tintBlendMode (SrcIn) and autoMirror; equal by all of these (not its id). ImageVector::Builder(name, defaultWidth, defaultHeight, viewportWidth, viewportHeight, tintColor, tintBlendMode, autoMirror) (the name can be left out) .addPath({.pathData = addPathNodes("Mโฆ"), .fill = โฆ}), .path({.stroke = โฆ, .strokeLineWidth = 2}, [](PathBuilder& p) { p.moveTo(โฆ).lineTo(โฆ); }), .addGroup({.name, .rotation, .pivotX, .pivotY, .scaleX, .scaleY, .translationX, .translationY, .clipPathData}) โฆ .clearGroup() or .group({โฆ}, [](ImageVector::Builder& b) { โฆ }), .build() (open groups closed). VectorPath: name, pathData, pathFillType, fill/stroke Brush (unset: none; gradients in the path's units, a far edge the drawn size), their alphas, strokeLineWidth (0: a hairline), cap/join/miter, trimPathStart/End/Offset (fractions of its first contour's length, as Android measures it; wrapping round its end). A group draws what is in it through Compose's matrix (moved by its translation and pivot, turned, scaled, moved back by its pivot) and clipped to its clip path. Path data as Compose's PathParser reads it (PathNodes: the 19 commands, absolute and relative; numbers run together, exponents, +; an arc's flags one character each, in every set) and toPath draws it (reflective curves after their kind; arcs as cubics of an eighth of a turn at most, radii too small grown; a zero radius a line, as SVG has it); PathParser keeps nodes, PathBuilder/PathData makes them by calls. ImageVector::fromSvgPath(data): one path filled black |
vectorResource (Android vector drawable XML) | โ
vectorResource("vectors/compass.xml") reads a <vector> file from the app's assets (on Android, the APK's) once and builds an ImageVector; ImageVector::fromVectorXml(xml) from a string. width/height (dp), viewportWidth/viewportHeight, autoMirrored, tint/tintMode (src_in, src_over, src_atop, multiply, screen, add), alpha (fading its paths' fills and strokes); nested <group> (name, rotation, pivotX/Y, scaleX/Y, translateX/Y, in Android's order); <path> pathData, fillColor/strokeColor (#RGB, #ARGB, #RRGGBB, #AARRGGBB), fillAlpha/strokeAlpha, strokeWidth (1 unset, as Compose reads it), strokeLineCap/Join, strokeMiterLimit, fillType; <gradient> inside <aapt:attr name="android:fillColor|strokeColor">: linear (startX/Y, endX/Y), radial (centerX/Y, gradientRadius), sweep (centerX/Y), <item offset color> stops or startColor/centerColor/endColor, tileMode (clamp, repeat, mirror), in its path's units (drawn through its groups). names of paths, groups and <clip-path>s; trimPathStart/End/Offset; a <clip-path> clips what comes after it in its group. Not yet: @color/โฆ references render black |
AnimatedImageVector, animatedVectorResource, rememberAnimatedVectorPainter | โ
Android's single-file <animated-vector> (as written by Android Studio and Shape Shifter): the drawing inside <aapt:attr name="android:drawable">, and the animation of each <target android:name> inside <aapt:attr name="android:animation">: <objectAnimator> (propertyName or <propertyValuesHolder>s; valueFrom โ the element's own value if absent โ and valueTo; valueType float/color/path; duration, startOffset, repeatCount, repeatMode; Android's interpolators), <set> together or sequentially. Animatable: the rotation, pivot, scale and translation of groups; the pathData of paths (morphing between paths made of the same commands, otherwise switching at the halfway point), fill/stroke colors and alphas, strokeWidth, trimPath*; the vector's alpha. AnimatedImageVector::vectorAt(ms) is the vector at that moment; rememberAnimatedVectorPainter(avd, atEnd) draws the start or the end, and plays toward it when atEnd changes (backwards when it turns off). Not yet: animators in separate files (@animator/โฆ), keyframes, path interpolators; an infinite repeat plays once |
Scaffold (34), TopAppBar (16), NavigationBar, BottomAppBar (5) | โ |
FlexibleBottomAppBar (M3 Expressive), BottomAppBarDefaults::exitAlwaysScrollBehavior | โ
the flexible bottom bar is 64 dp, with its content spread out 16 dp in from the sides (horizontalArrangement SpaceBetween, contentPadding, expandedHeight); BottomAppBarSlots::scrollBehavior / FlexibleBottomAppBarOptions::scrollBehavior: as the content scrolls forward, the bar loses that much height and slides down (the Scaffold's content grows), comes back as it scrolls back, and when left halfway it completes based on its velocity or the nearer end; attach nestedScrollConnection to an ancestor of the scrolled content (the Scaffold) |
ShortNavigationBar, ShortNavigationBarItem (M3 Expressive) | โ
64 dp tall (surfaceContainer, with the side and bottom insets of the system bars via windowInsets): EqualWeight items share the width, with the icon above the label and a 56ร32 indicator behind the icon; Centered items are grouped in the middle at equal width (that of the widest), with the label beside the icon and the indicator (a 40 dp pill) wrapping both. The default arrangement depends on the window (equal sharing below 600 dp, centered above), and the item's iconPosition follows the arrangement. The indicator grows in from the center (spring), and the ripple is bounded to it; the selected icon is onSecondaryContainer, the label secondary; tab role and selected state for accessibility |
WideNavigationRail, WideNavigationRailItem, rememberWideNavigationRailState (M3 Expressive) | โ
96 dp when collapsed, each icon above its label (64 dp items, 56ร32 indicator); with state.expand()/collapse()/toggle() (usually from a menu button in the header) it expands to 220 dp with a spring, and the items turn into 56 dp rows: the label after the icon, the indicator wrapping both (20 dp inset). Items follow the rail's state automatically (overridable with railExpanded); 40 dp between the header and the items, collapsedWidth/expandedWidth, the start/top/bottom insets of the system bars via windowInsets. ModalWideNavigationRail(modifier, items, {state, header, hideOnCollapse, โฆ}): when collapsed, a collapsed rail in place (or nothing, with hideOnCollapse); when expanded, it slides in from the start edge above everything (surfaceContainer, end corners rounded 16 dp) and dims what is behind it; tapping the scrim or pressing back closes it, tapping an empty part of the rail does not; the content leaves room only for the collapsed rail |
TopAppBarDefaults scroll behaviors (pinned/enterAlways/exitUntilCollapsed), MediumTopAppBar/LargeTopAppBar, TopAppBarState | โ via nestedScroll; a bar left halfway settles at the nearer end |
AppBarRow, AppBarColumn (AppBarScope: clickableItem, toggleableItem, customItem), AppBarMenuState | โ
as in Compose 1.5: actions that fit are icon buttons in the bar, the rest go into the โฎ menu (visible only when something is left out; space is reserved for the button, and the last action can use that space); maxItemCount caps the number of slots (the menu button being the last); overflowIndicator is your own open button; AppBarColumn is the same, vertically (a rail, a vertical toolbar; menu button at the bottom) |
Top bar colors, heights and insets (Compose's TopAppBarColors, expandedHeight, collapsedHeight, windowInsets) | โ
in TopAppBarSlots: containerColor (surface), scrolledContainerColor (surfaceContainer), navigationIconContentColor and titleContentColor (onSurface), actionIconContentColor and subtitleContentColor (onSurfaceVariant; actions are now onSurfaceVariant, as in Compose). expandedHeight (single-line bars 64 dp; medium/large 112/152, 24 more with a subtitle) and collapsedHeight (medium/large 64; if the expanded height is smaller than that, it is raised to match), with the constants TopAppBarDefaults::TopAppBarExpandedHeight, Medium/LargeAppBarCollapsed/ExpandedHeight. windowInsets (default TopAppBarDefaults::windowInsets(): the top and sides of the safe drawing area; WindowInsets{} for none). Note: in Compose 1.4.0, titleHorizontalAlignment and the Flexible bars are still internal API |
IconButton (16), FloatingActionButton (10), ExtendedFloatingActionButton (4) | โ |
MediumFloatingActionButton, SmallExtendedFloatingActionButton / MediumExtendedFloatingActionButton / LargeExtendedFloatingActionButton (M3 Expressive) | โ
the medium FAB is 80 dp (icon 28, corner 20); the extended FABs are 56 / 80 / 96 dp tall (icons 24 / 28 / 36, labels titleMedium / titleLarge / headlineSmall, side padding 16 / 26 / 28, icon-to-label gap 8 / 12 / 16, corners 16 / 20 / 28); when expanded turns off, the label fades and the button shrinks to a square FAB around its icon (with a spring), and accessibility still reads its label |
ToggleFloatingActionButton, FloatingActionButtonMenu, FloatingActionButtonMenuItem (M3 Expressive) | โ
a FAB that stays checked: primaryContainer โ primary, from a rounded square (16 dp) to a circle, optionally from one size to another (containerSize โ checkedContainerSize, shrinking toward contentAlignment); the content reads checkedProgress() (e.g. to rotate a plus sign by 45ยฐ into a cross), and the content color transitions too. Menu: the actions sit in a column above the button (4 dp apart, 8 dp from the button), pill-shaped (56 dp, primaryContainer, icon + label); they appear bottom to top when opening and disappear top to bottom when closing, 50 ms apart (expanding and fading); horizontalAlignment (end by default). Note: back does not close the menu by itself (as in Compose, the BackHandler is up to the app) |
HorizontalFloatingToolbar, VerticalFloatingToolbar, FloatingToolbarDefaults (M3 Expressive) | โ
a pill-shaped toolbar floating over the content (64 dp, 8 dp inner padding, shadow): the middle actions are always visible, while leadingContent/trailingContent open and close only with expanded (expanding horizontally or vertically). Colors: standardFloatingToolbarColors() (surfaceContainer; its FAB secondaryContainer) or vibrantFloatingToolbarColors() (primaryContainer; its FAB tertiaryContainer). The FAB variant: the toolbar plus a FAB 8 dp after it (below it when vertical); when collapsed, only the FAB remains (StandardFloatingActionButton/VibrantFloatingActionButton). exitAlwaysScrollBehavior(exitDirection): as the content scrolls forward, the bar slides out in that direction (Bottom/Top/Start/End) by the same amount, and comes back as it scrolls back; when left halfway, it completes based on its velocity or the nearer end (it does not consume the scroll); the limit is the bar's size + ScreenOffset (16 dp) |
ToggleButton, ElevatedToggleButton, TonalToggleButton, OutlinedToggleButton, ButtonShapes (M3 Expressive) | โ
ToggleButton(checked, onCheckedChange, modifier, {shapes, colors, enabled, interactionSource}, content): when checked, the corners morph from round to squarer (12 dp), and squarer still while pressed (8 dp) (Compose's fast spatial spring: damping 0.9, stiffness 1400); unchecked/checked colors depend on the type (surfaceContainer โ primary; Elevated surfaceContainerLow + shadow; Tonal secondaryContainer โ secondary; Outlined border โ inverseSurface), with a checkbox role and state for accessibility. Plain buttons also get squarer while pressed with ButtonStyle::shapes = ButtonShapes{} |
ButtonGroup, ButtonGroupScope::animateWidth (M3 Expressive) | โ
ButtonGroup([&](const ButtonGroupScope& group) { Button(onClick, group.animateWidth(interactionSource), {.interactionSource = interactionSource}, โฆ); โฆ }): buttons in a row with gaps (spacing, 12 dp); the one held down widens by expandedRatio (15%), its neighbors give up that share half each (at an end, the single neighbor gives all of it), and the row's width stays the same (fast spatial spring); a squeezed button keeps its height. The horizontal padding of buttons (and ToggleButtons) now flexes: when the full width given is too narrow, the edges shrink before the text does. Connected group (in place of segmented buttons): ToggleButtons with ButtonGroupDefaults::ConnectedSpaceBetween (2 dp) gaps, connectedLeading/Middle/TrailingButtonShapes() (outer corners half, inner corners 8 dp, 4 dp while pressed; the checked one fully round, with corners animating individually). Group from items: ButtonGroup({{.label, .icon, .onClick} / {.label, .checked, .onCheckedChange}, โฆ}) shows the items that fit as buttons (those with a checked state as ToggleButtons) and puts the rest into a trailing โฎ menu (shown with their icons in the menu, checked ones with a check mark; the menu closes on selection); widths are measured without being shown |
SplitButtonLayout, SplitButtonDefaults::LeadingButton/TrailingButton/TrailingButtonIcon (M3 Expressive) | โ
the primary action and the menu button 2 dp apart; outer corners half, the inner corners where they meet 4 dp (8 dp while pressed); the menu button (48 dp) is checked while its menu is open: it turns fully round and its arrow (TrailingButtonIcon) flips; colors via SplitButtonStyle::kind (Filled/Tonal/Elevated/Outlined, the same when checked); checked state for accessibility |
FilledIconButton/FilledTonalIconButton/OutlinedIconButton, IconToggleButton/FilledIconToggleButton/FilledTonalIconToggleButton/OutlinedIconToggleButton | โ
IconToggleButton(checked, onCheckedChange, modifier, {.checkedContainerColor, .checkedContentColor, .enabled, .interactionSource}, content); colors are the Material 3 defaults (filled: gray container โ primary; tonal: โ secondary container; outlined: when on, the border goes away and a dark inverse surface appears); a checked/unchecked checkbox for accessibility; the ripple stays inside the circle, in the content color |
SnackbarHost/SnackbarHostState (7/8), Circular/LinearProgressIndicator (5), ListItem (13) | โ
Progress indicators as in Compose 1.4: round caps, a 4 dp gap (plus the caps) between the indicator and the track (secondaryContainer), a stop dot at the end of the determinate linear one; the indeterminate linear one has two lines (heads and tails on their own timings within 1.75 s, emphasized accelerate), the indeterminate circular one has no track and rotates 1080ยฐ in a 6 s cycle, plus another 90ยฐ every 1.5 s over 300 ms, with its arc growing from 10% to 87% and shrinking back. co_await state.showSnackbarAsync(message, actionLabel, duration, withDismissAction) โ SnackbarResult::ActionPerformed / Dismissed (Compose's suspending showSnackbar): shown after the queued ones, it returns once the snackbar is gone; one without an action is Short, one with an action stays until answered (Compose defaults); if the awaiting coroutine is cancelled, the snackbar goes too; withDismissAction adds a close button. The old showSnackbar(message, actionLabel, onAction, duration) is still there too. Snackbar(content, modifier, {.action, .dismissAction, .actionOnNewLine, .shape, colors}) and Snackbar(data): Compose's layout (at most 600 dp; a single line is 48 dp, with the action on the text's baseline; if longer, the first baseline is 30 dp down, at least 68 dp; with actionOnNewLine, the action goes at the bottom end); SnackbarHost(state, modifier, [](const SnackbarData& d) { โฆ }) for your own snackbar (d.performAction(), d.dismiss()); enter/exit as in Compose: fade + scale from 80%. CircularProgressIndicator's stroke width is strokeWidth (4 dp by default) |
Interactive ListItem(onClick / selected / checked, ...), SegmentedListItem, ListItemShapes, ListItemDefaults::segmentedShapes | โ
as in M3 Expressive: the whole row takes the touch (click, select or check; long press), its shape morphs with the state (4 dp at rest, 12 on hover, 16 when pressed, focused or selected), the selected/checked one sits on secondaryContainer; disabled is onSurface 38%; 16/10 dp padding, 12 dp gap, at least 56 dp. SegmentedListItem + segmentedShapes(i, n): rows that look like a single card (the top corners of the first and the bottom corners of the last are 16 dp), SegmentedGap (2 dp) apart |
LinearWavyProgressIndicator, CircularWavyProgressIndicator (M3 Expressive), WavyProgressIndicatorDefaults | โ
determinate (progress) and indeterminate versions: the indicator advances as a wave, while the track (trackColor, secondaryContainer by default) stays flat and is separated from the indicator by gapSize; a stop dot (stopSize) at the end of the linear one's track. The wave height comes from amplitude(progress) (default: flat below 10% and above 95%, with the change animated over 500 ms), wavelength (linear 40 / indeterminate 20 / circular 15 dp), waveSpeed (dp/s, by default one wavelength per second; when flat, the wave stops and requests no frames); the linear one's wave fills its height (10 dp โ stroke), the circular one's is ยฑ1.6 dp, with a whole number of waves rotating around the circle. Indeterminate linear: Compose's two lines chase each other over 1750 ms, with the track in the gaps between them; indeterminate circular: rotates, grows and shrinks. The linear ones start at the layout's start (from the right in RTL; the plain LinearProgressIndicator now does too). The amplitude transition takes 500 ms as in Compose (with the standard curve when increasing, emphasized accelerate when decreasing). Note: the indeterminate circular timing is that of CircularProgressIndicator |
LoadingIndicator, ContainedLoadingIndicator (M3 Expressive) | โ a 38 dp shape inside 48 dp keeps rotating and morphs into the next one every 650 ms (soft burst, 9-sided cookie, pentagon, pill, sunny, 4-sided cookie, oval; each morph also turns a quarter turn with a slight overshoot, on top of one full turn every 4.7 s); in the determinate version, the shape grows from a circle to a soft burst as progress goes from 0 to 1, and turns half a turn. The contained one is onPrimaryContainer in a primaryContainer circle; the other one is primary. Note: instead of Material's shapes (androidx.graphics.shapes polygons and Morph), the shapes are approximated with distance-from-center functions, and the morph blends angle by angle |
Slider thumb and track (thumb/track), SliderState (rememberSliderState), SliderColors, SliderDefaults::Thumb/Track | โ
SliderOptions::thumb/track are built from the slider's state (value, range, steps, pressed/dragging, colors); the default is the classic circle thumb and a thin track. Presets: CircleThumb (classic, with a halo while pressed), Thumb (Material 3's 4ร44 dp bar, half as thick while pressed; lying flat on a vertical slider), ShapeThumb(shape) (any shape: square, MaterialShapes diamond/cookie/sunnyโฆ; grows a little while pressed), IconThumb(icon), ValueThumb (a pill showing the value); ClassicTrack (4 dp, step dots), Track (Material 3: 16 dp, a 6 dp gap from the thumb, 2 dp inner corners, a stop dot at the end, step dots 8 dp in from the ends; a thumb on a step sits over its dot). SliderDefaults::colors() gives Material 3's colors, classicColors() the classic ones. Slider(state) holds its own value (setValue snaps it to the range and step). RangeSlider: startThumb/endThumb (falling back to thumb, and then to the classic circle), each with its own SliderState (its value, whether it is held), rangeTrack with the range's RangeSliderState (activeRangeStart/End, held(), setActiveRange ordered and stepped); SliderDefaults::ClassicTrack/Track work for ranges too (the Material 3 one with a gap from both thumbs and stop dots at both ends); rememberRangeSliderState. Value bubble: SliderDefaults::Label(state, label, thumb, isPersistent) shows a label (PlainTooltip) above the thumb while it is held (or always). Sliders take a 48 dp touch area as in Compose (minimumInteractiveComponentSize) |
Label (M3), Modifier.minimumInteractiveComponentSize, LocalMinimumInteractiveComponentSize | โ
Label(label, modifier, interactionSource, isPersistent, content): while the interaction source reports pressed/dragged/hovered (or always), a label above the content, centered, 4 dp away (below it if there is no room); it takes no input. minimumInteractiveComponentSize: anything smaller grows to 48 dp (a local value; 0: off), with the content centered; touches (and mouse clicks) landing in that area go to the clickable inside. Users, as in Compose: Slider, RangeSlider, all IconButton/IconToggleButton types (a 40 dp circle in a 48 dp area), Checkbox/TriStateCheckbox/RadioButton when they handle their own click (24 dp, 48 dp area, a 40 dp unbounded ripple), and Switch (52ร32, 48 dp height). Those that leave the click to the row (no callback) keep their own size: checkbox and radio 24 dp, switch 52ร32. This is why the title in the top bar now sits at 56 dp after the navigation button (as in Compose). Also the ones built on a clickable Surface in Compose: all Button types, the ToggleButton types, SplitButton parts (40 dp height, 48 dp area), all chips (32 dp, 48 dp area) and SmallFloatingActionButton (40 dp, 48 dp area); the larger FABs are already bigger than 48. The SegmentedButton row is 48 dp: the border, the round ends and the selected fill are in the middle 40 dp band, and each segment takes touches from top to bottom (in Compose each segment draws its own border; it looks the same). Note: the top and bottom edges of a segment's keyboard focus ring are not visible, since they are clipped to the band |
FilterChip (3), AssistChip/SuggestionChip/InputChip (and the shadowed, borderless ElevatedAssistChip/ElevatedSuggestionChip/ElevatedFilterChip), RadioButton, Slider (2), SegmentedButton (2) (SingleChoiceSegmentedButtonRow: radio buttons, selectableGroup; MultiChoiceSegmentedButtonRow + SegmentedButton(checked, onCheckedChange, โฆ): checkboxes; {.enabled, .icon, .interactionSource}), PrimaryTabRow/Tab (2) (and LeadingIconTab, with its icon beside the text, 48 dp), Badge/BadgedBox, ElevatedButton/FilledTonalButton, ElevatedCard/OutlinedCard, VerticalDivider, ButtonDefaults | โ
Buttons as in Compose 1.4: 40 dp height, at least 58 dp width, 8 dp vertical padding; ButtonStyle::contentPadding (ButtonDefaults::ButtonWithIconContentPadding etc.: 16 dp at the start for a button with an icon); the outlined button uses outlineVariant; filled/tonal buttons get 1 dp on hover, the elevated button 1 dp (3 on hover), and they drop back while pressed |
RangeSlider | โ
the thumb nearest the press point responds, and the thumbs never cross; when they overlap, the drag direction picks one; steps |
VerticalSlider (M3 Expressive) | โ
an upright slider: as tall as possible, 44 dp wide; the value increases upward (downward with reverseDirection), taps and vertical drags adjust it (it does not take horizontal drags), all of SliderOptions (steps, interactionSource, onValueChangeFinished, enabled); it shares its implementation with the horizontal Slider |
SecondaryTabRow, PrimaryScrollableTabRow/SecondaryScrollableTabRow (ScrollableTabRow) | โ secondary: a thin indicator as wide as the tab; scrollable: tabs at their own width (at least 90 dp), edge padding, and the selected tab scrolls to the center |
dynamicLight/DarkColorScheme (10/10) | โ
Android 12+ wallpaper palette (JNI, Compose's tone mapping); on Linux, from the desktop's accent color (XDG portal, TonalPalette::fromSeed); the default scheme on other desktops |
Dialog/AlertDialog (5/2), ModalBottomSheet (3), DropdownMenu/DropdownMenuItem, Popup, BackHandler | โ
overlay layer: popups are composed in place, drawn on top, and receive input; back/Escape closes them. ModalBottomSheet(onDismissRequest, rememberModalBottomSheetState(skipPartiallyExpanded), โฆ): a sheet taller than half the screen opens halfway (PartiallyExpanded), expands fully when dragged up, and hides when dragged down (built on anchoredDraggable, with the fling velocity); currentValue/targetValue/isVisible are observable; show(), hide() (from code, onDismissRequest is not called โ co_await state.hide(); showBottomSheet.set(false);), expand(), partialExpand(); the scrim, back and dragging down slide it away and then call onDismissRequest; a list inside scrolls first, and a half-open sheet expands before the list scrolls. The back gesture (Android 14+) shrinks the sheet toward its bottom center as in M3 (at most 48 dp narrower and 24 dp shorter; the content does not stretch): on completion it hides, a fully expanded sheet that has a halfway stop goes down to half, and on cancel it grows back. A BackHandler/PredictiveBackHandler inside a focusable popup (Dialog, sheet, menu) gets back before the popup closes; the window's handlers do not get it while a popup is on top |
PredictiveBackHandler(enabled) { progress -> ... }, BackEventCompat | โ
Android 14+ back gesture: while enabled, when a gesture starts, the onBack coroutine starts with the flow of steps (Flow<BackEventCompat>: progress 0..1, touchX/touchY in dp, swipeEdge); the flow completes when the gesture ends and the coroutine continues (close, navigate back); if the gesture is cancelled, the coroutine is cancelled (an RAII guard inside it undoes the effect); the back key/Escape starts it with an empty flow. If there is a popup or focus, they are closed/cleared when the gesture ends. On Android 13+, the app registers its OnBackInvokedCallback only while something will take back: otherwise the system exits the app with its own "back to home" animation; before 13, the back key comes from SDL |
Modifier.systemGestureExclusion(), systemGestureExclusion(rect(coordinates)) | โ
drags that start at the edge go to the app instead of the system's back/home gesture (Compose's; with gesture navigation on Android 10+, where Android allows at most 200 dp per edge): the host collects the visible rectangles after layout (ComposeHost::systemGestureExclusion()), and on Android calls View.setSystemGestureExclusionRects when they change; the second form narrows it to a rectangle local to the node |
Autofill (ContentType, autofillManager()) | โ
Compose 1.8's autofill: semantics({.contentType = ContentType::Username + ContentType::EmailAddress}) (Username, Password, NewPassword, EmailAddress, PhoneNumber, CreditCardNumber, PostalCode, SmsOtpCode and Compose's other types; combined with +) tells what a text field accepts. On Android 8+, text fields are exposed to autofill as virtual views of the surface (hints; password fields via setDataIsSensitive): the password manager is notified when focus enters/leaves a field, when the text changes, and when fields appear and disappear; the manager fills the fields (SemanticsAction::SetText, which goes through the input transformation). autofillManager().commit() (form submitted: offers to save) and cancel(); obtained during composition and captured in the event handler. Accessibility services can also write text into fields with ACTION_SET_TEXT. No effect on desktop and the web |
Modifier.sensitiveContent(isContentSensitive = true) | โ
while the node stays in the layout, it declares that it shows sensitive content (a password, a card number) (Compose's): the host tracks its presence after layout (ComposeHost::sensitiveContent()), and on Android 15+ the window is hidden in screen sharing and recordings (View.setContentSensitivity(SENSITIVE)); false withdraws the hint. No effect on desktop and the web |
Modifier.clipScrollableContainer(orientation) | โ clips only along that axis, leaving a 30 dp margin on both sides of the cross axis (Compose's; the way scroll containers clip, so that card shadows stay visible) |
Modifier.keepScreenOn() | โ
as long as the node stays in the layout, the screen does not dim/sleep (Compose's; video, recipes); the host tracks its presence after layout (ComposeHost::keepScreenOn()), and the platform disables SDL's screensaver (FLAG_KEEP_SCREEN_ON on Android). Kompoz apps now let the screen dim by default (SDL's default kept the screen on at all times) |
BottomSheetScaffold, rememberBottomSheetScaffoldState, SheetState (expand/partialExpand/hide) | โ
an always-present bottom sheet: sheetPeekHeight of it is visible (above the navigation bar), and it opens by dragging/flinging as far as its content (at most the screen); a list inside scrolls once the sheet is fully open, and when pulled down the list first scrolls back to the top, then the sheet goes down; sheetDragHandle, sheetSwipeEnabled, topBar, snackbarHost; currentValue/targetValue are observable and saved (rememberSaveable); it does not re-add system bar insets that the surrounding layout has already consumed |
ModalNavigationDrawer (4) (DrawerState, ModalDrawerSheet, NavigationDrawerItem), NavigationRail (3) | โ
opening/closing by dragging, back key; bar โ rail via LocalWindowSize. Back gesture (Android 14+, as in M3): ModalDrawerSheet/DismissibleDrawerSheet widens by at most 12 dp for a gesture from the drawer's own edge, narrows by 24 dp for one from the other edge, and gets 48 dp shorter (from the middle of its outer edge; the content keeps its shape); on completion the drawer closes (a widened one narrows back while closing), and on cancel it returns to its previous state. As in M3, there are also sheet variants that take a DrawerState |
SearchBar (3), SearchBarDefaults.InputField | โ
a pill in the layout that expands into a full-screen search view (back closes it, the keyboard's Search key triggers onSearch) |
BasicAlertDialog, AlertDialogDefaults, AlertDialog(onDismiss, slots, modifier, properties), DialogProperties::usePlatformDefaultWidth | โ
as in Compose 1.4.0. BasicAlertDialog(onDismiss, modifier, content, properties): your own content, 280โ560 dp wide; the minimum width is passed on to the content too (Compose's propagateMinConstraints). AlertDialog: shape (28 dp corners), containerColor (surfaceContainerHigh), iconContentColor (secondary), titleContentColor (onSurface), textContentColor (onSurfaceVariant); in a short window the text leaves room for the buttons (weight(1, false)); the buttons sit at the end, 8 dp apart, and when they don't fit, they stack upward in rows (the confirm button on top, rows 12 dp apart). usePlatformDefaultWidth = false: no 48/24 dp margins from the edges, and it can grow to the window's size (a full-screen dialog). Note: AlertDialog's signature has changed (modifier was added, with properties after it) |
DatePicker, DatePickerDialog, rememberDatePickerState | โ
month pages are swiped or changed with the arrows, and tapping the month name shows a year grid; day, month and year can be picked in any order (when the month changes, the selected date moves to the same day); std::chrono dates, Turkish/English strings (DatePickerStrings, by default LocalStrings's); input mode (DisplayMode::Input, via the pencil next to the headline or rememberDatePickerState(..., DisplayMode::Input)): the date is typed into an outlined field using digits only, the separators appear automatically (the GG.AA.YYYY pattern comes from strings.inputPattern), and once complete and valid it is selected and the calendar opens on that month; an invalid day or an out-of-range year is reported below the field (with Compose's strings, including the Turkish ones); showModeToggle = false hides the button |
TimePicker, TimeInput, TimePickerDialog, rememberTimePickerState | โ
dial (tap/drag; once the hour is picked it moves on to the minutes; an inner ring in 24-hour mode), รร/รS (AM/PM) in 12-hour mode, side by side in a landscape window; TimeInput is two number fields |
DateRangePicker, rememberDateRangePickerState | โ
Baลlangฤฑรง/Bitiล (Start/End) tabs in the header, and the selected one is edited with DatePicker's calendar (instead of Compose's vertical list); the range is shown as a band, and the end cannot be before the start; input mode (the pencil or DisplayMode::Input): start and end are typed into two side-by-side fields (like DatePicker's); a start after the end or an end before the start shows "Geรงersiz tarih aralฤฑฤฤฑ giriลi" (invalid date range input) and is not selected; a momentarily mistyped start does not wipe out an end typed earlier |
DockedSearchBar | โ the field stays in place, and the suggestions open below it at the same width, over the page (240 dp โ 2/3 of the window); tapping outside / back closes it |
SearchBarState, rememberSearchBarState, ExpandedFullScreenSearchBar, ExpandedDockedSearchBar (new search API) | โ
the expanded/collapsed state lives in one object (currentValue/targetValue/progress are observable; animateToExpanded()/animateToCollapsed() can be co_awaited, snapTo), and the query in a TextFieldState: SearchBarDefaults::InputField(state, textFieldState, {.onSearch, .placeholder, icons}), the collapsed bar is SearchBar(state, inputField), and the expanded form is separate: full screen (as in M3, it expands by growing out of the collapsed bar and shrinks back into it: the surface goes from the bar's bounds to the window's, the field at the bar's height moves to 8 dp below the status bar, and the results fade; the back gesture shrinks the view to nine tenths and shifts it toward the side opposite the edge the gesture came from, and up/down with the finger by at most 24 dp, never past the bar; on completion it returns from there into the bar) or in place of the collapsed bar and at its width (tapping outside closes it). Animations run in the state's scope; they are not cut off even if the field leaves the collapsed bar while expanding. BasicTextField(TextFieldState) exists too. AppBarWithSearch(state, inputField, modifier, {.navigationIcon, .actions, .scrollBehavior}): search as a top bar (72 dp, below the status bar, with the pill between the navigation icon and the actions); with SearchBarDefaults::enterAlwaysSearchBarScrollBehavior(), as the content scrolls forward the bar first slides up out of the way (the content takes its place), and when scrolling back it returns before the list moves. TopSearchBar(state, inputField, modifier, scrollBehavior) (M3 1.4): just the search bar as a top bar โ below the status bar, 8 dp in from the edges, at most 720 dp wide and centered, with no background behind it; with a scroll behavior it slides away in the same way |
TooltipBox, PlainTooltip, RichTooltip, rememberTooltipState | โ
on a long press (the click underneath does not fire) or when the mouse hovers; above the anchor, or below it if there is no room; a transient one lasts 1.5 s, a persistent one closes on a tap outside; with caretSize (TooltipDefaults::caretSize), an arrow pointing at the anchor's center (shadowed on a rich tooltip). M3 1.4's positioning: TooltipBox(TooltipDefaults::rememberTooltipPositionProvider(TooltipAnchorPosition::Above/Below/Left/Right/Start/End, spacingBetweenTooltipAndAnchor = 4), โฆ) โ on that side, centered on the anchor along the cross axis, or on the opposite side if there is no room; Start/End follow the text direction; the arrow turns to that side too |
MenuBar, MenuBarMenu, MenuBarItem, KeyShortcut (Compose Multiplatform desktop window menus) | โ
drawn as a strip at the top of the window (not a native menu): clicking a title opens its menu below it, and while one is open, moving the mouse onto another title opens that one; items show their shortcuts on the right (Ctrl+Shift+S), checked items (checked), submenus (opening beside the item on hover; on the left if there is no room on the right), separators, disabled items; clicking outside / Escape closes it, and the cards are fitted to the window. Shortcuts also work while the menu is closed: if the focused node and the handlers around it (like a text field's Ctrl+A) don't take them, they go to the window's handlers (ComposeHost::addKeyHandler); a disabled item's shortcut does not work. In the showcase, Dosya/Gรถrรผnรผm/Git (File/View/Go) menus on desktop/web |
Modifier.onPointerEvent (PointerEventType.Enter/Exit/Move/Press/Release/Scroll), Modifier.onClick (PointerMatcher, keyboardModifiers), TooltipArea (TooltipPlacement.CursorPoint/ComponentRect) (Compose Multiplatform desktop) | โ
onPointerEvent: when the mouse enters/leaves the node (including leaving the window), while it moves over it, when a button is pressed and released (event.button: Primary/Secondary/Tertiary/Back/Forward; SDL's buttons, rclick/mclick in scripts), and when the wheel turns (change.scrollDelta, positive downward; consuming it stops the scrolling); keyboardModifiers are the held Shift/Ctrl/Alt (the platform reports them on every mouse and key event: ComposeHost::setKeyboardModifiers); no event passes (PointerEventPass), innermost first. onClick: with the primary button and finger taps (double click, long press), and with the other buttons when pressed and released on the same node; no ripple, focus or semantics. TooltipArea: delayMillis (500) after the mouse enters the area, the tooltip appears 16 dp below the cursor (above it if it doesn't fit) or relative to a point of the area, and follows the mouse; it closes on a press or when the mouse leaves, and lets input through |
ContextMenuArea, ContextMenuItem, text field right-click menu (Compose Multiplatform desktop) | โ
a right click (ComposeHost::secondaryClick; SDL's right button, rclick x y in scripts) opens the innermost menu under it at the clicked point, flipping left/up if it doesn't fit in the window; rows are as wide as the widest item (112โ280 dp); selecting an item runs it and closes the menu, clicking outside / Escape closes it, and an enabled = false item is faded and not clickable. In a text field, the click focuses the field and moves the cursor there if it is outside the selection; Cut/Copy are disabled without a selection or in a password field, Cut/Paste are disabled when read-only; copying from the menu does not drop the selection (it does not collapse to the cursor as with the touch toolbar) |
VerticalScrollbar, HorizontalScrollbar, rememberScrollbarAdapter, ScrollbarStyle (Compose Multiplatform desktop) | โ
the thumb is as long as the visible fraction (at least 16 dp) and sits where the content is; it is read while drawing (scrolling composes nothing); dragging scrolls the content by the thumb's travel, clicking the track scrolls one page (the visible size) in that direction; the color darkens while hovered and dragged (hoverDurationMillis); no thumb when there is nothing to scroll. rememberScrollbarAdapter(ScrollState) (ScrollState::viewportSize exists too) and (LazyListState) (the content size estimated from the average of the visible items, as in Compose); your own ScrollbarAdapter works too; ScrollbarOptions{.reverseLayout, .style, .interactionSource} |
Shape.createOutline, Outline (Rectangle, Rounded, Generic), RoundRect, CornerRadius, GenericShape | โ
as in Compose: every shape makes its Outline in a size and layout direction (shape.createOutline(size, layoutDirection)); GenericShape([](Path& p, Size size, LayoutDirection d) { โฆ }) is any path, OutlineShape([](Size size, LayoutDirection d) { return Outline::Rounded(RoundRect{โฆ}); }) any outline (a RoundRect's corners can be elliptical: CornerRadius(x, y); Path::addRoundRect(RoundRect)). Backgrounds, borders, shadows, clip and ripples go by the outline: a rect or a rounded rect with circular corners drawn as such, anything else as its path; clip clips without a layer (GraphicsContext::pushClipShape) |
ShapeOutline, OutlineShape, MaterialShapes, RoundedPolygon, Morph | โ
shapes other than rounded rectangles: OutlineShape(outline) (points in the unit square, clockwise, any polygon) works for background, border (inside), shadow, clip, graphicsLayer clipping and ripple, and is mirrored in RTL. Material's 35 shapes such as MaterialShapes::Cookie9Sided() (all() with their names; close to Material's: rounded polygons, stars, petal and pixel shapes), toShape(outline); for your own shapes, roundedPolygon({{x, y, rounding}, โฆ}) and roundedStar(points, innerRatio, rounding/innerRounding). Morph(start, end): angle-by-angle blending between two shapes, in 64 steps (toShape(progress) is used with an animation; remember the morph). Notes: Compose's corner smoothing is missing; the morph uses polar sampling |
DropdownMenuPopup, DropdownMenuGroup, MenuDefaults, checked/selected DropdownMenuItem (M3 Expressive menus) | โ
the popup has no card of its own; each group is a card (surfaceContainerLow, shadowed), with groups MenuDefaults::GroupSpacing (2 dp) apart; groupShape(index, count) is 16 dp at the menu's ends and 4 dp at the joins, itemShape 12 dp at the group's ends and 4 dp in between. DropdownMenuItem(checked, onCheckedChange, โฆ) (checkbox role) and DropdownMenuItem(selected, onClick, โฆ) (radio role): when checked/selected, on tertiaryContainer, 12 dp rounded, with a โ before the text (checkedLeadingIcon); items in a group are 4 dp inset. MenuItemOptions::supportingText on every item (a small description below) |
ExposedDropdownMenuBox, ExposedDropdownMenu, ExposedDropdownMenuDefaults.TrailingIcon | โ tapping the field opens/closes it; the menu sits below the field at the same width (above it if there is no room) and scrolls for long lists; with a read-only field or a field that filters as you type |
PermanentNavigationDrawer/PermanentDrawerSheet, DismissibleNavigationDrawer/DismissibleDrawerSheet | โ permanent: the content sits beside it; dismissible: no scrim, it pushes the content, dragging and the back key |
currentWindowAdaptiveInfo(), WindowSizeClass, NavigationSuiteScaffold (material3-adaptive) | โ
window size classes (width Compact < 600 โค Medium < 840 โค Expanded dp, height 480 / 900; isWidthAtLeastBreakpoint); NavigationSuiteScaffold shows the items in a bottom bar, a rail or (optionally) a permanent drawer depending on the window (NavigationSuiteScaffoldDefaults::calculateFromAdaptiveInfo: as in Compose, a bar in a short window, a rail at medium/expanded width); the content stays in place when the window crosses a breakpoint (its state is kept) and does not re-add the system insets that the bar/rail already avoids; Scaffold also no longer counts insets consumed around it. ListDetailPaneScaffold<T>(navigator, listPane, detailPane(optional<T>)) + rememberListDetailPaneScaffoldNavigator<T>() (navigateTo(role, contentKey), navigateBack, canNavigateBack, currentDestination, detailKey; observable): in a wide window (โฅ 840 dp) the list (360 dp) and the detail side by side, and a single pane when narrower โ the detail slides in from the end, the back key (handleBack) slides back to the list, the exiting detail keeps showing what it showed, and detail-to-detail transitions happen in place; maxHorizontalPartitions, listPaneWidth, spacerSize. SupportingPaneScaffold<T>(navigator, mainPane, supportingPane(optional<T>)) + rememberSupportingPaneScaffoldNavigator<T>() (navigateTo(SupportingPaneScaffoldRole::Supporting, contentKey), supportingKey): in a wide window the main pane plus a supporting pane at the end (supportingPaneWidth, 360 dp; on the left in RTL), and just the main pane when narrower โ the supporting pane slides in from the end, and the back key returns; both scaffolds share the same navigator and single/dual-pane logic. When the panes are side by side, a handle in the gap between them (paneExpansionDragHandle, e.g. VerticalDragHandle(paneExpansionDraggable(state, interactionSource), interactionSource)) drags the first pane's width; rememberPaneExpansionState({PaneExpansionAnchor::Proportion(0.5f), PaneExpansionAnchor::Offset(360), PaneExpansionAnchor::Offset(-100) /* from the end */}) snaps to the nearest anchor on release and to the next one in that direction on a fling (without anchors, it stays where it was released), in the opposite direction in RTL; setFirstPaneWidth/setFirstPaneProportion/clear from code. VerticalDragHandle: 4 ร 48 dp, and 12 ร 52 dp in onSurface while pressed/dragged. Notes: no third (extra) pane and no foldable posture (windowPosture); LocalWindowInfo.current() (Compose's): isWindowFocused (false while another window, a system dialog or the notification shade is on top; from SDL's window focus events) and containerSize (the window's size in dp โ pixels in Compose); LocalViewConfiguration.current() (Compose's): longPressTimeout, doubleTapTimeout, doubleTapMinTime, touchSlop (dp), minimumTouchTargetSize, maximumFlingVelocity (dp/s) โ on Android from the device's android.view.ViewConfiguration; the host's clicks/drags/flings and pointerInput blocks (with the value at the place they were composed: PointerInputScope::viewConfiguration()) respect these, and a second tap arriving within doubleTapMinTime of the first does not count as a double tap. Minimum touch target (Compose's): if there is no direct target where a finger touches (or inside the touched card/list), clickable/pointerInput elements smaller than minimumTouchTargetSize (48 dp) have their area grown evenly to that size, and the nearest of those containing the touch counts as touched; a release inside the enlarged area is also a click; PointerInputScope::extendedTouchPadding() and isOutOfBounds(size, extendedTouchPadding) for touch detectors; the mouse uses only the real bounds |
Animation
| API | Status |
|---|---|
animateFloatAsState (7), animateColorAsState (3), FastOutSlowInEasing (4) | โ |
tween (18), spring (7), infiniteRepeatable (5), repeatable, snap, easings | โ |
Animatable (13), animateDecay, exponentialDecay/rememberSplineBasedDecay, VelocityTracker | โ
co_await anim.animateTo(...); for floats: animateTo(targetValue, spring, initialVelocity) (spring physics carries the velocity: a flung value overshoots, a released one returns with the drag velocity), animateDecay(initialVelocity, DecayAnimationSpec) (the velocity falls off as exp(โfrictionยทt) and stops below the threshold), updateBounds(lowerBound, upperBound) (snapTo and animations stop at the bounds), velocity(), targetValue() (where the decay will stop). exponentialDecay(frictionMultiplier, absVelocityThreshold) (Compose's: 4.2/s), splineBasedDecay() (the scrollables' fling), calculateTargetValue (page snapping), valueAt/velocityAt/durationMillis. VelocityTracker: addPosition(ms, position), calculateVelocity() (least-squares slope over the last 100 ms; ignores anything before a 40 ms pause), resetTracking(). rememberDraggableState(onDelta, [](float velocity) { โฆ }) reports the release velocity; anchoredDraggable carries the fling velocity while settling with a spring (without overshooting the ends). splineBasedDecay() / rememberSplineBasedDecay() is Android's fling curve (OverScroller's spline, Compose's AndroidFlingSpline/FlingCalculator; in dp, so density cancels out): the fling starts at the given velocity, and with 0,015 friction and physical deceleration its distance grows with roughly the 1.74th power of the velocity (2000 dp/s โ 647 dp, 925 ms; 8000 dp/s โ 7186 dp); the flings of all scrollables (verticalScroll, lazy lists, the pager's estimate, snapping lists, scrollable2D โ in the finger's direction) follow this curve; exponentialDecay stays exponential |
AnimatedVisibility (11) + fadeIn/fadeOut (11), AnimatedContent (10), Crossfade (2) | โ
fade/expand/shrink/slide/scale; exiting content receives no touch or focus; AnimatedContent(n, [](const int&) {...}) also works without a type argument; as the transitionSpec, [](const T& initial, const T& target) { return togetherWith(โฆ); } (the direction depends on the states: from below when a counter increases, from above when it decreases); SizeTransform (default: the container grows with a spring from the old content's size to the new one's, clipping with clip; togetherWith(โฆ).withSizeTransform(SizeTransform{.clip = false}), or std::nullopt โ as large as what is shown, like Crossfade). keyframes(durationMillis, {{ms, value, easing}, โฆ}[, delayMillis]): in float animations the values are absolute (a 1 โ 1.3 โ 1 pulse), in others such as color they are fractions of the path from start to target; repeated with repeatable/infiniteRepeatable. AnimatedContentOptions: {.contentAlignment = Alignment::Center} (the contents sit at that alignment in the container โ also while its size animates; top start by default), {.contentKey = [](const Screen& e) { return uint64_t(e.kind); }} (a new state with the same key takes over without a transition, keeping the content's state: Yรผkleniyor โ Yรผklendi (Loading โ Loaded) slides, while new data within Yรผklendi does not; data arriving while a transition runs goes only to the entering content). A child's own enter/exit within the content: scope.animateEnterExit(slideInVertically(1), slideOutVertically(1)) (Compose's Modifier.animateEnterExit; it enters as the content enters and exits its own way as the content exits, and the content stays until all such children have exited; if reversed midway, it turns back from where it is; no enter for content visible from the start); EnterTransition::None/ExitTransition::None (the content appears/disappears instantly, and only the children animate); targetContentZIndex (togetherWith(โฆ).withTargetContentZIndex(-1): the entering content comes in beneath the exiting one โ for going back; at the default 0, the new one is on top) |
rememberInfiniteTransition (4), animateContentSize (2), updateTransition (6), MutableTransitionState, rememberTransition, AnimatedVisibility(visibleState) | โ
MutableTransitionState<S>(initialState): setTargetState(โฆ) starts the transition โ a target different from the initial state animates on first appearance too (an AnimatedVisibility arriving as the screen opens, a row added to a list); currentState() catches up once the values arrive, and isIdle() tells that it has come to rest there (not if it turned back on the way; that is when to remove a row that is fully gone from the list); all of them are observable. AnimatedVisibility(visibleState, modifier, enter, exit, content): shows according to the target, starting from the current state; it updates currentState once the content has fully entered or left. rememberTransition(state) (and updateTransition(state)): animate values from the current state to the target, and update the state when they arrive |
Modifier.animateBounds, LookaheadScope (Compose 1.8) | โ
when layout moves the node or changes its size, it slides there from where it was drawn with a spring (or the given AnimationSpec), with its content laid out at the in-between size; its position is measured relative to the nearest LookaheadScope (a Box), and scrolling that does not animate. There is no lookahead pass: if under an aligned parent its position changes while its size grows, the path reaches the target within the duration but is not perfectly linear; touch goes to where it is placed; without movableContentOf, content moved to another parent is recreated (without animation). Modifier.skipToLookaheadSize(): while the surrounding layout animates its size (animateBounds, a moving shared element), the content is laid out at its destination size while its own size follows the animation (Compose's: text does not re-wrap while growing) |
Modifier.approachLayout, DeferredTargetAnimation | โ
the content is first measured as it will end up (lookahead: ApproachMeasureScope::lookaheadSize); while isMeasurementApproachInProgress(lookaheadSize) is true, approachMeasure(scope, measure, constraints, placeAt) lays it out at the in-between size, and afterwards like the lookahead; state read there triggers a new layout. DeferredTargetAnimation<T>(spec): the first target is taken as is, and it animates toward a new one via updateTarget(target, scope); isIdle, pendingTarget. Note: the lookahead is local (Compose's covers the whole LookaheadScope), and there is no placement approach (isPlacementApproachInProgress) |
Navigation: NavHost, NavController, rememberNavController, composable(route), navigate, popBackStack, currentBackStackEntryAsState | โ
routes with {name} path segments and optional ?name={name} query parameters (arguments().getInt/getString/โฆ); NavOptions{popUpTo, inclusive, saveState, launchSingleTop, restoreState} (bottom navigation tabs keep their own stacks); the back key pops the stack; the back gesture (Android 14+) reveals the screen below with the pop transition, as far as the gesture's progress (seeking along the transition's timeline, as in Compose), pops on completion with the transition continuing from where it was, and on cancel the top screen becomes fully visible again; forward and back transitions (NavHostOptions, 700 ms crossfade by default; as in Compose, the entry navigated to comes in over the previous one, and the one returned to comes in from beneath โ cumulative z-index); each entry's rememberSaveable state is kept while it is on the stack. Nested graphs: navigation(startDestination, route) { โฆ } (the graph's route leads to its start destination; the graph has its own entry and ViewModels on the stack, which are removed when you go back out of its last screen; popUpTo with the graph's route closes the whole flow), getBackStackEntry, entry.parent(), entry.hierarchy() (tab selection). Deep links: composable(route, {navDeepLink("kompoz://item/{n}")}, โฆ), navigate(uri), handleDeepLink (start screen + graphs + destination); the command line on desktop, the # part of the URL on the web (the shown screen is written there), the VIEW intent on Android. Type-safe routes: a struct whose fields are the arguments, plus KOMPOZ_ROUTE(Type, fieldsโฆ) (or KOMPOZ_NAMED_ROUTE, by name); required fields are path segments, std::optionals are query parameters; composable<T>, navigation<T>(startDestination, โฆ), navigate(T{โฆ}), toRoute<T>(), hasRoute<T>(), getBackStackEntry<T>(), popBackStack<T>(), navDeepLink<T>(basePath); fields can be strings, numbers, bool, enums and std::optionals of these (/Zc:preprocessor on MSVC, which CMake adds) |
SharedTransitionLayout (8), sharedElement, sharedBounds, rememberSharedContentState | โ
the content of AnimatedContent/AnimatedVisibility receives an AnimatedVisibilityScope; the entering element moves from its partner's place (or, in an interrupted transition, from where it was being drawn at that moment) to its own with a spring animation, drawn above the content (even if its parent is fading), while its partner is hidden; sharedElement is re-measured at the in-between size, sharedBounds crossfades the two and scales to the width as in Compose (text does not reflow); exiting content waits until the transition ends. sharedBounds(โฆ, boundsTransform, SharedResizeMode::scaleToBounds(ContentScale, Alignment)) (FillWidth + center by default; with Fit, FillBoundsโฆ each content is measured at its own size and fitted to the bounds at that scale) or SharedResizeMode::remeasureToBounds() (both are re-measured at the in-between size every frame, crossfading; Compose's ResizeMode). Missing: boundsTransform is only an AnimationSpec, no skipToLookaheadSize. Overlay: sharedElement/sharedBounds(โฆ, boundsTransform, [resizeMode,] renderInOverlayDuringTransition, zIndexInOverlay) (while moving, drawn in the overlay in z order, or, with false, in its place in the content, below whatever comes after it); Modifier.renderInSharedTransitionScopeOverlay(scope, renderInOverlay, zIndexInOverlay) draws a node (such as a bottom bar) in the overlay while the condition holds (empty: while an element is moving), among the moving ones in z order |
UI, drawing and text
| API | Status |
|---|---|
Modifier (207), Alignment (98), Color (86), dp (147) | โ |
graphicsLayer (14), alpha, shadow (2) | โ
as in Compose's GraphicsLayerScope: scale, translation, rotationX/rotationY (3D, seen from a camera cameraDistance ร 72 px away, as Android draws it) and rotationZ about transformOrigin, alpha, shape + clip, blendMode, colorFilter, compositingStrategy (Auto: offscreen only when faded; Offscreen; ModulateAlpha: each draw faded, no layer), outsets, renderEffect (BlurEffect(radiusX, radiusY, edgeTreatment) with any TileMode, OffsetEffect, each taking another to go through first: what is drawn within the layer's bounds goes through it, then is cut to its shape if clip is set; GraphicsContext::pushEffectLayer), and size to read in the lambda. Offscreen only where it must be (faded, a blend mode, a color filter, a render effect, Offscreen); otherwise the matrix and the clip go straight into the drawing (pushTransform, pushClipShape). The lambda version is read at draw time. shadowElevation with ambientShadowColor and spotShadowColor (Compose's, also on a GraphicsLayer): its shape's shadow (clipped or not) drawn first, turned and moved with it, as Android draws a RenderNode's โ lit from above the window's top middle, 500 dp up, 800 dp across, the ambient shadow at 3.9% and the spot one at 19% (GraphicsContext::light, ShadowLight, set by the platform), faded with the layer (seen through below alpha 1), each point as high as its 3D turn puts it. On the GPU renderer a rounded rect of one radius (a rect, a circle), flat, drawn through a similarity, as Skia's analytic shadow on Android's GPU (an ambient shadow spread by half the elevation in px, up to 150, falling off as exp(โ4 (1 โ t)ยฒ) โ 0.018 over that times 1 + z / 128; a spot one cast from the light onto the plane under it; each left out under an opaque caster); any other outline, a turn in 3D or a perspective as Skia blurs them (the outline stroked and filled, and cast from the light, blurred; under a perspective cast the way round Skia means it to be, where its own check leaves some shapes' mirrored); in an offscreen layer in its px, as Android's. Elsewhere (SDL) those blurs, through its shadows. Modifier.shadow(elevation, shape, clip, ambientColor, spotColor) is Compose's: a graphicsLayer with them, clipped by default when the elevation is above 0 (shadow(elevation, cornerRadius) a shorthand). SDL_Renderer ignores a layer's blend mode and color filter |
Modifier.dropShadow, Modifier.innerShadow (Compose 1.9; Shadow(color, offset, blurRadius, spread)) | โ
a shadow beneath the shape (spread, offset, blurred) or inside it (as if cast from its edge), drawn before what follows; in SDL the blurred mask is built once on the CPU per size, corner and blur with three box blurs (Skia's sigma), and released after a second without use; very large ones (over 4096 px) and the other drawing surfaces fall back to fading rings (no inner shadow there). Rounded-rectangle shapes (circle, pill) and outline shapes (heart, cookie, GenericShape; in SDL the path is rasterized with FreeType and goes through the same blur; elsewhere, the GPU renderer too, as Compose draws them: under the shape its outline filled and stroked twice the spread wide, each blurred, the two over each other; inside it, the color where its hole, worn away by the spread and moved, blurred isn't โ GraphicsContext::drawShadowPath's default, through effect layers). brush (spread over what the shadow covers), alpha and blendMode (Compose 1.9's; SDL draws the blend modes it can, and a brushed shadow as its mask colored by the brush); as Compose's, a drop shadow ends where its bitmap does (its radius and spread past the shape, moved) |
scale, rotate shorthand modifiers | โ
graphicsLayer shorthands |
Modifier.zIndex | โ
drawing and touch order among siblings (equal ones keep composition order); in a custom Layout, Placeable::place(x, y, zIndex), and the two add up |
Modifier.dragAndDropSource, Modifier.dragAndDropTarget, DragAndDropTransferData, DragAndDropEvent | โ
in-app drag and drop: long press and drag with a finger (direct drag with the mouse), a faded copy of the source follows the pointer, and targets that opt in via shouldStartDragAndDrop receive onStarted/onEntered/onMoved/onExited/onDrop/onEnded; text + MIME types + localState (any value); Escape cancels; haptic feedback at the start. Drags coming from other apps go to the same targets (desktop, SDL's drop events): files from a file manager (text/uri-list, with the paths in files) or text from a browser (text/plain); since SDL reports what is being carried only on drop, targets are asked up front against both (ComposeHost::externalDragEntered/Moved/Drop/Left). Notes: no dragging out of the app; no drags from other apps on Android |
Brush (13) gradients | โ
linear/horizontal/vertical/radial/sweep, color stops; background(Brush), drawPath(path, brush) |
onSizeChanged (5), Layout (5) (Measurable/Placeable, layoutId), the layout modifier (5) | โ |
rememberSnapFlingBehavior, SnapPosition, LazyListOptions::flingBehavior | โ
LazyRow(โฆ, {.flingBehavior = rememberSnapFlingBehavior(state, SnapPosition::Center)}): when a drag or fling (or the wheel) ends, the item nearest to where the fling would land (spline decay) stops aligned to the start/center/end; it reaches distant items too. In grids as well: LazyVerticalGrid(โฆ, {.flingBehavior = rememberSnapFlingBehavior(gridState, SnapPosition::Start)}) stops with a row aligned (a column in a horizontal grid). Note: not in the pager (the pager already snaps to pages) |
CenterAlignedTopAppBar, Card(onClick) / ElevatedCard(onClick) / OutlinedCard(onClick), SmallFloatingActionButton / LargeFloatingActionButton, FabPosition | โ
the centered title sits at the center of the whole bar, as wide as fits between the icons on both sides (reserving the wider side's width on both sides); cards as in Compose 1.4 have 12 dp corners and clip their content to those corners (filled: surfaceContainerHighest, elevated: surfaceContainerLow at 1 dp, outlined: surface + a 1 dp outlineVariant border); the ripple of clickable cards follows their corners; 40 / 96 dp FABs; ScaffoldSlots::floatingActionButtonPosition: End (default), Start, Center (above the bottom bar), EndOverlay (overlapping the bottom bar) |
TopAppBarSlots::subtitle (M3 Expressive subtitle) | โ
below the title, small (labelMedium) and onSurfaceVariant; in TopAppBar it is centered together with the title in the 64 dp, in CenterAlignedTopAppBar both are centered; MediumTopAppBar / LargeTopAppBar grow by 24 dp for it (136 / 176 dp), with the subtitle below the large title (labelLarge / titleMedium), and it appears in the top row together with the small title as the bar collapses |
MediumFlexibleTopAppBar, LargeFlexibleTopAppBar, TopAppBarSlots::titleHorizontalAlignment, TwoRowsTopAppBar | โ
M3 Expressive's flexible bars: a larger title (headlineMedium / displaySmall) and subtitle (labelLarge / titleMedium), 112 / 120 dp (136 / 152 with a subtitle), 64 dp when collapsed; titleHorizontalAlignment puts the large title (and subtitle) of a two-row bar at the start, center or end, and when centered the collapsed title is centered too (with end: the collapsed one at the start). TwoRowsTopAppBar(title(bool expanded), slots, modifier, subtitle(bool)): the title and subtitle are called twice โ true for the large row, false for the collapsed top row โ and receive the ambient text style rather than the bar's font, so each row can pick its own size or even a different text; everything else (icons, colors, heights โ 112 by default, 136 with a subtitle, 64 collapsed โ, alignment, scroll behavior) comes from slots |
Modifier.pointerHoverIcon (desktop), PointerIcon | โ
pointerHoverIcon(PointerIcon::Hand / Text / Crosshair / Default or Cursor::Move, ResizeHorizontal, ResizeVertical, NotAllowed, Wait, overrideDescendants): the innermost one under the pointer wins (an icon, a clickable's hand, a text field's I-beam); overrideDescendants puts the outer one above its descendants'; SDL's system cursors (desktop, web). Checkbox/TriStateCheckbox/Switch/RadioButton take an interactionSource, and RadioButton also enabled. Notes: no custom image cursors |
Modifier.selectable, toggleable, triStateToggleable, selectableGroup, matchParentSize, fillParentMaxWidth/Height/Size | โ
selectable(selected, onClick, SemanticsRole::RadioButton) / toggleable(value, onValueChange, role) / triStateToggleable(state, onClick, role): a clickable row with its semantic information (selected/checked, indeterminate); give no callback to the RadioButton/Checkbox/TriStateCheckbox/Switch inside it ({}: Compose's onClick = null) โ then they are not clickable, and the row takes the touch. selectableGroup() marks a radio group. In a Box, matchParentSize(): as large as the size set by the other children, without growing the box (backgrounds, overlays). In a lazy list item, fillParentMaxWidth/Height/Size(fraction): a fraction of the list's visible area (excluding padding); elsewhere it behaves like fillMaxโฆ. selectableGroup is exposed to Android accessibility as a single-selection collection (CollectionInfo: the number of options, one row if they are side by side, otherwise one column; for each option its index and whether it is selected, CollectionItemInfo): TalkBack says "4 รถฤeden 2'si" (2 of 4) |
Modifier.onGloballyPositioned, Modifier.onPlaced, LayoutCoordinates | โ
reported after layout whenever its position or size changes (scrolling included): size(), positionInRoot/InWindow(), positionInParent() (relative to the parent's content; unaffected by scrolling), boundsInRoot/InWindow() (cut by the surrounding scrollers and clips โ clipToBounds, clip(shape) โ; empty if not visible), localToRoot, rootToLocal, localToWindow, windowToLocal; the size at its place in the chain (padding(10).onGloballyPositioned: the inside). onPlaced fires only when its size or its position within its parent changes. The contents of popups are also relative to the window. boundsInParent(), localPositionOf(sourceCoordinates, relativeToSource), localBoundingBoxOf(sourceCoordinates, clipBounds) (another node's point/bounds as seen from here), parentLayoutCoordinates()/parentCoordinates() (the node it is inside; nothing above the root; the two are the same). Notes: a snapshot (not a live object); graphicsLayer translations/scales are not taken into account |
Modifier.onVisibilityChanged, onFirstVisible, LayoutBoundsHolder / Modifier.layoutBounds | โ
as in Compose 1.9: true once the whole node (or more than minFractionVisible of it) has been visible in the window โ or, with {.viewport = holder}, within a list's/box's area โ for minDurationMs; false immediately when it becomes invisible, leaves the layout or is removed; onFirstVisible only the first time (impression counting). Modifier.onLayoutRectChanged(callback, throttleMillis = 0, debounceMillis = 64) (Compose 1.8): RelativeLayoutBounds (boundsInRoot, boundsInWindow, positionInWindow(), width()/height(), fractionVisibleInRect(r), fractionVisibleInWindow()) when its position changes โ once, after it has stayed still for debounceMillis (when scrolling stops); with throttleMillis, also at most that often while it keeps moving; and if both are 0, immediately on every change; a node leaving the layout is not reported |
SubcomposeLayout (subcompose(slotId) { โฆ }) | โ
slots are requested during measurement: a slot requested for the first time (or whose dependency changed) is composed in the same frame and layout runs again (until then it has nothing to measure); a slot can depend on another slot's size or be chosen according to the constraints; a slot that is no longer requested goes away (with its state and effects); pre-measurements (intrinsics) do not change the slots. Notes: the content runs later, during composition (capture by value); it recomposes when dependency changes, not when the slot content's lambda changes |
TextStyle (24), FontWeight (26), TextAlign (16), TextOverflow (9), TextDecoration, FontStyle (4), softWrap, TextDirection, autoSize (TextAutoSize.StepBased), letterSpacing, lineHeight, TextAlign.Justify, TextIndent, brush, shadow (Shadow), drawStyle, textGeometricTransform | โ
textGeometricTransform (TextGeometricTransform(scaleX, skewX); TextStyle and SpanStyle): stretches glyphs horizontally (their advances too, so measurement, cursor and selection follow) and skews them (a point y above the baseline moves yยทskewX to the right; in SDL via a FreeType transform; color emoji stay as they are); drawStyle (TextStyle and SpanStyle; passed down via ProvideTextStyle): Stroke(width, miter, cap, join) strokes the glyph outlines instead of filling them (width in dp, half on each side of the outline, 0 a hairline: a pixel; in SDL with FreeType's stroker, the join and its miter limit taken from join and miter; color emoji stay as they are), and Fill fills again; path effects are not applied. A text shadow (blurred too) follows the outline, the stretch and the skew. brush (TextStyle and SpanStyle): paints the text instead of its color, with a gradient spanning the bounds of the whole text (in SDL each glyph's corners take the brush's color at that point; color emoji keep their own colors; a span's own color overrides the brush); Shadow(color, offset, blurRadius) (dp; pixels in Compose): an offset copy beneath the glyphs, blurred with Skia's sigma for that radius (0.57735 r + 0.5); no shadow on color emoji; FontWeight is a number between 1 and 1000, as in Compose (Thin 100 โฆ Normal 400, Medium 500, SemiBold 600, Bold 700 โฆ Black 900; W100โฆW900, FontWeight{550}): the family's file closest to that weight in CSS order is used (400 tries 500 first, 500 tries 400 first; lighter weights go lighter, heavier ones go heavier), and for 600 and above it is synthetically emboldened as on Android if the file is 200 or lighter; the bundled Noto Sans has 400, 500 and 700 (fonts/Medium.ttf is new; on desktop KOMPOZ_FONT_MEDIUM, Application::mediumFontPath), system fonts' weights come from the OS/2 table; on the web, the fallback text drawn by the browser also uses that weight; italic (a synthetic slant if missing); TextAlign.Start/End follow the paragraph's direction, Left/Right do not; Justify: wrapped lines stretch at their spaces to fill the width (the last line of a paragraph and lines with mixed directions stay at the start; cursor and selection follow the words); TextIndent(firstLine, restLine) (sp): the first line and the other lines of each paragraph start indented from the start (from the right in RTL), and line breaking uses the narrowed width too (not in text fields); autoSize: the largest font size that fits the width, the height and maxLines (by binary search, in steps; scaling the AnnotatedString's own sizes proportionally); letterSpacing (sp, in SpanStyle too; passed down via ProvideTextStyle; in text fields too): added after each letter, with cursor and selection following; as on Android, it is not applied to connected or hanging scripts (Arabic, Devanagariโฆ); lineHeight (sp, baseline to baseline): the added or removed space is distributed by lineHeightStyle (LineHeightStyle, as in Compose) โ alignment Top (all of it at the bottom), Center (half and half), Bottom (all of it at the top) or Proportional (in the ratio of the line's ascent/descent; the font's line gap counts as descent), trim Both/FirstLineTop/LastLineBottom/None (whether the top of the first line and the bottom of the last line keep the font's spacing), mode Fixed (each line exactly that tall) or Minimum (never shorter than the font's; no Tight); the default, like Compose's, is Proportional + Both (single-line text stays the same); in text fields the lines are that tall with the glyphs centered (lineHeightStyle is not taken into account there). fontFeatureSettings (TextStyle and SpanStyle; passed down via ProvideTextStyle): OpenType features in CSS's font-feature-settings syntax ("smcp", "frac", "tnum", "\"liga\" 0"โฆ), passed to HarfBuzz in the SDL backend (only the ones the font has take effect; not in builds without HarfBuzz or in the fallback text drawn by the browser); Font::features is a number (FontFeatures::id/settings); lineBreak (LineBreak, as in Compose; Unspecified = Simple): Simple fills each line in turn, Heading (Strategy::Balanced) finds the narrowest width that does not increase the line count and evens out the lines (the last one included), Paragraph (Strategy::HighQuality) minimizes the sum of the squares of the leftover space at the ends of all lines but the last (over all possible breaks at spaces; the old way if a word is longer than a line); Strictness and WordBreak (Chinese/Japanese/Korean rules) are stored but change nothing, and there is no hyphenation (Hyphens); not in text fields. fontSize, letterSpacing, lineHeight (TextStyle; the first two in SpanStyle) are a TextUnit: a plain number is sp, 1.5_em / TextUnit::em(1.5f) a fraction of a font size (Compose's .em) โ a font size in em is a fraction of the surrounding one (the span's, the text's, LocalTextStyle's), while letter spacing and line height in em are fractions of their own font size; 16_sp can be written too. Note: an em letter spacing in a text style is resolved against the text's size and is not rescaled in a span that changes the size. Material typography (Typography) carries the line heights and letter spacings of the Material 3 type scale (bodyLarge 16/24 sp, 0.5 sp; labelLarge 14/20 sp, 0.1 spโฆ; LineHeightStyle(Center, Trim.None)): a single-line Text is as tall as its line height, as in Compose, with the glyphs centered; as in Compose, increasing fontSize does not grow the line height automatically (also set lineHeight, or use mode = Minimum). The weights are Material 3's: titles (title*) and labels (label*) are Medium, the rest Regular; the labels of the navigation bar and rail are labelMedium whether selected or not (color distinguishes the selected one) |
Modifier.basicMarquee (MarqueeAnimationMode, MarqueeSpacing, MarqueeDefaults) | โ
content that doesn't fit (measured without a width limit) scrolls in place, with a copy following after spacing; iterations (MarqueeDefaults::Forever), repeatDelayMillis, initialDelayMillis, velocity (negative: the other direction), starting on the right in RTL; WhileFocused only while focused (place it before focusable()). Only the drawing scrolls (layout and touch stay in place, as in Compose), and layout is not redone while it scrolls |
rememberTextMeasurer, TextMeasurer.measure, DrawScope.drawText(TextLayoutResult), Text(onTextLayout = โฆ), TextLayoutResult | โ
rememberTextMeasurer() takes the composition's text style and content color as defaults; measure(text or AnnotatedString, TextStyle, maxWidth, maxHeight) (line count, overflow, wrapping, alignment and direction come from the style; except autoSize). Result: size() (wrapped text is as wide as the given width, otherwise the widest line), lineCount, hasVisualOverflow/didOverflowWidth/didOverflowHeight (maxLines or height), getLineStart/End/Top/Bottom/Baseline/Left/Right, isLineEllipsized, first/lastBaseline, getLineForOffset, getLineForVerticalPosition, getOffsetForPosition, getBoundingBox, getCursorRect, getSelectionRects. d.drawText(textLayoutResult, color or brush, topLeft, alpha, shadow, textDecoration, drawStyle, blendMode) as Compose's (the color or brush paints what the text's own style painted, spans and links keeping theirs; the shadow, decoration and draw style go where no span set one), d.drawText(measurer, text or AnnotatedString, topLeft, style, overflow, softWrap, maxLines, size, blendMode) (in a size: its width the lines align in). Text(โฆ, style, onTextLayout) reports whenever the layout comes out different (with the node's width; "read more"). Notes: offsets are UTF-8 bytes; rectangles instead of getPathForRange |
AnnotatedString/SpanStyle (6/3), LinkAnnotation (Url, Clickable), TextLinkStyles, BaselineShift, minLines, TextOverflow.StartEllipsis/MiddleEllipsis, InlineTextContent, appendInlineContent, AnnotatedString.fromHtml | โ
buildAnnotatedString, nested withStyle; ParagraphStyle (withStyle(ParagraphStyle{โฆ}, โฆ), addStyle, pushStyle; textAlign, textDirection, lineHeight, lineHeightStyle, textIndent, lineBreak): its range becomes a paragraph of its own (with line breaks before and after it), its settings replace the text's, in nested ones the inner one wins, and line height trimming applies to each block's own first and last line; Compose's pushStyle/pop()/pop(index) (styles left open close at the end of the text) and string annotations (addStringAnnotation, pushStringAnnotation, withAnnotation; getStringAnnotations(tag, start, end) โ finds the ones around a position, not one that ends at it โ and hasStringAnnotations); clickable withLink(onClick) and withLink(LinkAnnotation): a Url opens on the platform if there is no listener (uriHandler), a Clickable calls its listener with its tag; links are underlined and in the primary color, with TextLinkStyles' style on top, hoveredStyle while hovered, pressedStyle while pressed (hand cursor on hover), and focusedStyle while focused: each link has its own node over its first line (like Compose's TextLinkScope) โ Tab moves from link to link, Shift+Tab back, Enter opens; to accessibility services it appears as a separate clickable node carrying the link text, and their click opens it; tapping the other lines of a link that spans several lines also opens it (those lines are handled by the text itself). SpanStyle.baselineShift (BaselineShift::Superscript/Subscript = half of its ascent up/down) grows the line on that side. minLines: the text is at least that many lines tall (empty lines at the style's line height). StartEllipsis/MiddleEllipsis cut off the start or the middle on a single line (maxLines = 1 or softWrap = false), and clip on multiple lines (as on Android). Inline components: b.appendInlineContent(id, alternateText) (the fallback defaults to U+FFFD) and Text(text, modifier, style, {{id, InlineTextContent{Placeholder{width, height, placeholderVerticalAlign}, [](const std::string& alternateText) {โฆ}}}}): the text reserves Placeholder-sized space for each one (in sp, or in em โ a fraction of the font size at that point; sp values scale with the system font scale), and the component is placed in that space at exactly that size and receives the fallback text (the fallback text is not drawn; for an ID missing from the map, the fallback text is shown); PlaceholderVerticalAlign: AboveBaseline (its bottom on the baseline), TextTop/TextBottom (its top/bottom aligned with the text's), TextCenter (centered on the text); Top/Bottom/Center behave like the Text* ones here; the line grows as much as the space requires (at least as tall as the text itself; as in Compose, lineHeight does not grow in Fixed mode), and the space is never split (not even when wider than the line, at a space in its fallback text, or in a bidirectional line); a component whose space falls beyond maxLines is not placed. AnnotatedString::fromHtml(html, linkStyles, linkInteractionListener) (Compose 1.7; the tags of the compact mode of Android's Html): <b>/<strong>, <i>/<em>, <u>, <s>/<del>, <sup>/<sub>, <big>/<small>, <tt>/<code> (monospace), <h1>โ<h6> (bold, 1.5โ1 em), <font color>, <span style> (color, background-color, font-weight, font-style, text-decoration), <a href> (LinkAnnotation::Url, styled with linkStyles), <br>; <p>, <div>, <ul>/<li> (โข), <blockquote>, <pre> (whitespace preserved) on lines of their own; entities (&, ü, ç, ©โฆ); whitespace collapses to a single space as in HTML; unknown tags are skipped but their text is kept; a tag that closes late also closes the ones inside it |
FontFamily (17), FontVariation | โ
Default/SansSerif/Serif/Monospace (system fonts) + FontFamily::fromFiles; missing weights/italics are synthesized. Variable fonts: a FontFile's variationSettings (FontVariation::weight(650), width, slant, italic, grade, opticalSizing, a font's own axis with {"XXXX", value}; FontVariation::settings(weight, style, ...) derives wght and ital from the weight and style) selects that instance: the same file can appear in a family as several weights (in SDL each instance is a separate FreeType face, via FT_Set_Var_Design_Coordinates; HarfBuzz shapes with it too) |
| Fallback fonts (symbols, emoji, CJK) | โ missing glyphs come from the system fallback chain; color emoji: CBDT (FreeType), COLRv0 (FreeType), Android COLRv1 (the Android text stack) |
| Emoji sequences (ZWJ, flags, skin tones, keycaps, tag sequences) | โ a single glyph (the font's own ligature; on Android, color emoji fonts go through the platform's text stack); the cursor, deletion and line breaking treat the whole grapheme cluster as one |
| Text shaping and right-to-left text | โ
all text goes through HarfBuzz (desktop and Android): kerning, ligatures, Arabic letter joining, Indic scripts (Devanagariโฆ); simplified Unicode bidi (UAX #9; isolates and bracket pairs are supported, embeddings/overrides are not): text inside a bracket pair ((), [], {}, ใใ, ใใโฆ Unicode's paired brackets; with a 63-entry stack as in BD16, and a closing bracket also closes those still open inside it) takes the embedding level's direction if it contains that direction, and the other direction only if it contains only the other direction and the text before the pair is also in that direction (N0; numbers count as right-to-left) โ so in a Turkish sentence, after Arabic, the closing bracket of a bracketed Arabic word does not end up outside; text inside an isolate (LRI U+2066, RLI U+2067, FSI U+2068 โฆ PDI U+2069) is resolved in its own direction (for FSI, by the first strong letter inside) one level up, and counts as a single neutral outside โ so a number after an Arabic name in a Turkish sentence does not attach to the name; an unclosed isolate runs to the end of the line, isolates can be nested, and the inside of an isolate does not determine the paragraph's direction; the paragraph's direction comes from the first strong letter (TextDirection.Content), inline direction runs, digits after Arabic; the cursor, touch, selection and text fields land in the right place in right-to-left text. Differently styled parts (an AnnotatedString's bold word, a linkโฆ) are reordered along with the line: they are split at the line's unidirectional segments and put into display order, each drawn in its segment's direction; the cursor, selection and touch follow them. In a multi-line text field each paragraph takes its direction from its own first letter (a Turkish paragraph from the left, an Arabic one from the right; along with its alignment, cursor, touch and selection); a single-line field is entirely unidirectional |
sp (41), LocalDensity (16) | โ Android system font size (also when it changes at runtime) |
onKeyEvent/onPreviewKeyEvent (3/6) | โ
KeyEvent (KeyDown/KeyUp, Ctrl/Shift/Alt); order: preview from outer to inner โ text field โ onKeyEvent from inner to outer |
semantics (9) and accessibility | โ
Modifier.semantics({contentDescription, role, checked, stateDescription, heading}), clickable nodes merge their content; {.mergeDescendants = true} (a row's icon and texts are read as one unit, while buttons inside stay separate), clearAndSetSemantics({...}) (nothing inside is read โ buttons included โ, and the node says only what it is given: five stars โ "Puan: 4 / 5" (Rating: 4 / 5)), hideFromAccessibility() (both the node and its contents leave the tree); multiple semantics on one node are merged (a toggleable's role/state + a description given later). On Android, AccessibilityNodeProvider (TalkBack, UI Automator), click/focus/scroll actions. On Linux, AT-SPI (Orca, Accerciser) when the desktop's accessibility is on or with KOMPOZ_ATSPI=1: roles, names, states, extents, actions (click, scroll, custom actions), values (a slider's, settable), text (a text field's, settable; a password's as dots), and events for focus, states, names, typed text, values, children and live regions (announcements). On Windows, UI Automation (Narrator, NVDA): control types, names, states, bounding rectangles, the Invoke, Toggle, SelectionItem, Value, RangeValue and Scroll patterns, focus, and events for focus, properties, structure and live regions (no custom actions: UI Automation has none). progressBarRangeInfo (ProgressBarRangeInfo{current, rangeStart, rangeEnd, steps}, ::Indeterminate; provided by Slider and the progress indicators โ wavy ones included) and setProgress (an accessibility service sets the slider's value, which snaps to its steps; on Android RangeInfo and ACTION_SET_PROGRESS: TalkBack announces a percentage and changes it with the volume keys), liveRegion (LiveRegionMode::Polite/Assertive: read out when its text changes, without moving focus), customActions ({"Arลivle", [] { โฆ; return true; }}: in TalkBack's actions menu), traversalIndex (among the nodes of its semantic parent, smaller values are read first) Modifier.progressSemantics(value, start, end, steps) (the value is clamped to the range) and progressSemantics() (indeterminate) |
ui.test (41) | โ
ComposeTestRule: onNodeWithText/Tag/ContentDescription, onAllNodesWithโฆ + assertCountEquals; assertExists/DoesNotExist/IsDisplayed/TextEquals/TextContains/HasClickAction/IsEnabled/IsSelected/IsOnโฆ; performClick/TextInput/Key/ScrollTo; AssertionError on failure |
Build log
- Smart recomposition +
recomposeHighlighterโ LazyColumn/LazyRow+itemsโIcon+IconButtonand 35 Material icons โ- Coroutines,
Animatable,tween/spring, progress indicators โ Scaffold+TopAppBar+NavigationBar+FloatingActionButton+SnackbarโImage+ContentScale,Shape+clip,alphaโAnimatedVisibility/AnimatedContent/CrossfadeโderivedStateOf,mutableStateListOf,rememberUpdatedState,produceStateโCanvas/drawBehind/drawWithContent,aspectRatio,wrapContentSize,requiredSize,graphicsLayer+scaleIn/scaleOut,pointerInput+detectTapGestures/detectDragGestures,combinedClickableโBrushgradients,Path(fill/stroke),drawWithCacheโ- Material components:
ListItem, chips,RadioButton,Slider,SegmentedButton, tabs,Badge, button/card variants,BottomAppBarโ Popup,Dialog/AlertDialog,DropdownMenu,ModalBottomSheet,BackHandler(including the Android back button) โLazyVerticalGrid,HorizontalPager,BoxWithConstraints,onSizeChanged,animateContentSizeโ- Focus and text input:
FocusRequester,FocusManager,onFocusChanged,KeyboardOptions/KeyboardActions, filled/outlinedTextFieldโ ModalNavigationDrawer,NavigationRail,LocalWindowSizeโLayout,Modifier.layout,layoutId,IntrinsicSize,FlowRow/FlowColumn,updateTransitionโAnnotatedString/SpanStyle/links, italics,TextOverflow/softWrap,LocalDensity+ system font scale โFontFamily+ glyph fallback (symbols, color emoji, CJK) โanimateItem,SwipeToDismissBox,draggableโ- Accessibility: semantics tree + Android AccessibilityNodeProvider โ
nestedScroll,VerticalPager,TopAppBarscroll behaviors,MediumTopAppBar/LargeTopAppBarโ- Dynamic color (Material You):
dynamicLightColorScheme/dynamicDarkColorScheme;Switch/Checkboxenabledโ WindowInsetstypes,windowInsetsPadding/imePadding/consumeWindowInsets,SystemBarsAppearanceโ- Keyboard and focus:
focusable,onKeyEvent/onPreviewKeyEvent, directional focus, bring-into-view (also when the keyboard opens) โ - Multiline
TextField/OutlinedTextFieldandrememberTextFieldStateโ - Multi-touch:
detectTransformGestures,transformableโ PullToRefreshBoxโstickyHeaderโSearchBar,BasicTextFieldโDatePicker,DatePickerDialogโTimePicker,TimeInput,TimePickerDialogโDateRangePickerโDockedSearchBarโTooltipBox,PlainTooltip,RichTooltipโExposedDropdownMenuBoxโRangeSliderโSecondaryTabRow,PrimaryScrollableTabRow,SecondaryScrollableTabRowโLazyHorizontalGrid,LazyVerticalStaggeredGridโModifier.scrollable,scale/rotate,mutableIntStateOfand similar โPermanentNavigationDrawer,DismissibleNavigationDrawerโsnapshotFlowโ- Touch text selection (handles, toolbar) โ
sizeIn,defaultMinSizeโ- Test assertions (
assertIsDisplayed,assertTextEquals,onAllNodesWithText,performScrollToโฆ) โ - Cursor handle, finger/mouse distinction, word selection on mouse double-click โ
SelectionContainerโ- Emoji sequences: grapheme clusters, HarfBuzz โ
- Selection across multiple texts, mouse selection,
DisableSelectionโ StateFlow,MutableStateFlow,collectAsStateโ- HarfBuzz shaping for all text, right-to-left text (bidi),
TextDirectionโ SharedTransitionLayout,sharedElement,sharedBoundsโ- Overscroll stretch โ
- Web (Emscripten) and Windows (MinGW) builds โ
- Fallback fonts on the web: the browser draws missing characters โ
- Smoothness on Android: off-screen layers are not drawn, layer textures and paths are not recreated every frame, optimized native code in the debug APK too,
KOMPOZ_PROFILEโ - Zoom with mouse/touchpad, transition from scrolling to pinching โ
- Navigation:
NavHost,NavController, routes and arguments,rememberSaveableโ viewModel()(scoped to the navigation entry or to the app),rememberSaveablein LazyColumn items โ- Nested navigation graphs (
navigation()), graph-scoped ViewModels โ - Redrawing only the changed region (
DisplayList: frames are recorded and diffed node by node; if nothing changed on screen, the frame is not presented),KOMPOZ_DAMAGE_SHOW/KOMPOZ_DAMAGE_CHECK/KOMPOZ_FULL_REDRAWโ - Deep links (
navDeepLink; desktop command line, web#URL, Android intent) โ - Type-safe routes (
KOMPOZ_ROUTE,composable<T>,navigate(T{โฆ}),toRoute<T>()) โ - Cold
Flowand its operators (map,filter,take,debounce,flatMapLatest,combine,stateInโฆ) โ animateItem: a removed item fades out in place (fadeOutSpec,Composer::keepGroup) โAsyncImage: JPEG/WebP and images from the network, using each platform's own decoder; memory cache โrememberSaveableacross process death too (Saver,SavedValue, saved instance state on Android,KOMPOZ_STATE_FILE) โSharedFlow/MutableSharedFlow,shareIn,SharingStarted(WhileSubscribed) โ- Disk cache for images and decoding at the laid-out size (
DiskCache,ImageRequestSize,imageSampleSize) โ - Platform services:
uriHandler,softwareKeyboardController,hapticFeedback,clipboardManagerโ reverseLayout,animateScrollToItem,ScrollState::animateScrollToโModifier.zIndex(andPlaceable::place(x, y, zIndex)) โBottomSheetScaffoldโ- Drag and drop (
dragAndDropSource/dragAndDropTarget) โ - Drag and drop from outside the app: dropping files and text (desktop) โ
- Adaptive layout:
WindowSizeClass,currentWindowAdaptiveInfo,NavigationSuiteScaffoldโ SavedStateHandle: ViewModel state across process death too โCarousel(multi-browse and uncontained) โTriStateCheckbox, textautoSizeโSubcomposeLayoutโ- Lifecycle:
LocalLifecycleOwner, lifecycle effects,collectAsStateWithLifecycle, navigation entry lifecycles โ InteractionSource: press/hover/focus/drag interactions,hoverable,indication = nullโanchoredDraggable,AnchoredDraggableState,DraggableAnchorsโ- Ripple:
ripple(),Indication,LocalIndicationโ LazyListState.layoutInfo(visibleItemsInfo),contentTypeโrememberTextMeasurer,TextLayoutResult,drawText,onTextLayoutโ- Velocity-based animations:
animateDecay, spring with initial velocity,updateBounds,VelocityTracker, release velocity of drags โ keyframes, state-dependenttransitionSpec,SizeTransformโonGloballyPositioned,onPlaced,LayoutCoordinatesโselectable,toggleable,triStateToggleable,selectableGroup,matchParentSize,fillParentMax*โSnackbarResult: awaitableshowSnackbarAsync, dismiss action โrememberModalBottomSheetState: partially expanded sheet,show/hide/expand/partialExpandโpointerHoverIcon,interactionSourceon selection controls โCenterAlignedTopAppBar, clickable cards, small/large FAB,FabPositionโrememberSnapFlingBehavior(lists that snap to items) โModifier.indication(source, indication)โIconToggleButtonfamily,OutlinedIconButtonโSlider/RangeSliderinteractionSourceโTextFieldprefix/suffixโAnimatedContentcontentAlignment,contentKeyโModifier.animateEnterExit,EnterTransition::None/ExitTransition::Noneโsemantics(mergeDescendants),clearAndSetSemantics,hideFromAccessibilityโTextFieldStateselection andedit { }(TextFieldBuffer),InputTransformation(maxLength,allCaps,byValue,then) โOutputTransformation(display formatting, cursor mapping) โTextFieldState.undoState(undo/redo, Ctrl+Z) โModifier.focusProperties(canFocus,next/previous/up/down/left/right,FocusRequester::cancel()) โLocalLayoutDirection(right-to-left layout),absoluteOffset,absolutePaddingโSlider/RangeSlider/Switchmirroring in RTL,Icons::AutoMirroredโ- RTL from the system language (
ComposeHost::setLayoutDirection,isRtlLanguage), RTL alignment of dropdown menus โ LazyGridState(item-index-based position,scrollToItem,layoutInforows/columns) โProvideTextStyle; direction-neutral text (digits) follows the layout direction โMultiChoiceSegmentedButtonRow,SegmentedButton(checked, onCheckedChange),SegmentedButtonOptionsโLazyStaggeredGridState(item-index-based position,scrollToItem,layoutInfolanes) โListDetailPaneScaffold(single/dual pane, navigator, back button) โLinearWavyProgressIndicator/CircularWavyProgressIndicator(wavy progress) โToggleButtonfamily andButtonShapes(morphing corners),RoundedCornerShapePercent,lerp(Shape)โButtonGroup(the pressed button expands, its neighbors shrink), flexible button content padding โ- Shapes with individual corners (
RoundedCornerShape(4 corners),CornerSize,CornerRadii), mirrored in RTL โ - Connected button group shapes (
ButtonGroupDefaults::connectedโฆButtonShapes), per-corner morphing โ SplitButtonLayoutandSplitButtonDefaultsbuttons (split button) โToggleFloatingActionButtonandFloatingActionButtonMenu(FAB menu) โHorizontalFloatingToolbar/VerticalFloatingToolbar(floating toolbar, retracts on scroll) โLoadingIndicator/ContainedLoadingIndicator(spinning, shape-morphing loading indicator) โShortNavigationBar/ShortNavigationBarItem(equal distribution or centered, horizontal items) โWideNavigationRail/WideNavigationRailItem(expandable/collapsible wide rail) โSupportingPaneScaffold(main + supporting pane, navigator) โHorizontalCenteredHeroCarousel(carousel with a large item in the center) โModalWideNavigationRail(wide rail that opens over the content) โ- M3 Expressive FAB sizes:
MediumFloatingActionButton,Small/Medium/LargeExtendedFloatingActionButton(collapses viaexpanded) โ - Subtitle in top app bars (
TopAppBarSlots::subtitle) โ FlexibleBottomAppBarandexitAlwaysScrollBehaviorfor bottom bars โVerticalSliderโCarouselItemScope/carouselItemInfo(reading the item size while drawing) โButtonGroupbuilt from items, overflowing ones go into a โฎ menu โ- Icon in the
Switchthumb (thumbContent), thumb that grows while pressed โ - Tooltip caret (
PlainTooltip/RichTooltipcaretSize): a triangle facing the anchor that keeps pointing at it near the edge too โ SecureTextField/OutlinedSecureTextField(TextObfuscationMode: briefly reveals the last typed character, hidden, visible) โElevatedAssistChip/ElevatedSuggestionChip/ElevatedFilterChip,LeadingIconTabโ- Dragging the pane width:
PaneExpansionState/PaneExpansionAnchor,paneExpansionDraggable,VerticalDragHandleโ - M3 Expressive menus:
DropdownMenuPopup,DropdownMenuGroup,MenuDefaults::groupShape/itemShape, checked/selectedDropdownMenuItem,supportingTextโ SearchBarState(rememberSearchBarState,animateToExpanded/Collapsed,snapTo),InputField(state, TextFieldState),SearchBar(state),ExpandedFullScreenSearchBar,ExpandedDockedSearchBarโAppBarWithSearch(search in the top bar, with navigation icon and actions) andSearchBarDefaults::enterAlwaysSearchBarScrollBehaviorโ- Arbitrary shapes everywhere:
ShapeOutline/OutlineShape(background, border, shadow, clip, graphicsLayer clip, ripple) โ MaterialShapes(35 shapes),roundedPolygon/roundedStar,Morph(shape-to-shape transition) โ- Desktop scrollbars:
VerticalScrollbar/HorizontalScrollbar,rememberScrollbarAdapter(ScrollState,LazyListState),LocalScrollbarStyleโ - Right-click menus:
ContextMenuArea, Cut/Copy/Paste/Select all in text fields (ComposeHost::secondaryClick) โ - Window menu bar and shortcuts:
MenuBar,KeyShortcut,ComposeHost::addKeyHandlerโ Modifier.basicMarquee(scrolling text);DrawScope.translatealso shifts thedrawContent()inside it (layer-free translation:GraphicsContext::pushTranslation) โTextStyle.letterSpacing/SpanStyle.letterSpacing,TextStyle.lineHeightโTextAlign.Justify(justified text),TextStyle.textIndent(TextIndent) โ- Gradient text (
TextStyle.brush,SpanStyle.brush) and text shadow (Shadow, blurred) โ PathEffect(dashed lines and lines with rounded corners),drawPoints,PathMeasureโ- Desktop mouse:
Modifier.onPointerEvent,Modifier.onClick(mouse-button and keyboard-modifier matching),TooltipAreaโ ColorFilter(tintwithBlendMode,colorMatrix,lighting) andColorMatrix; grayscale, sepia, and tinting for images โModifier.dropShadow/Modifier.innerShadow(real blur, spread, offset, color) โLinkAnnotation(Url,Clickable) andTextLinkStyles(hovered/pressed), superscript/subscript (BaselineShift),minLines,TextOverflow.StartEllipsis/MiddleEllipsisโSliderthumb and track slots, ready-made thumbs (circle, M3 bar, shape, icon, value), Material 3 track,SliderState,SliderColorsโRangeSliderthumb and track slots,RangeSliderState(rememberRangeSliderState), Material 3 track for ranges โModifier.animateBoundsandLookaheadScope(an item whose position and size change slides to its new bounds) โ- Value bubble (
Label,SliderDefaults::Label) and 48 dp touch target (minimumInteractiveComponentSize) โ Modifier.pullToRefresh,PullToRefreshDefaults.IndicatorBox, arc indicator with an arrowhead โ- 48 dp touch target for
IconButton,Checkbox,RadioButton,Switch; the hover/focus layer of the unbounded ripple is a circle โ - 48 dp touch target for
Button,ToggleButton,SplitButton, chips, andSmallFloatingActionButtonโ - 48 dp touch target for the
SegmentedButtonrow โ - Top bar colors (
navigationIconContentColor,titleContentColor,actionIconContentColor,subtitleContentColor),expandedHeight/collapsedHeight,windowInsetsโ BasicAlertDialog,AlertDialogcolors/shape/modifierand wrapping buttons,DialogProperties::usePlatformDefaultWidthโChannel(rendezvous/buffered/conflated, overflow policies),produce,consumeEach,receiveAsFlowโPathEffect.stampedPathEffect(Translate/Rotate/Morph) โTextStyle.lineBreak(LineBreak::Simple/Heading(balanced) /Paragraph(fewest unbalanced line breaks)) โ- Links get keyboard focus and accessibility clicks (
TextLinkStyles.focusedStyle) โ fontFeatureSettings(OpenType features passed to HarfBuzz) โTextUnit: font size, letter spacing, and line height inemunits โPointerEventPass(awaitPointerEvent(Initial / Main / Final)) โPath.op(PathOperation: union, intersection, difference, reverse difference, xor),Path::containsโmovableContentOf(MovableContent): content that moves together with its state, nodes, and effects โBlendModein drawing (blendModeondrawRect/drawRoundRect/drawCircle/drawPath),CompositingStrategy.OffscreenโModifier.blur(BlurredEdgeTreatment.Rectangle/Unbounded) โ- Isolates in bidirectional text (LRI, RLI, FSI โฆ PDI) โ
- Shadows of outline-based shapes (
dropShadow/innerShadowfor heart, cookie,GenericShape) โ - Line heights and letter spacings in the Material type scale,
LineHeightStyle(alignment, trim,Minimum), line height in text fields โ - Numeric
FontWeight(100โ900, CSS-style nearest font file selection, Android's synthetic bold), bundled Noto Sans Medium, Material 3 weights โ - Bracket pairs in bidirectional text (UAX #9 BD16 + N0) โ
- Styled bidirectional text is reordered along with the line (runs are split at direction boundaries and drawn in visual order) โ
LazyHorizontalStaggeredGridโPainter(BitmapPainter,VectorPainter,ColorPainter,BrushPainter, custom),Modifier.paint,Image(painter),Icon(painter),painterResourceโModifier.draggable2D,Draggable2DStateโModifier.focusGroup(arrow keys move within the group first; from outside, the group is a single target) โModifier.scrollable2D,Scrollable2DStateโ- Magnifier: Android's text magnifier on text handles,
Modifier.magnifierโ ContentTransform::targetContentZIndex, forward/back z-order in NavHost โLayoutCoordinates:parentLayoutCoordinates,localPositionOf,localBoundingBoxOf,boundsInParentโ- Snap fling in grids (
rememberSnapFlingBehavior(LazyGridState)) โ rememberGraphicsLayer(record,drawLayer,toImageBitmap) โMediumFlexibleTopAppBar,LargeFlexibleTopAppBar,titleHorizontalAlignmentโrememberAsyncImagePainter(as a Painter) โLocalOverscrollFactory(to disable the stretch) โ- Per-paragraph direction in multi-line text fields โ
SharedResizeModeforsharedBounds(scaleToBounds, remeasureToBounds) โ- AnnotatedString.Builder:
pushStyle/pop, string annotations (getStringAnnotations) โ ParagraphStyle(per-paragraph alignment, direction, indent, line height, line breaking) โBasicText,BasicSecureTextFieldโTextFieldColors(TextFieldOptions::colors),TextSelectionColors/LocalTextSelectionColorsโTextField(enabled = false), disabledTextFieldColorsโTextFieldshape(the filled field's default rounds only the top corners) โKeyboardOptions::showKeyboardOnFocusโ- Alignment lines:
alignByBaseline,alignBy,paddingFromBaseline,placeable[FirstBaseline]โ Snackbarcomposables (actionOnNewLine, colors), custom snackbars inSnackbarHost,SnackbarData::performAction/dismissโTextFieldLabelPosition(Attached(alwaysMinimize),Above(alignment)) โBringIntoViewRequesterโwindowInsetsTopHeight/BottomHeight/StartWidth/EndWidthโModifier.focusRestorer,focusTargetโonVisibilityChanged,onFirstVisible,layoutBoundsโDatePickerinput mode (DisplayMode,DateInput) โDateRangePickerinput mode (DateRangeInput) โAppBarRow(overflow menu) โ- Interactive
ListItem(clickable/selectable/checkable, shape morphs),SegmentedListItemโ AppBarColumnโ- Real
width(IntrinsicSize::Min)(widest word) โ InlineTextContent,appendInlineContent(inline composables within text) โ- Pager:
PageSize,snapPosition,PagerSnapDistance,settledPage/targetPage,getOffsetDistanceInPages, cross-axis alignment โ MutableTransitionState,rememberTransition,AnimatedVisibility(visibleState)โTextFieldLineLimits,BasicTextFielddecorator(decorationBox) โcompositionLocalOf/staticCompositionLocalOf(lazy default),providesDefault,CompositionLocalContextโMotionScheme(standard/expressive),MaterialExpressiveThemeโ- Pager
reverseLayoutand pagekeyโ TwoRowsTopAppBarโmutableStateMapOfโ- Android's fling curve (spline decay) โ
- Observable
ScrollState.value,canScrollForward/Backward,isScrollInProgress(scroll and lazy states) โ userScrollEnabled,verticalScroll(enabled, reverseScrolling)โArrangementin lazy lists (verticalArrangement/horizontalArrangement) โPath.arcTo/addArc/addPath/translate,DrawScope.drawOval/clipPath/withTransformโBorderStroke(brush border), colored and clippingshadowโClipboard/ClipEntry(LocalClipboard) โAnnotatedString.fromHtmlโdetectDragGesturesAfterLongPressโTaskOf<T>(value-returning coroutines),withBackground(background thread) โawaitEachGesture,awaitFirstDown,waitForUpOrCancellationโLocalWindowInfo(isWindowFocused,containerSize) โSwipeToDismissBoxdismisses on fling, pages settle according to release velocity โPredictiveBackHandler,BackEventCompat; Android 13+OnBackInvokedCallback(14+ gesture progress, the system's back-to-home animation) โ- Popups have their own back scope (
BackHandlers inside them take precedence),ModalBottomSheetback gesture (M3: shrinks; on completion it hides or settles to half-expanded) โ - Back gesture for drawers (M3:
ModalDrawerSheet/DismissibleDrawerSheetexpands or shrinks, lowers, and closes on completion) โ NavHostback gesture (the screen underneath is revealed by the pop transition in proportion to the gesture;AnimatedContentOptions::seekState/seekFraction, Compose'sSeekableTransitionState.seekTo) โExpandedFullScreenSearchBaropens by growing out of the bar (M3) and tracks the back gesture โLocalViewConfiguration(Android's own thresholds;PointerInputScope::viewConfiguration()) โ- Semantics:
progressBarRangeInfo/setProgress,liveRegion,customActions,traversalIndex(with the Android bridge) โ Modifier.onLayoutRectChanged(RelativeLayoutBounds, throttle/debounce) โTooltipAnchorPosition,TooltipPositionProvider(rememberTooltipPositionProvider), side carets โModifier.systemGestureExclusion(areas excluded from Android's edge gestures) โ- Minimum touch target: small targets receive touches landing near them (
extendedTouchPadding) โ TileModefor gradients (Clamp, Repeated, Mirror, Decal) โTopSearchBar,rememberBottomAppBarState/exitAlwaysScrollBehavior(state)(the last missing pieces of M3 1.4) โModifier.keepScreenOn(and the screen is allowed to dim by default) โModifier.onFocusEvent,Modifier.progressSemanticsโ- Scrolling containers clip only along the scroll axis, leaving 30 dp of slack on the cross axis (Compose's
clipScrollableContainer: card shadows stay visible) โ Modifier.sensitiveContent(hidden during screen sharing on Android 15+), publicModifier.clipScrollableContainerโOverscrollEffect,rememberOverscrollEffect,Modifier.overscroll, theoverscrollEffectparameters of scroll containers (stretch is now an effect) โ- Autofill:
ContentType,autofillManager()(commit/cancel), AndroidAutofillManagerbridge,SemanticsAction::SetText(andACTION_SET_TEXT) โ - IME insets follow the keyboard as it slides (Android 11+
WindowInsetsAnimation),imeAnimationSource/imeAnimationTarget,isImeVisibleโ CustomSaver,listSaver,mapSaver,rememberSaveable(saver, init)(and theMutableStateform, as withstateSaver) โ- Material state is restored after process death: top app bar, drawer, floating toolbar, modal sheet, search bar, swipe-to-dismiss, time/date pickers, wide rail, pane width โ
BringIntoViewSpec,LocalBringIntoViewSpec(where to place the focused item when bringing it into view) โ- Custom indications:
IndicationNodeFactory,IndicationNodeโ TextToolbar,LocalTextToolbar(replacing the selection toolbar with your own) โ- Shared transition overlay:
renderInSharedTransitionScopeOverlay,zIndexInOverlay,renderInOverlayDuringTransitionโ Modifier.skipToLookaheadSize()(laying out content at its target size while the size animates) โ- Multicolor vectors:
ImageVector::Builder,VectorPath; Compose's vector model (a tree ofVectorGroups,PathNodes,PathParser,PathBuilder, names,tintColor/tintBlendMode), drawn through a bitmap as Compose'sVectorPainter(Alpha8 for one color) โ - Arcs in path data (
A/a) โ vectorResource: Android vector drawable XML โ- Vector drawable gradients;
Brush::radialGradient/sweepGradientwith color stops โ - ImageVector groups (
addGroup/clearGroup/group,clipPathData),<clip-path>โ trimPathStart/End/Offseton vector paths;rememberVectorPainterwith composable content (vector::Group,vector::Path,vector::RenderVectorGroup,VectorConfig/VectorProperty) โAnimatedImageVector,<animated-vector>,animatedVectorResource,rememberAnimatedVectorPainterโ- Outlined text:
TextStyle.drawStyle,SpanStyle.drawStyle(Stroke) โ TextGeometricTransform(scaleX,skewX) โ- Text shadows (blurred ones too) follow the outline, scaling and skew โ
FontVariation: variable font axes (FontFile::variationSettings) โModifier.scrollableAreaโrequiredWidthIn,requiredHeightIn,requiredSizeInโWindowInsets.captionBar,mandatorySystemGestures,tappableElement,waterfall,safeGesturesand their padding modifiers โModifier.onConsumedWindowInsetsChangedโModifier.imeNestedScrollโModifier.approachLayout,DeferredTargetAnimationโModifier.preferredFrameRate(Hz orFrameRateCategory) โmovableContentOfwith parameters, andmovableContentWithReceiverOfโselectableGroupin Android accessibility (CollectionInfo) โ- Inside a scrollable, the press indication appears after the tap timeout โ
BrushPaintercolor filter (ColorFilter::apply(Brush)) โ- Carousel
maskClip/maskBorderโ - The wavy indicator's amplitude uses Compose's easing curves (standard while increasing, emphasized accelerate while decreasing) โ
Circular/LinearProgressIndicatorwith Compose 1.4's drawing and motion โSwitchwith Compose 1.4's colors (unchecked track surfaceContainerHighest, disabled faded over the surface) and fast spatial spring; jumps to 28 dp instantly when pressed โCheckbox/TriStateCheckboxwith Compose 1.4's drawing and motion (20 dp box, the check path is drawn in and morphs into a line, disabled onSurface 38%) โDrawScopedrawRect/drawRoundRect/drawCirclewithStrokecenter the stroke on the shape's edge, as in Compose (previously drawn inset) โRadioButtonwith Compose 1.4's colors and motion (disabled onSurface 38% instantly, dot with the fast spatial spring, color with the default effects spring) โ- Cards with Compose 1.4's tokens (12 dp corners, content clipped, filled card surfaceContainerHighest, elevated card 1 dp) โ
- Buttons with Compose 1.4's dimensions and elevations (8 dp vertical padding, at least 58 dp,
contentPadding,ButtonDefaults, outlineVariant border, elevation on hover) โ - Chips with Compose 1.4's colors and dimensions (suggestion/input label onSurfaceVariant, filter leading icon primary, disabled onSurface 12%, input chip 4 dp at the edge +
ChipOptions::avatar); on selectable chips the icon expands and fades in, and leaves as its space shrinks; filter/input chips are announced as checkboxes โ - FABs rise 8 dp on hover as in Compose 1.4 (6 while pressed);
ExtendedFloatingActionButtonhas 16 dp before the icon, 12 after it, 20 dp after the label, at least 80 dp โ NavigationBarItemas in Compose 1.4: the indicator, 12 dp down, expands from the center and fades in (fast spatial + effects spring), icon 16 dp, label 48 dp down, selected label secondary, colors transition smoothly, the ripple stays within the indicator and starts where the finger touched; announced as a tab โNavigationRail/NavigationRailItemas in Compose 1.4: at least 80 dp, 4 dp between items and at the ends, 8 dp below the header; 80 ร 56 items, indicator at the top expanding from the center, label 36 dp down and secondary when selected, the ripple in the indicator starts at the finger โ- The
ShortNavigationBarItemripple starts in the indicator where the finger touched โ NavigationDrawerItemas in Compose 1.4: at least 56 dp (grows with a long label), selected background appears instantly, label in the surrounding text style, announced as a tab โTab/LeadingIconTabas in Compose 1.4: colors come from the row's content color (primary for the primary row, onSurface for the secondary) and an unselected tab defaults to the same color (set apart withTabOptions::unselectedContentColor), color transitions use effects springs, ripple in the selected color; a tab with an icon is 72 dp, text baseline 17 dp from the bottom, icon 20 dp above it; the indicator slides with the default spatial spring; announced as a tab โListItemwith Compose 1.4's layout: 16 dp on both sides, supporting text that wraps onto multiple lines also counts as three-line (12 dp, leading/trailing content at the top), the item is a single accessibility node โBadgedBoxas in Compose 1.4: a badge with a count sits 12 dp in from the end, its bottom edge 14 dp below the top โ- Icon buttons with Compose 1.4's colors: tonal secondaryContainer, disabled container onSurface 10%, outlined border in the surrounding content color (38% when disabled); for toggles, filled unchecked surfaceContainer/onSurfaceVariant, tonal unchecked secondaryContainer and checked secondary, disabled outlined has no container even when checked โ
SegmentedButtonas in Compose 1.4: background changes instantly, the check mark fades in while growing from the bottom-left corner (disappears instantly when deselected), the label slides over by half the icon area to make room โ- Bottom sheets with Compose 1.4's motion: default spatial spring when opening and after a drag, fast effects spring when closing, scrim with an effects spring; drag handle onSurfaceVariant, not faded (
AnchoredDraggableState::animateTo(value, spec)added) โ - Tooltips grow from 80% and fade in as in Compose 1.4, and fade out the same way; rich tooltip with a 3 dp shadow, title baseline 28 dp, text 24 dp below it, action at least 36 dp; plain tooltip at least 40 dp โ
BottomAppBarwith Compose 1.4's padding: 4 dp at the start, end and top (actions centered 2 dp lower), FAB 12 dp from the top, 16 dp from the end โ- Menu items use labelLarge for text and bodyMedium for supporting text, as in Compose โ
- Scripted screenshots are taken from the rendered frame (the offscreen OpenGL window was getting cropped when it grew) โ
SecondaryTabRowindicator is 3 dp, as in Compose 1.4 โColorSchemewith all of Compose 1.4's colors (errorContainer, surfaceVariant, surfaceBright/Dim, surfaceContainerLowest, scrim, surfaceTint, primary/secondary/tertiary fixed colors; in dynamic color too),contentColorFor; scrims use the scheme's scrim color โCutCornerShape(straight-cut corners; a single size, a percentage, or each corner separately),AbsoluteCutCornerShape,AbsoluteRoundedCornerShape(not mirrored in right-to-left layouts) โBasicTooltipBox,BasicTooltipState,rememberBasicTooltipState(foundation: positioned where told, no animation,focusable,enableUserInput; persistent by default); only one tooltip is visible at a time (opening one closes the other, like Compose's shared MutatorMutex) โmediaQuery,derivedMediaQuery,UiMediaScope(Compose 1.12, experimental): window size, most precise pointer (mouse/stylus, touch, remote/controller), keyboard (physical, on-screen, none), microphone, camera, viewing distance (TV far, car/dock medium), posture; on Android via InputManager/PackageManager and the dock event, on desktop via SDL (microphone/camera are probed after the first frame). Note: foldable posture is always Flat, since it would require androidx.window โFlexBox,FlexBoxConfig,Modifier.flex(FlexConfig)(Compose 1.12, experimental; CSS flexbox): direction (Row/Column and their reverses), wrapping (Wrap/WrapReverse), justifyContent, alignItems (including Baseline), alignContent, row/column gaps; per child grow, shrink (relative to the base size, freezing at its minimum), basis (natural/dp/percentage), alignSelf, order; config based on constraints. Note: with Compose's bug of ignoring gaps in reverse wrapping fixed โGrid,GridConfig,GridTrackSize,GridArea,GridFlow,Modifier.gridItem(Compose 1.12, experimental; CSS grid): column/row tracks (Fixed dp, percentage of the space left after gaps, Flex fr starting from min-content, MinMax, MinContent, MaxContent, Auto from min-content to max-content), row/column gaps, Row/Column flow; per child 1-based row/column (negative: counted from the end), span, range ({1, 2}), named area, in-cell alignment; auto-placement into empty cells (with a cursor), implicit Auto tracks as needed, widening growable tracks for spanning items; config based on constraints โ- Incremental layout (like Compose): a node remeasured with the constraints it was last measured with, and with nothing changed in or below it, keeps its size and its children's positions; what gets remeasured: composed nodes, nodes whose state read during measurement changed, the next frame of a layout animation, window/density/insets changes and recomposition from the root (everything).
KOMPOZ_LAYOUT_CHECK(always on in tests) compares every layout against one measured from scratch;KOMPOZ_FULL_LAYOUTrestores the old behavior. Scrolling the "Bileลenler" (Components) screen on the Redmi: 42 โ ~58 fps โ - Post-layout tree traversals got shorter: the absolute position of a subtree that didn't move and wasn't remeasured is not recomputed; position reporting only checks clipping on nodes that have none of the elements it cares about (until their modifier changes); scrolling (ScrollState, lazy lists, nested scroll, bringIntoView, accessibility) only remeasures the scrolled node; ripple only requests a redraw. The modal drawer's offset reads its measured width as state. Per-frame work while scrolling on the Redmi: "Liste" (List) ~10 โ ~3 ms, "Bileลenler" ~13 โ ~7.5 ms โ
- A layout pass that remeasures and moves no node skips the tree walk: popups are repositioned from the previous list, and position reporting (
reportPositions) is skipped โ - Indeterminate progress indicators (Circular/Linear, Wavy, LoadingIndicator, ContainedLoadingIndicator) and the wavy wave-shift animation read their values in the draw phase, as in Compose: while spinning they are not recomposed or measured, only redrawn; when not drawn (offscreen) they request no frames.
animateBounds's position animation also only requests a redraw โ - Compose's hardware keyboard mapping in text fields (outside macOS): Ctrl+โ/โ moves word by word (selecting with Shift), Ctrl+โ/โ to the start/end of the paragraph, Ctrl+Backspace/Delete deletes a word (or the selection if there is one), Alt+Delete deletes to the end of the line, Ctrl+H deletes backward; fields whose value is held by the app (
String) also undo and redo with Ctrl+Z / Ctrl+Shift+Z / Ctrl+Y (with their own history; the selection is restored too, and an undo rejected by the app doesn't break the cursor) โ - Mouse selection in text fields (like Compose): pressing focuses the field and places the cursor, dragging selects; double-click selects a word, triple-click a paragraph, and dragging extends by that unit (backward too); Shift+click extends the selection from where it started; clicks are counted press-to-press within
doubleTapTimeout; clicking a focused field brings back a dismissed keyboard โ - Mouse in
SelectionContainerlike Compose: double-click selects a word, triple-click a paragraph, and dragging extends by that unit (across texts, backward too); Shift+click extends the selection (or from the last clicked position); clicks are counted press-to-press withindoubleTapTimeout.pointerInputevents now carry the pressed modifier keys (keyboardModifiers), whether they came from a mouse, and the pressed button โ - Mouse cursor: I-beam only in text fields and in selectable text inside a
SelectionContainer(Compose'stextPointerIcon); a plainfocusable()box and empty areas of a container keep the default cursor; a hand cursor on every line of a link that wraps onto the next line โ NavigationSuiteScaffoldas in Compose 1.4: new types (ShortNavigationBarCompact/Medium,WideNavigationRailCollapsed/Expanded) andnavigationSuiteType(), which picks the default; a new overload with items the app composes itself (NavigationSuiteItem, primary action content: above the end in the bar, at the top in the rail/drawer; vertical arrangement of items);NavigationSuiteScaffoldState(Visible/Hidden, hide/show/toggle/snapTo: the bar slides down, the rail slides to the start, and the content takes its place);NavigationSuite,NavigationSuiteScaffoldLayout;WindowAdaptiveInfo.windowPosture(bar in tabletop posture); aNavigationRailItemoverload with composable icon/label, item arrangement inWideNavigationRail. The showcase uses the new default โInfiniteTransition::animateFloatState/animateColorStateโInfiniteState<T>(Compose's State-returninganimateFloat): if the value is read in composition, the reader recomposes every frame; if read during drawing (graphicsLayer {},drawBehind, Canvas), it is only redrawn; if read during layout (offset {}), it is only re-laid out โ- Keyboard focus is shown only by the 10% state layer, as in Compose Material 3; the 2 dp ring Kompoz had added was removed (Compose's focus indicator tokens aren't used by any component) โ
- The mouse wheel scrolls smoothly like Compose: 64 dp per notch; the first 6 dp immediately, the rest at 1 dp per ms (at most 100 ms); notches arriving during the animation are added to the target; high-precision wheels/touchpads (fractional deltas) scroll instantly; a press stops it; the pager snaps to the nearest page when it ends โ
MaterialThemegives scrollbars the scheme'sonSurfacecolor (12% / 50% on hover), like Compose Multiplatform's Material theme: visible in the dark theme too โShortNavigationBarwas adding the bottom inset twice (64 + 2 ร inset); now it is 64 dp + inset like Compose, with 64 dp items โScaffold,BottomSheetScaffoldandNavigationSuiteScaffoldLayoutmeasure the body, and carousel item content (inside aBox), with no minimum constraint, like Compose: content that doesn't fill stays at its own size (previously it was forced to fill the area); full-screenPopupcontent still gets the window size (like Compose'sDialogwithout the platform default width) โ- Preview (Compose's
@Preview):KOMPOZ_PREVIEW(name, .width, .height, .darkTheme, .background) { ... }registers a component in the app's source; when the app is launched withKOMPOZ_PREVIEW=name, its window shows only that component at that size, and with*all of them stacked (with name and size);scripts/dev.shrebuilds and restarts on every save (screen, scroll andrememberSaveablestate are preserved viaKOMPOZ_STATE_FILE) โ Hct(the Material Color Utilities color space: CAM16 hue and chroma, L* tone) andTonalPalette::fromSeed(color): Android 13+'s "tonal spot" palettes from a single color; on Linux the desktop accent color (the XDG portal'sorg.freedesktop.appearance/accent-color) is read and tracked as it changes, anddynamicLightColorScheme()/dynamicDarkColorScheme()work with it (SDL already tracks dark/light mode from the portal); the showcase follows the system theme live until the button is pressed โ- The library's own texts (Compose's string resources):
Strings(include/kompoz/strings.hpp) holds what components show or tell accessibility services (the text field's context menu and toolbar, "Selected"/"Checked" descriptions, "More options", the exposed dropdown's and split button's arrows, the snackbar's dismiss button, the bottom sheet's drag handle, pull to refresh's label, the date and time pickers' texts) andLocalStringsprovides them.Strings::english()is the default andStrings::turkish()the other; the host providesStrings::forLanguage(...)of the system's preferred language at the root (ComposeHost::setStrings; the SDL backend reads it on desktop, Android and the web,KOMPOZ_LANGUAGE=tr|endecides instead). An app provides its own (CompositionLocalProvider({LocalStrings.provides(mine)}, โฆ));DatePickerOptions::stringsandTimePickerOptions' labels, when set, still win. Until now these texts were in Turkish (the date pickers' in English) โ - Colors from a picture and from the wallpaper itself:
quantizeColors(the Celebi quantizer of Android and Material Color Utilities: Wu's quantizer refined by k-means in L*a*b*),seedColors(as Android's System UI ranks a wallpaper's seeds: monet'sColorScheme.getSeedColors) andTonalPalette::fromImage, which does to a picture what Android does to the wallpaper (WallpaperColors.fromBitmap's unfiltered scaling to 112 ร 112 of area, the quantizer, the seed);wallpaperLightColorScheme()/wallpaperDarkColorScheme()/isWallpaperColorAvailable()(LocalWallpaperTonalPalette) from the wallpaper itself, found and followed as it changes on Linux (KDE Plasma; GNOME, Cinnamon, MATE; Xfce), on Windows, and on Android 8.1+ (the system's WallpaperColors: its first main color that isn't grey, as System UI does when it can't see their populations). A source of its own beside the system's palette (dynamicLightColorScheme()) and the app's scheme: the library doesn't choose between them; the app, or its user, does (the showcase asks). The script'ssleep mswaits in real time for work on other threads โ Offsetcompares with==, so a state of them (a drawing's points) takesset(); Windows' accent color (Settings > Personalization > Colors, DWM's registry value, followed as it changes) is the system's palette there (dynamicLightColorScheme()), as the XDG portal's is on Linux โColoras Compose's 64 bits, with its color spaces (ColorSpace,ColorSpaces,Rgb, connectors, adaptation),lerpin Oklab and colors animated as Compose'sColor.VectorConverterโPathEffectandPathMeasureas Skia's: dashes, corners, stamps and chains as Skia makes them, applied where a path is drawn at its pixels; lengths, positions and pieces of curves asSkContourMeasuremeasures and cuts them โ
Next
- iOS (needs an Xcode project or CMake setup); macOS is built, tested and run by CI but not yet used by hand.
- macOS's accessibility API (NSAccessibility); TalkBack works on Android, AT-SPI on Linux, UI Automation on Windows.
- Item reuse in lazy lists via
contentType(Compose's SubcomposeLayout pool): measured, not needed for now (0.06โ0.13 ms per frame in a 10,000-row list). - No equivalent of Kotlin's
bydelegation: state is read and written with.get()/.set().