Compose UI:Slider

投稿日:  更新日:

Jetpack composeは、アプリ開発に必要な一通りのUIコンポーネントをライブラリで提供しています。

そのライブラリ中のSliderについて、構成や使用方法などをまとめます。

※環境:Android Studio Hedgehog | 2023.1.1 Patch 2
    Kotlin 1.9.0
    Compose Compiler 1.5.1
    androidx.compose.material3:material3 1.1.1

スポンサーリンク

UIの概要

Sliderはつまみ(Thumb)をドラッグして、バー(Track)上を左右に移動させることで、値の入力を行うUIです。

@Preview_mdpi
@Composable
fun SliderBasic() {
    Row(
        modifier = Modifier.size(320.dp, 120.dp).padding(20.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        var _value by remember { mutableFloatStateOf(0.5f) }
        Text(text = "Value [ %5.2f ]".format(_value))
        Slider(
            value = _value,
            onValueChange = { _value = it }
        )
    }
}
スポンサーリンク

関数の引数

Sliderの状態(装飾、演出など)は、引数により指定できます。

タイプ1タイプ2(@ExperimentalMaterial3Api付き)
@Composable
fun Slider(
    value: Float,
    onValueChange: (Float) -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
    /*@IntRange(from = 0)*/
    steps: Int = 0,
    onValueChangeFinished: (() -> Unit)? = null,
    colors: SliderColors = SliderDefaults.colors(),
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) { ... }
@Composable
@ExperimentalMaterial3Api
fun Slider(
    value: Float,
    onValueChange: (Float) -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
    onValueChangeFinished: (() -> Unit)? = null,
    colors: SliderColors = SliderDefaults.colors(),
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    thumb: @Composable (SliderPositions) -> Unit = { ... },
    track: @Composable (SliderPositions) -> Unit = { ... },
    /*@IntRange(from = 0)*/
    steps: Int = 0,
) { ... }
引数概要
valueFloat現在の値
onValueChange(Float) -> Unit値の変更時に実行されるラムダ式
(引数で値の取得が可能)
modifierModifierUI全般のCompose修飾子
enableBooleanUI有効フラグ(true:有効 / false:無効)
valueRangeClosedFloatingPointRange値の範囲(最小...最大)
例:-0.5...0.5
stepsIntノッチ(目盛り)数(0の時はノッチ無し)
例:steps←5:min-1-2-3-4-5-max
onValueChangeFinished(( ) -> Unit)?ドラッグ⇒リリース時に実行されるラムダ式
colorsSliderColorsスライダーの配色
・つまみ(thumbColor)
・アクティブのバー(activeTrackColor)
・非アクティブのバー(inactiveTrackColor)
interactionSourceMutableInteractionSourceユーザが行ったUI操作の観測と報告
thumb@Composable
(SliderPositions) -> Unit
スライダーのつまみ
(Composable関数のコマンドブロック)
track@Composable
(SliderPositions) -> Unit
スライダーのバー
(Composable関数のコマンドブロック)
スポンサーリンク

関数の構成

Sliderは2つのBoxを重ね合わせた構成になっています。

一つのBoxはスライダーのバー(SliderDefaults.Track)を格納し、もう一つはスライダーのつまみ(SliderDefaults.Thumb)を格納します。

これらが、Layout(Composable関数)により配置されています。

Slider関数の構成

@Composable
fun Slider( ... ) {
    require(steps >= 0) { "steps should be >= 0" }

    SliderImpl(
        ... ,
        thumb = {
            SliderDefaults.Thumb(
                interactionSource = interactionSource,
                colors = colors,
                enabled = enabled
            )
        },
        track = { sliderPositions ->
            SliderDefaults.Track(
                colors = colors,
                enabled = enabled,
                sliderPositions = sliderPositions
            )
        }
    )
}

@Composable
private fun SliderImpl(
    ... 
    thumb: @Composable (SliderPositions) -> Unit,
    track: @Composable (SliderPositions) -> Unit
) {
    ...
    Layout(
        {
            Box(modifier = Modifier.layoutId(SliderComponents.THUMB)) { thumb(sliderPositions) }
            Box(modifier = Modifier.layoutId(SliderComponents.TRACK)) { track(sliderPositions) }
        },
        ...
    ) { measurables, constraints ->
        ...
        layout(
            sliderWidth,
            sliderHeight
        ) {
            trackPlaceable.placeRelative(	// 先に来るものが下、Track:バー
                trackOffsetX,
                trackOffsetY
            )
            thumbPlaceable.placeRelative(	// 後に来るものが上、Thumb:つまみ
                thumbOffsetX,
                thumbOffsetY
            )
        }
    }
}
スポンサーリンク

ノッチ(目盛り)の表示

引数:stepsでノッチ(目盛り)を表示することが出来ます。

ノッチを表示することで、つまみの位置はノッチの位置に限定されます。つまり、ノッチ間の値は入力できません。

「steps = 4」とした場合、ノッチは4つです。その結果、レンジ(引数:valueRangeで指定、最小と最大の幅)は5等分されることになります。

@Preview_mdpi
@Composable
fun StepsSample() {
    Row(
        modifier = Modifier
            .size(320.dp, 120.dp)
            .padding(20.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        var _value by remember { mutableFloatStateOf(0.4f) }
        Text(text = "Value [ %5.2f ]".format(_value))
        Slider(
            value = _value,
            onValueChange = { _value = it },
            steps = 4
        )
    }
}

スライダーのノッチ(目盛り)

ちなみに、デフォルトは「steps = 0」です。ノッチ無しを意味します。

スポンサーリンク

配色の変更

引数:colorsで配色を変更できます。

Sliderはマテリアルデザインに準拠しています。ですので、デフォルト(SliderDefaults.colors()の出力)はテーマで指定されたマテリアルデザインの配色になります。

@Preview_mdpi
@Composable
fun ColorsSample() {
    Row(
        modifier = Modifier
            .size(320.dp, 120.dp)
            .padding(20.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        var _value by remember { mutableFloatStateOf(0.4f) }
        Text(text = "Value [ %2.1f ]".format(_value))
        Slider(
            value = _value,
            onValueChange = { _value = it },
            steps = 4,
            colors = SliderDefaults.colors(
                thumbColor = Color(0xffff0000),			// つまみの色
                activeTrackColor = Color(0xffffb0b0),	// バーの色
                activeTickColor = Color(0xff000000),	// ノッチの色
                inactiveTrackColor = Color(0xffe0e0e0),
                inactiveTickColor = Color(0xff808080)
            )
        )
    }
}

配色の変更

スポンサーリンク

つまみとバーの変更(タイプ2を使用)

Slider(タイプ2)は引数:thumbでつまみを変更できます。また、引数:trackでバーを変更できます。

引数:thumbとtrackの値はComposable関数のコマンドブロック(スロット)です。この中で、つまみとバーのUIコンポーネントを記述します。

【注意】

タイプ2は「アノテーション:@ExperimentalMaterial3Api」付きです。

@ExperimentalMaterial3Apiは「実験的なAPI」を意味します。

ですので、今後のリリースで変更や削除の可能性があります。その点を理解した上で使用する必要があります。

使用に際して、上記の点についての許諾(アノテーション:@OptIn)が求められます。

Thumb(つまみ)の作成

Thumb(つまみ)のUIコンポーネント(Composable関数)を作成します。

※コマンドブロックの引数:sliderPositionsは未使用

HeartSliderColors
@Stable
class HeartSliderColors private constructor(
    val thumbColor: Color,
    val disableThumbColor: Color,
    val activeTrackColor: Color,
    val disabledActiveTrackColor: Color,
    val inactiveTrackColor: Color,
    val disabledInactiveTrackColor: Color,
    val activeTickColor: Color,
    val disabledActiveTickColor: Color,
    val inactiveTickColor: Color,
    val disabledInactiveTickColor: Color
) {
    companion object Factory {
        @Composable
        fun create(
            thumbColor: Color = MaterialTheme.colorScheme.primary,
            disabledThumbColor: Color = MaterialTheme.colorScheme.onSurface,
            activeTrackColor: Color = MaterialTheme.colorScheme.primary,
            disabledActiveTrackColor: Color = MaterialTheme.colorScheme.onSurface,
            inactiveTrackColor: Color = MaterialTheme.colorScheme.surfaceVariant,
            disabledInactiveTrackColor: Color = MaterialTheme.colorScheme.onSurface,
            activeTickColor: Color = MaterialTheme.colorScheme.onPrimary,
            disabledActiveTickColor: Color = MaterialTheme.colorScheme.onSurface,
            inactiveTickColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
            disabledInactiveTickColor: Color = MaterialTheme.colorScheme.onSurface
        ): HeartSliderColors {
            return HeartSliderColors(
                thumbColor, disabledThumbColor,
                activeTrackColor, disabledActiveTrackColor,
                inactiveTrackColor, disabledInactiveTrackColor,
                activeTickColor, disabledActiveTickColor,
                inactiveTickColor, disabledInactiveTickColor
            )
        }
    }

    @Composable
    fun thumbColor(enabled: Boolean): State<Color> {
        return rememberUpdatedState(if (enabled) thumbColor else disableThumbColor)
    }

    @Composable
    fun trackColor(enabled: Boolean, active: Boolean): State<Color> {
        return rememberUpdatedState(
            if (enabled) {
                if (active) activeTrackColor else inactiveTrackColor
            } else {
                if (active) disabledActiveTrackColor else disabledInactiveTrackColor
            }
        )
    }

    @Composable
    fun tickColor(enabled: Boolean, active: Boolean): State<Color> {
        return rememberUpdatedState(
            if (enabled) {
                if (active) activeTickColor else inactiveTickColor
            } else {
                if (active) disabledActiveTickColor else disabledInactiveTickColor
            }
        )
    }
}
@Composable
fun HeartThumb(
    sliderPositions: SliderPositions,   // 未使用
    modifier: Modifier = Modifier,
    colors: HeartSliderColors = HeartSliderColors.create(),
    enabled: Boolean = true,
    thumbSize: DpSize = DpSize(24.dp, 24.dp)
) {
    val _color = if(enabled) colors.thumbColor else colors.disableThumbColor
    Image(		// ハートアイコン:baseline_favorite_black_24
        painter = painterResource(R.drawable.baseline_favorite_black_24),
        contentDescription = null,
        modifier = modifier.size(thumbSize),
        colorFilter = ColorFilter.tint(_color, BlendMode.SrcIn)
    )
}

Track(バー)の作成

Track(バー)のUIコンポーネント(Composable関数)を作成します。

コマンドブロックの引数:sliderPositionsはつまみの位置情報を持っています。これを使って、バーの配色(Active/Inactive)を変えることができます。ただし、レンジ(最小と最大の幅)を1.0fに正規化した時の係数(0.5の時、レンジの中間)なので、注意してください。

@Composable
fun HeartTrack(
    sliderPositions: SliderPositions,
    modifier: Modifier = Modifier,
    colors: HeartSliderColors = HeartSliderColors.create(),
    enabled: Boolean = true,
) {
    val inactiveTrackColor = colors.trackColor(enabled, active = false)
    val activeTrackColor = colors.trackColor(enabled, active = true)
    val inactiveTickColor = colors.tickColor(enabled, active = false)
    val activeTickColor = colors.tickColor(enabled, active = true)
    Canvas(
        modifier
            .fillMaxWidth()
            .height(10.dp)
//            .graphicsLayer(alpha = 0.99f)
            .graphicsLayer(
                compositingStrategy = CompositingStrategy.Offscreen
            )
    ) {
        val _width = size.width
        val _height = size.height
        
        // スライドバー(Inactive)
        val _inactivePath = Path().apply {
            moveTo(0.0f, _height)
            lineTo(_width, 0.0f)
            lineTo(_width, _height)
            lineTo(0.0f, _height)
        }
        drawPath(path = _inactivePath, color = inactiveTrackColor.value)

        // スライドバー(Active)
        val _activePosition = _width * sliderPositions.activeRange.endInclusive
        val _activeSize = Size(_activePosition, _height)
        drawRect(color = activeTrackColor.value, size = _activeSize, blendMode = BlendMode.SrcIn)

        // 目盛り
        sliderPositions.tickFractions.groupBy {
            it > sliderPositions.activeRange.endInclusive
        }.forEach { (key, list) ->
            list.forEach {
                val _color = if(key) inactiveTickColor.value
                else activeTickColor.value
                drawLine(
                    color = _color,
                    start = Offset(_width * it, 0.0f),
                    end = Offset(_width * it, _height),
                    blendMode = BlendMode.SrcIn
                )
            }
        }
    }
}

ThumbとTrackの指定

引数:thumbとtrackへ作成したUIコンポーネントを指定します。

@OptIn(ExperimentalMaterial3Api::class)
@Preview_mdpi
@Composable
fun ThumbTrackSample() {
    Row(
        modifier = Modifier
            .size(320.dp, 120.dp)
            .padding(20.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        var _value by remember { mutableFloatStateOf(0.5f) }
        Text(text = "Value [ %5.2f ]".format(_value))
        Slider(		// タイプ2
            value = _value,
            onValueChange = { _value = it },
            steps = 5,
            thumb = { sliderPositions ->
                HeartThumb(sliderPositions = sliderPositions) },
            track = { sliderPositions ->
                HeartTrack(sliderPositions = sliderPositions) }
            }
        )
    }
}

つまみとバーの変更

スポンサーリンク

派生タイプ:RangeSlider

RangeSliderは2つのつまみ(StartThumb,EndThumb)をドラッグして、バー(Track)上を左右に移動させることで、値の範囲の入力を行うUIです。

引数はSliderとほぼ同じです。

ただし、引数:valueがClosedFloatingPointRange型なっているので、範囲演算子「..」を用いた範囲値を入力します。

fun RangeSlider(
    value: ClosedFloatingPointRange<Float>,
    onValueChange: (ClosedFloatingPointRange<Float>) -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
    /*@IntRange(from = 0)*/
    steps: Int = 0,
    onValueChangeFinished: (() -> Unit)? = null,
    colors: SliderColors = SliderDefaults.colors()
) { ... }

以下は、サンプルです。

@Preview_mdpi
@Composable
fun RangeSliderBasic() {
    Column(modifier = Modifier.size(320.dp, 120.dp).padding(20.dp)) {
        var _range by remember { mutableStateOf(0.2f..0.8f) }
        val _start = _range.start
        val _end = _range.endInclusive
        Text(text = "Range [ %5.2f~%5.2f ]".format(_start, _end))
        RangeSlider(
            value = _range,
            onValueChange = { range -> _range = range }
        )
    }
}

レンジの開始点と終了点が同じになる時、つまみが重なってしまいます。重なったつまみのドラッグは、対象を間違えることがあります。操作性を損なう結果になるので注意してください。

スポンサーリンク

関連記事:

Jetpack composeは、アプリ開発に必要な一通りのUIコンポーネントをライブラリで提供しています。 そのライブラリ中のButtonについて、構成や使用方法などをまとめます。 ※環境:Android Studio Flamingo | 2022.2.1    :androidx.compose.material3:material3:1.1.1    :androidx.compose.ui:ui:1.4.3 ...
Jetpack composeは、アプリ開発に必要な一通りのUIコンポーネントをライブラリで提供しています。 そのライブラリ中のSurfaceについて、構成や使用方法などをまとめます。 ※環境:Android Studio Flamingo | 2022.2.1    :androidx.compose.material3:material3:1.1.1    :androidx.compose.ui:ui:1.4.3 ...
Jetpack composeは、アプリ開発に必要な一通りのUIコンポーネントをライブラリで提供しています。 そのライブラリ中のRadioButtonについて、構成や使用方法などをまとめます。 ※環境:Android Studio Hedgehog | 2023.1.1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
Jetpack composeは、アプリ開発に必要な一通りのUIコンポーネントをライブラリで提供しています。 そのライブラリ中にUI要素を格納する機能をもつコンテナ型のUIがあります。 Column/Row/Boxがコンテナ型のUIです。構成や使用方法などをまとめます。 ※環境:Android Studio Hedgehog | 2023.1.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
Jetpack composeは、アプリ開発に必要な一通りのUIコンポーネントをライブラリで提供しています。 そのライブラリ中のImageについて、構成や使用方法などをまとめます。 ※環境:Android Studio Hedgehog | 2023.1.1 Patch 2     Kotlin 1.9.0     Compose Compiler 1.5.1     androidx.compose.foundation 1.5.0 ...
Jetpack composeは、アプリ開発に必要な一通りのUIコンポーネントをライブラリで提供しています。 そのライブラリ中のDividerについて、構成や使用方法などをまとめます。 ※環境:Android Studio Hedgehog | 2023.1.1 Patch 2     Kotlin 1.9.0     Compose Compiler 1.5.1     androidx.compose.material3:material3 1.1.1 ...
Jetpack composeは、アプリ開発に必要な一通りのUIコンポーネントをライブラリで提供しています。 そのライブラリ中のSpacerについて、構成や使用方法などをまとめます。 ※環境:Android Studio Hedgehog | 2023.1.1 Patch 2     Kotlin 1.9.0     Compose Compiler 1.5.1     androidx.compose.foundation 1.5.0 ...
スポンサーリンク