| RadialMenu | Kotlin | Compose Multiplatform | AGP | Min SDK |
|---|---|---|---|---|
| 1.0.0 | 2.1.20 | 1.7.3 | 8.x | 21 |
Note: RadialMenu follows Semantic Versioning. Minor versions add features without breaking the API. Major versions may contain breaking changes documented in CHANGELOG.md.
Long press anywhere → drag to select → release to confirm
A lightweight Compose Multiplatform radial menu (also known as a circular menu or pie menu) targeting Android and Desktop JVM. It operates as a long press menu overlay equipped with drag to select interaction. The menu is built entirely with continuous physics-based selection and is highly inspired by the famous Pinterest context menu.
It features cross-platform haptic feedback, smooth animated menu transitions, and an edge-aware placement geometry ensuring the gesture menu is never clipped off-screen or covered by the user's thumb.
| Platform | Status |
|---|---|
| Android (Compose + View) | ✅ Supported |
| Desktop JVM | ✅ Supported |
- Compose Multiplatform (supports Android and Desktop).
- Fully generic items, define your own actions with any number of items (recommended 2 to 8 items).
- Long press to activate, drag to select gesture based item choosing.
- Smart edge aware angle calculation (ensures menu is never obscured by the finger nor clipped by screen edges).
- Haptic feedback on activation, hover over action, and selection.
- Toggle icon states (for example tracking liked or saved variables and updating icons).
- Item badges, show counts or custom text on each menu item.
- Customizable animations, choose from presets or create your own configuration.
- Dynamic icon scaling on hover for clear visibility.
- Drag direction indicator to help guide the finger.
- Accessibility, content descriptions, screen reader support, and announcements.
- Zero external dependencies (relies purely on
std-liband Compose or Canvas). - ProGuard and R8 safe, includes consumer rules and
@Keepannotations.
Full documentation: gawwr4v.github.io/RadialMenu
// Android + Desktop (Compose Multiplatform)
implementation("io.github.gawwr4v:radialmenu:1.0.4")Recommendation: Use
1.0.4instead of1.0.3for Desktop or full Kotlin Multiplatform consumption.1.0.4is the first complete Maven Central release with Android, Desktop, and KMP metadata all published correctly.
import io.github.gawwr4v.radialmenu.*
val items = listOf(
RadialMenuItem(id = 1, icon = painterResource(R.drawable.ic_share), label = "Share"),
RadialMenuItem(id = 2, icon = painterResource(R.drawable.ic_heart), label = "Like"),
RadialMenuItem(id = 3, icon = painterResource(R.drawable.ic_bookmark), label = "Save")
)
// Wrap your layout to intercept gestures
Box {
RadialMenuWrapper(
items = items,
onItemSelected = { item ->
println("Selected: ${item.label}")
}
) {
Text("Long press me to open radial menu!")
}
// Place overlay at root level for fullscreen rendering
RadialMenuOverlay(items = items)
}<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Your Main Content Here -->
<!-- Radial Menu Overlay -->
<io.github.gawwr4v.radialmenu.RadialMenuView
android:id="@+id/radialMenu"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:rm_menuRadius="90dp"
app:rm_iconSize="32dp"
app:rm_accentColor="@color/white" />
</FrameLayout>import io.github.gawwr4v.radialmenu.*
val radialMenu = findViewById<RadialMenuView>(R.id.radialMenu)
val items = listOf(
RadialMenuItem(id = 1, icon = shareDrawable.toPainter(), label = "Share"),
RadialMenuItem(id = 2, icon = heartDrawable.toPainter(), label = "Like"),
RadialMenuItem(id = 3, icon = bookmarkDrawable.toPainter(), label = "Save")
)
radialMenu.setItems(items)
radialMenu.onItemSelected = { item ->
when (item.id) {
1 -> { /* trigger share */ }
2 -> { radialMenu.setItemActive(2, !item.isActive) }
3 -> { radialMenu.setItemActive(3, !item.isActive) }
}
}RadialMenuView is fully usable from Java via xml or programmatic initialization:
// Java usage example
RadialMenuView menu = findViewById(R.id.radialMenu);
// Create generic Action list
List<RadialMenuItem> items = new ArrayList<>();
items.add(new RadialMenuItem(1, new BitmapPainter(...), ...));
menu.setItems(items);
// Use Kotlin Function1 equivalent using Java lambdas
menu.setOnItemSelected(item -> {
Toast.makeText(context, "Selected: " + item.getLabel(), Toast.LENGTH_SHORT).show();
return kotlin.Unit.INSTANCE;
});
// Menu automatically opens on long press. Listen for taps:
menu.setOnTap(() -> {
Toast.makeText(context, "Tapped!", Toast.LENGTH_SHORT).show();
return kotlin.Unit.INSTANCE;
});The Compose API (
RadialMenuWrapper) is Kotlin-only. For Java projects, useRadialMenuViewin your XML layouts.
The menu dynamically supports any number of items. Items are evenly fanned around the center angle with ICON_SPREAD_DEGREES (45°) spacing.
val items = listOf(
RadialMenuItem(id = 1, icon = shareIcon, label = "Share"),
RadialMenuItem(id = 2, icon = heartIcon, label = "Like"),
RadialMenuItem(id = 3, icon = bookmarkIcon, label = "Save"),
RadialMenuItem(id = 4, icon = flagIcon, label = "Flag"),
RadialMenuItem(id = 5, icon = reportIcon, label = "Report")
)Note: More than 8 items may cause poor UX on small screens. A warning is logged at runtime.
RadialMenu includes built-in dark and light theme colors.
import io.github.gawwr4v.radialmenu.RadialMenuColors
// Automatic theme-aware colors (follows system dark mode)
RadialMenuWrapper(
colors = RadialMenuColors.autoTheme()
) { /* ... */ }
// Force dark
RadialMenuWrapper(
colors = RadialMenuColors.dark()
) { /* ... */ }
// Force light
RadialMenuWrapper(
colors = RadialMenuColors.light()
) { /* ... */ }Pass a RadialMenuAnimationConfig to control timing and physics:
RadialMenuWrapper(
items = items,
onItemSelected = { /* ... */ },
animationConfig = RadialMenuAnimationConfig.bouncy()
) { /* content */ }| Preset | Description |
|---|---|
RadialMenuAnimationConfig.default() |
Smooth, balanced |
RadialMenuAnimationConfig.snappy() |
Fast and responsive |
RadialMenuAnimationConfig.bouncy() |
Spring physics |
RadialMenuAnimationConfig.slow() |
Deliberate, cinematic |
RadialMenuAnimationConfig(
openDurationMs = 250,
closeDurationMs = 150,
itemScaleDurationMs = 80,
selectedItemScale = 1.6f,
enableSpringAnimation = true,
springDampingRatio = Spring.DampingRatioMediumBouncy,
springStiffness = Spring.StiffnessMedium
)Show a count or custom text badge on any item:
RadialMenuItem(id = 1, icon = notifIcon, label = "Notifications", badgeCount = 5)
RadialMenuItem(id = 2, icon = updateIcon, label = "Updates", badgeText = "NEW")Badges automatically display as a small red circle at the top-right of the item icon. Counts > 99 display as "99+".
| XML Attribute | Format | Default Value | Description |
|---|---|---|---|
rm_accentColor |
color |
White |
The background color of the selected item. |
rm_menuRadius |
dimension |
90dp |
Radius distance from center to icons. |
rm_iconSize |
dimension |
32dp |
Icon width and height. |
rm_overlayColor |
color |
Black 50% |
Background dimming scrim color. |
rm_badgeColor |
color |
#FF4444 |
Badge background color. |
rm_animationDurationMs |
integer |
100 |
Item scale animation duration in ms. |
A naive radial menu typically spawns symmetrically around the touch point. This can lead to menu items being cut off by the screen edges or pointing directly "downward," thereby obscuring the icons underneath the user's thumb/wrist.
The Solution: RadialMenu calculates a dynamic "center angle" based on the absolute spatial (x,y) location of the touch relative to screen dimensions.
- The
base anglesmoothly interpolates from 330° (far left edge) → 270° (center) → 210° (far right edge). - Aggressive edge-boosting ensures menu nodes steeply fan outwards or entirely inward near the absolute edge.
- The top-adjust variable keeps the top 25% boundary clear.
- Overall calculation ensures icons never exceed downwards bounds (only
195°to345°are permitted).
Edge-hug layout is opt-in. You must pass
enableEdgeHugLayout = truetoRadialMenuWrapperor setradialMenuView.enableEdgeHugLayout = trueon the View to activate it. The default isfalse.
When the user long-presses within EDGE_THRESH_DP (default: 80dp) of two screen edges simultaneously (i.e., a corner), and the menu has 4 or more items, the radial fan cannot fit in the available space. In this case, RadialMenu switches to edge-hug mode:
- Items arrange in an L-shape along the two adjacent edges of the corner.
- The primary edge gets
ceil(n/2)items, the secondary edge gets the remainder. - The corner cell (the exact intersection point) is always left vacant - items start one full step away.
- Selection switches from angle-based to nearest-item distance to match the non-radial arrangement.
If item count is 3 or fewer, the standard radial layout is always used, even in a corner, because 3 items can fit comfortably in a single quadrant.
Configuration (in RadialMenuDefaults):
| Constant | Default | Description |
|---|---|---|
enableEdgeHugLayout |
false |
Opt-in to L-shaped edge-hug layout in screen corners (4+ items) |
EDGE_THRESH_DP |
80f |
Distance from screen edge (dp) that defines the corner zone |
CORNER_ITEM_THRESHOLD |
3 |
Max items that still use radial layout in a corner (4+ triggers edge-hug) |
Menu items always render above all other UI elements, including toolbars, navigation bars, FABs, and bottom sheets. This is handled automatically - no setup required from the developer.
- Compose: The
RadialMenuOverlayrenders inside aPopup, which sits above all other composables in the window. - Android View: When the menu opens, an overlay is attached to the window's decor view, ensuring it draws above the entire view hierarchy. This requires an Activity context. If the context is not an Activity (e.g., Dialog or Service), the overlay falls back to rendering within the current view hierarchy.
Edge-hug zone detection uses the usable content area (excluding system bars) rather than raw screen dimensions, so it only activates at true screen corners, not near in-app UI element edges.
| Feature | RadialMenu | Others |
|---|---|---|
| Platforms | ✅ Android + Desktop JVM | ❌ Android only |
| Kotlin Multiplatform | ✅ | ❌ |
| Compose Multiplatform API | ✅ Android + Desktop | ❌ |
| Traditional View API | ✅ Android | ✅ Android |
| Edge-aware positioning | ✅ Never clips | ❌ |
| Drag-to-select gesture | ✅ | ❌ Click only |
| Haptic feedback | ✅ | ❌ |
| Badge support | ✅ | ❌ |
| RTL support | ✅ | ❌ |
| Dark/light/auto theme | ✅ | ❌ |
| Spring physics animations | ✅ | ❌ |
| External dependencies | 0 | JitPack required |
| Published on Maven Central | ✅ | ❌ |
- 💬 GitHub Discussions, questions, ideas, show and tell
- 🐛 Issues, bug reports and feature requests
- 🤝 Contributing, how to contribute
Copyright 2026 gawwr4v
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
RadialMenu is what developers are looking for when they search for:
- Android radial menu
- Desktop radial menu
- Kotlin Multiplatform radial menu
- Android circular menu
- Compose Multiplatform circular menu
- Android pie menu
- Desktop pie menu
- KMP context menu
- Pinterest long press menu Android
- Kotlin circular menu
- Compose radial menu
- Android floating action menu
- Multiplatform gesture menu
- KMP radial menu
- Compose Desktop circular menu
- Jetpack Compose radial menu

