Skip to content

sonms/Velvet-Compose

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

30 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🎑 Velvet-Compose WheelPicker

Maven Central License API

Bring the classic iOS 3D wheel picker to your Jetpack Compose app.
Supports infinite scrolling, 3D graphic layers, and full style customization.


πŸ“Έ Preview

Vertical Horizontal

πŸš€ Getting Started

Gradle

Add the dependency to your build.gradle.kts:

dependencies {
    implementation("io.github.sonms:wheelpicker:0.0.1")
}

πŸ“– Usage

Basic Usage

val items = remember { (1..12).map { it.toString().padStart(2, '0') } }
val state = rememberWheelPickerState(initialIndex = 0)

VerticalWheelPicker(
    items = items,
    state = state,
    visibleItemCount = 5,
    infinite = true,
    onItemSelected = { index, item ->
        // handle selection
    },
) { item, isSelected ->
    Text(
        text = item,
        fontSize = if (isSelected) 20.sp else 16.sp,
        fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
        color = if (isSelected) Color.Black else Color.Gray,
    )
}

Time Picker Sample (AM/PM + Hour + Minute)

@Composable
fun TimePickerSample() {
    val amPmItems = remember { listOf("AM", "PM") }
    val hourItems = remember { (1..12).map { it.toString().padStart(2, '0') } }
    val minuteItems = remember { (0..59).map { it.toString().padStart(2, '0') } }

    val amPmState = rememberWheelPickerState(initialIndex = 0)
    val hourState = rememberWheelPickerState(initialIndex = 0)
    val minuteState = rememberWheelPickerState(initialIndex = 0)

    val itemHeight = 48.dp

    Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
        // Custom selector spanning all three pickers
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 16.dp)
                .height(itemHeight)
                .background(
                    color = Color.Gray.copy(alpha = 0.15f),
                    shape = RoundedCornerShape(12.dp),
                )
        )

        Row(
            horizontalArrangement = Arrangement.Center,
            verticalAlignment = Alignment.CenterVertically,
        ) {
            VerticalWheelPicker(
                items = amPmItems,
                state = amPmState,
                modifier = Modifier.width(80.dp),
                itemHeight = itemHeight,
                visibleItemCount = 5,
                infinite = false,
                style = WheelPickerDefaults.style(
                    selector = WheelPickerDefaults.selectorStyle(
                        background = Color.Transparent,
                        showDivider = false,
                    ),
                ),
            ) { item, isSelected ->
                Text(
                    text = item,
                    fontSize = if (isSelected) 20.sp else 16.sp,
                    color = if (isSelected) Color.Black else Color.Gray,
                )
            }

            VerticalWheelPicker(
                items = hourItems,
                state = hourState,
                modifier = Modifier.width(80.dp),
                itemHeight = itemHeight,
                visibleItemCount = 5,
                infinite = true,
                style = WheelPickerDefaults.style(
                    selector = WheelPickerDefaults.selectorStyle(
                        background = Color.Transparent,
                        showDivider = false,
                    ),
                ),
            ) { item, isSelected ->
                Text(
                    text = item,
                    fontSize = if (isSelected) 20.sp else 16.sp,
                    color = if (isSelected) Color.Black else Color.Gray,
                )
            }

            VerticalWheelPicker(
                items = minuteItems,
                state = minuteState,
                modifier = Modifier.width(80.dp),
                itemHeight = itemHeight,
                visibleItemCount = 5,
                infinite = true,
                style = WheelPickerDefaults.style(
                    selector = WheelPickerDefaults.selectorStyle(
                        background = Color.Transparent,
                        showDivider = false,
                    ),
                ),
            ) { item, isSelected ->
                Text(
                    text = item,
                    fontSize = if (isSelected) 20.sp else 16.sp,
                    color = if (isSelected) Color.Black else Color.Gray,
                )
            }
        }
    }
}

🎨 Customization

Style

WheelPickerDefaults.style() provides full customization:

VerticalWheelPicker(
    items = items,
    style = WheelPickerDefaults.style(
        selector = WheelPickerDefaults.selectorStyle(
            background = Color.Gray.copy(alpha = 0.15f),
            shape = RoundedCornerShape(12.dp),
            showDivider = true,
            dividerColor = Color.Gray,
            dividerThickness = 1.dp,
        ),
        fade = WheelPickerDefaults.fadeStyle(
            fraction = 0.3f,
            enabled = true,
        ),
        transform = WheelPickerDefaults.transformStyle(
            rotationEnabled = true,
            maxRotationDegree = 30f,
            scaleEnabled = true,
            minScale = 0.85f,
            alphaEnabled = true,
            minAlpha = 0.7f,
        ),
    ),
) { item, isSelected -> }

Selector Customization

There are two ways to customize the selector area:

1. Draw a custom Box externally

You can draw a Box outside the picker to create a selector that spans multiple pickers:

Box(
    modifier = Modifier
        .fillMaxWidth()
        .padding(horizontal = 16.dp)
        .height(itemHeight)
        .background(
            color = Color.Gray.copy(alpha = 0.15f),
            shape = RoundedCornerShape(12.dp),
        )
)

2. Use the built-in selector via WheelPickerDefaults.selectorStyle

VerticalWheelPicker(
    style = WheelPickerDefaults.style(
        selector = WheelPickerDefaults.selectorStyle(
            background = Color.Gray.copy(alpha = 0.15f),
            shape = RoundedCornerShape(12.dp),
            showDivider = true,
            dividerColor = Color.Gray,
            dividerThickness = 1.dp,
        ),
    ),
) { item, isSelected -> }

State Control

Control the picker programmatically using WheelPickerState:

val state = rememberWheelPickerState(initialIndex = 0)

// Read current index
val currentIndex = state.currentIndex

// Scroll without animation
LaunchedEffect(Unit) {
    state.scrollToIndex(3)
}

// Scroll with animation
LaunchedEffect(Unit) {
    state.animateScrollToIndex(3)
}

πŸ“‹ API Reference

VerticalWheelPicker

Parameter Type Default Description
items List<T> required List of items to display
modifier Modifier Modifier Modifier
state WheelPickerState rememberWheelPickerState() State holder
itemHeight Dp 48.dp Height of each item
visibleItemCount Int 5 Number of visible items (odd recommended)
infinite Boolean true Enable infinite scrolling
style WheelPickerStyle WheelPickerDefaults.style() Style configuration
onItemSelected (Int, T) -> Unit {} Callback when item is settled
itemContent @Composable (T, Boolean) -> Unit required Item UI slot

HorizontalWheelPicker

Same as VerticalWheelPicker but with itemWidth instead of itemHeight.

WheelPickerState

Property / Function Description
currentIndex Currently selected item index
isScrollInProgress Whether scrolling is in progress
scrollToIndex(index) Scroll to index without animation
animateScrollToIndex(index) Scroll to index with animation

πŸ“„ License

Copyright 2026 sonms

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

    https://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.

About

Bring the classic iOS 3D wheel picker to your Jetpack Compose appπŸš€ Features infinite scrolling and dynamic 3D graphic layers.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages