Slider class
Material Design 슬라이더.
다양한 값 중에서 선택하는 데 사용됩니다.
공식 문서 코드
앱에서 음량 조절을 하게 만들고 싶을 수 있다. 그럴 때에는 음량 조절을 어떻게 하게 만들어야 할까? 사용자가 직접 값을 입력? 물론 좋다. 하지만 뭐니뭐니 해도 예로부터 음량 조절은 드래그로 값을 조절할 수 있는 '바'로 만드는 것이 국룰이었다.
여기서 Slider 위젯이 그 역할을 아주 간단하게 만들어줄 수 있다. Slider 는 값의 범위를 신속하게 바꾸고 싶고 사용자 경험을 더 만족시켜 줄 수 있는 위젯이다. Slider 를 알아보면서 코틀린과 연결하여 음량 조절 기능도 만들어보자.
하위 속성
| 속성명 | 타입 | 기본값 | 설명 |
| value | double | – | 현재 슬라이더의 값으로 반드시 min과 max 사이여야 함 |
| onChanged | ValueChanged<double>? | null | 값이 변경될 때 호출되는 콜백, null이면 슬라이더가 비활성화됨 |
| onChangeStart | ValueChanged<double>? | null | 사용자가 드래그를 시작할 때 호출되는 콜백 |
| onChangeEnd | ValueChanged<double>? | null | 사용자가 드래그를 마쳤을 때 호출되는 콜백 |
| min | double | 0.0 | 선택 가능한 최소 값 |
| max | double | 1.0 | 선택 가능한 최대 값 |
| divisions | int? | null | 슬라이더를 나눌 구간 수, null이면 연속형 슬라이더 |
| label | String? | null | 현재 값을 나타내는 레이블, 값 인디케이터로 표시됨 |
| activeColor | Color? | null | 선택된 범위의 트랙 색상 |
| inactiveColor | Color? | null | 선택되지 않은 범위의 트랙 색상 |
| secondaryTrackValue | double? | null | 보조 트랙을 표시할 값, buffering 등을 표현할 때 사용 |
| secondaryActiveColor | Color? | null | 보조 트랙에 사용할 색상 |
| thumbColor | Color? | null | 슬라이더 썸(손잡이)의 색상 |
| overlayColor | MaterialStateProperty<Color?>? | null | hover, focus 상태일 때 오버레이 색상 |
| mouseCursor | MouseCursor? | null | 마우스 커서 스타일 |
| semanticFormatterCallback | SemanticFormatterCallback? | null | 접근성 화면 읽기를 위한 값 포맷터 |
| focusNode | FocusNode? | null | 슬라이더의 포커스를 관리하는 노드 |
| autofocus | bool | false | 초기 빌드시 자동 포커스를 받을지 여부 |
| allowedInteraction | SliderInteraction? | tapAndSlide | 사용자가 슬라이더와 상호작용하는 방식 설정 |
| padding | EdgeInsetsGeometry? | null | 슬라이더 주변 여백을 설정 |
일단 사용하는 방법은 굉장히 쉽다. 조절할 값을 만들어주고 어떻게 조절하는지만 입력하면 된다.
double sliderValue = 0;
일단 가장 먼저 하위 속성인 value 안에 들어갈 double 타입의 값을 만들어줘야 한다. 여기서 입력했던 값은 Slider 의 초기값이 되겠다. (당연하지만 final 로 선언하면 적용 안됨)
Slider(
value: sliderValue,
onChanged: (value) {
setState(() {
sliderValue = value;
});
},
),
이게 끝이다. value 에 만들어둔 값을 넣어주고 이제 onChanged 에서는 value 로 현재 슬라이더의 Thumb 가 위치해 있는 곳의 값을 넘겨주기 때문에 초기값에 대입해 주면 간단하게 바로 슬라이더를 만들 수가 있다. 여기서 많이 쓰는 속성들을 좀 정리해 주겠다.
먼저 가장 많이 쓰는 하위 속성을 알아보겠다. (사실 바로 밑에 건 반필수 이긴 하다.)
Slider(
min: 0, // 최소
max: 100, // 최대
label: sliderValue.round().toString(),
value: sliderValue,
onChanged: (value) {
setState(() {
sliderValue = value;
});
},
),
그냥 보자마자 알겠지만 말 그대로 최솟값과 최댓값을 설정해 줄 수 있다. 이렇게 하면 정해진 범위 내에서 슬라이더가 동작하게 된다.
그다음은 label 이다. 이 값은 Thumb 가 현재 위치해 있는 곳의 값을 UI 로 표시해 준다. 다만 단독으로는 사용 못 하고 지점을 나눌 수 있게 해주는 divisions 를 사용해줘야 한다.
Slider(
divisions: 10, // max / 10 해서 현코드에서는 10 단위
min: 0,
max: 100, // divisions 사용 시 필수 값
label: sliderValue.round().toString(), // round() 로 소수점 출력제한
value: sliderValue,
onChanged: (value) {
setState(() {
sliderValue = value;
});
},
),

솔직히 이것들만 알면 다 안거다. 바로 한번 코틀린과 연동시켜서 음량 조절기능을 구현해 보자.
기본적으로 효과음 크기를 조절한다고 해보자.
(전체코드)
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(MaterialApp(home: Test()));
class Test extends StatefulWidget {
const Test({super.key});
@override
State<Test> createState() => _TestState();
}
class _TestState extends State<Test> {
double volume = 50.0;
Future<void> setVolume(double volume) async {
await MethodChannel('com.example.test').invokeMethod('setVolume', {"volume": volume});
}
Future<void> playSound(String fileName) async {
await MethodChannel('com.example.test').invokeMethod('playSound', fileName);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
RotatedBox(
quarterTurns: -1,
child: Slider(
value: volume,
min: 0,
max: 100,
onChanged: (value) {
setState(() {
volume = value;
setVolume(volume);
});
},
),
),
Text(volume.round().toString()),
ElevatedButton(
onPressed: () => playSound("mario_death"),
child: Text("Play"),
),
],
),
),
);
}
}
일단 먼저 슬라이더 부분인데 뭐 다를 것 없다.
double volume = 50.0;
RotatedBox(
quarterTurns: -1, // 왼쪽으로 90도 회전
child: Slider(
value: volume,
min: 0,
max: 100,
onChanged: (value) {
setState(() {
volume = value;
setVolume(volume); // 코틀린에 현재 값 전달
});
},
),
),
초기 값 생성해 주고~ 최소/최댓값 지정해 주고~ 슬라이더에 넣어주고~ 이렇게 하면 금방 만들 수 있다.
그리고 효과음 조절 기능이기도 하고 심심해서 RotatedBox 사용해서 세로로 돌려줬다.

Future<void> setVolume(double volume) async {
await MethodChannel('com.example.test').invokeMethod('setVolume', {"volume": volume});
}
Future<void> playSound(String fileName) async {
await MethodChannel('com.example.test').invokeMethod('playSound', fileName);
}
이제 요 친구들은 둘 다 MethodChannel 사용해서 네이티브와 통신을 해줬다. setVolumn 은 현재 디바이더 값을 넘겨줘서 소리 크기를 조절할 수 있게 해 주고 playSound 는 말 그대로 효과음 재생시켜 주게 한다.
이제 핵심인 코틀린 코드다.
(전체코드)
package com.example.test
import android.media.MediaPlayer
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity() {
private val CHANNEL = "com.example.test"
private var volume: Float = 1.0f
private var mediaPlayer: MediaPlayer? = null
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result ->
when (call.method) {
"setVolume" -> {
val volumeDouble = call.argument<Double>("volume") ?: 100.0
volume = (volumeDouble.coerceIn(0.0, 100.0) / 100.0).toFloat()
result.success(null)
}
"playSound" -> {
val fileName = call.arguments as String
playSound(fileName)
result.success(null)
}
else -> result.notImplemented()
}
}
}
private fun playSound(fileName: String) {
val resId = getResId(fileName)
if (resId != 0) {
mediaPlayer?.release()
mediaPlayer = MediaPlayer.create(this, resId)
mediaPlayer?.setVolume(volume, volume)
mediaPlayer?.setOnCompletionListener {
it.release()
mediaPlayer = null
}
mediaPlayer?.start()
}
}
private fun getResId(fileName: String): Int {
return when (fileName) {
"mario_death" -> R.raw.mario_death
else -> 0
}
}
override fun onDestroy() {
mediaPlayer?.release()
super.onDestroy()
}
}
최대한 쉽게 만들었다. 그리고 패키지 이름은 꼭 자기 것으로 수정해 주길 바란다. (com.example.test -> 본인 패키지 이름)
private var volume: Float = 1.0f
private var mediaPlayer: MediaPlayer? = null
효과음을 담당해 줄 volume 과 그 효과음 재생용 mediaPlayer 를 써줬다.
그리고 이제 MethodChannel 을 썼다면 필연적으로 나오는 '그' 코드를 써준다.
// Flutter 엔진이 구성될 때 호출됨
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result ->
when (call.method) {
"setVolume" -> { // 볼륨 설정 요청
val volumeDouble = call.argument<Double>("volume") ?: 100.0
volume = (volumeDouble.coerceIn(0.0, 100.0) / 100.0).toFloat()
result.success(null)
}
"playSound" -> { // 효과음 재생 요청
val fileName = call.arguments as String
playSound(fileName)
result.success(null)
}
else -> result.notImplemented() // 오류 처리
}
}
}
아까 Flutter 에서 보내준 MethodChannel 을 전달받고 메서드들을 실행시켜 준다.
이제 마지막으로 그 메소드들를 봐보자면
private fun playSound(fileName: String) { // 전달된 파일 이름으로 효과음 재생
val resId = getResId(fileName) // 파일 이름으로 리소스 ID 찾기
if (resId != 0) {
mediaPlayer?.release()
mediaPlayer = MediaPlayer.create(this, resId)
mediaPlayer?.setVolume(volume, volume) // 현재 볼륨 적용
mediaPlayer?.setOnCompletionListener {
it.release()
mediaPlayer = null
}
mediaPlayer?.start() // 재생 시작
}
}
당연한 건지 모르겠지만 일단 파일하나를 디렉터리에 위치시켜 놔야 한다.
경로는 이렇다.
android/app/src/main/res/raw/mario_death.mp3

raw 디렉토리가 없다면 만들면 된다. 그리고 재생할 파일을 넣어주자.
마리오 죽는 소리인데 난 이걸로 했다.
암튼 이제 방금 위에서 호출했던 getResId 메서드를 만들어주면 된다.
private fun getResId(fileName: String): Int { // 파일 이름을 통해 raw 리소스의 ID를 반환
return when (fileName) {
"mario_death" -> R.raw.mario_death // "mario_death" 파일 이름 매칭
else -> 0
}
}
override fun onDestroy() { // 액티비티 종료 시 리소스 정리
mediaPlayer?.release()
super.onDestroy()
}
이렇게 해서 실행해 보면 굉장히 잘 된다는 것을 알 수 있다.

안타깝지만 gif 는 소리를 못 넣기 때문에 직접 만들어서 시험해 보길 바란다. 도움이 되었길 바라며 마치겠다.